NSURLCache:行为不一致

时间:2013-11-27 17:06:45

标签: objective-c caching nsurlcache

我观察了我的应用程序的一些奇怪的行为,有时缓存响应,有时不缓存它们(所有响应都有Cache-Control:max-age = 600)。

测试很简单:我做了一个test.php脚本,它只是设置标题并返回一个简单的JSON:

<?php
        header('Content-Type: application/json');
header('Cache-Control: max-age=600');
?>
{
    "result": {
        "employeeId": "<?php echo $_GET['eId']; ?>",
                "dateTime": "<?php echo date('Y-m-d H:i:s'); ?>'" }
}

这是我从PHP页面得到的回复:

HTTP/1.1 200 OK
Date: Thu, 28 Nov 2013 11:41:55 GMT
Server: Apache
X-Powered-By: PHP/5.3.17
Cache-Control: max-age=600
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/json

{
    "result": {
        "employeeId": "",
        "dateTime": "2013-11-28 11:41:55'" 
    }
}

然后我创建了一个简单的应用程序并添加了AFNetworking库。

当我用几个参数调用脚本时,缓存正常工作:

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

NSDictionary *params = @{ 
                         @"oId": @"4011",
                         @"eId": self.firstTest ? @"1" : @"0",
                         @"status": @"2031",
                         };
[manager GET:@"http://www.mydomain.co.uk/test.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"JSON: %@", responseObject);

    NSLog(@"Cache current memory usage (after call): %d", [cache currentMemoryUsage]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"Error: %@", error);
}];

但是当我增加参数数量时,例如:

NSDictionary *params = @{
                         @"organizationId": @"4011",
                         @"organizationId2": @"4012",
                         @"organizationId3": @"4013",
                         @"organizationId4": @"4014",
                         @"organizationId5": @"4015",
                         @"organizationId6": @"4016",
                         @"eId": self.firstTest ? @"1" : @"0",
                         @"status": @"2031",
                         };

它不再起作用,并且每次调用它时都会执行一个新请求。

我做了很多测试,在我看来它与URL的长度有关,因为如果我包含这组参数:

NSDictionary *params = @{
                         @"oId": @"4011",
                         @"oId2": @"4012",
                         @"oId3": @"4013",
                         @"oId4": @"4014",
                         @"oId5": @"4015",
                         @"oId6": @"4016",
                         @"eId": self.firstTest ? @"1" : @"0",
                         @"status": @"2031",
                         };

它有效!!

我做了很多测试,这是我发现的唯一模式......

为了从等式中排除AFNetworking,我创建了另一个仅使用NSURLConnection的测试程序,我可以看到相同的行为,因此它不是AFNetworking,绝对是NSURLCache。这是另一个测试:

NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://www.mydomain.co.uk/test.php?eId=%@&organizationId=4011&organizationId2=4012&organizationId3=4013&organizationId4=4014&organizationId5=4015&organizationId6=4016", self.firstTest ? @"1" : @"0"]];  // doesn't work
//NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://www.mydomain.co.uk/test.php?eId=%@&oId=4011&oId2=4012&oId3=4013&oId4=4014&oId5=4015&oId6=4016", self.firstTest ? @"1" : @"0"]];  // work
//NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://www.mydomain.co.uk/test.php?eId=%@", self.firstTest ? @"1" : @"0"]];  // work

NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request
                                     returningResponse:&response
                                                 error:&error];

if (error == nil) {
    // Parse data here
    NSString *responseDataStr = [NSString stringWithUTF8String:[data bytes]];
    NSLog(@"Response data: %@", responseDataStr);
}

我还尝试确定URL中有多少个字符会触发问题,但即使在这种情况下,我也会得到奇怪的结果:

这个长度为112个字符,不起作用:

http://www.mydomain.co.uk/test.php?eId=1&organizationId=4011&organizationId2=4012&organizationId3=4013&orgaId4=4

这个长度为111个字符并且有效:

http://www.mydomain.co.uk/test.php?eId=1&organizationId=4011&organizationId2=4012&organizationId3=4013&orgId4=4

我已经重命名了PHP脚本,看看URL的第一部分是否重要,我又有一个奇怪的行为:

这个长度为106个字符,不起作用:

http://www.mydomain.co.uk/t.php?eId=1&organizationId=4011&organizationId2=4012&organizationId3=4013&org=40

这个长度为105个字符并且有效:

http://www.mydomain.co.uk/t.php?eId=1&organizationId=4011&organizationId2=4012&organizationId3=4013&org=4

所以我从页面名称中删除了3个字符,并且我的工作阈值低了6个字符。

有什么建议吗?

谢谢, DEM

3 个答案:

答案 0 :(得分:4)

我正在目睹类似某些类似于NSURLCache未缓存的某些响应,我想出了另一个可能的原因:

在我的情况下,我能够确定未缓存的响应是使用 Chunked transfer-encoding 返回的响应。我在其他地方读过NSURLCache应该在iOS 6之后缓存那些但是出于某些原因它不适用于我的情况(iOS 7.1和8.1)。

我看到此处显示的示例响应也有Transfer-Encoding: chunked标题。

可能是你的一些响应是使用分块编码(那些没有缓存的)返回的,而有些则不是(那些缓存的)?

我的后端也在Apache上运行PHP,我仍然无法弄清楚为什么会这样做...... 可能是一些Apache扩展...

无论如何,我认为这听起来比请求URL长度方案更合理。


修改

已经有一段时间了,但我终于可以确认,在我们的案例中,分块传输编码会导致响应不被缓存。我已经使用iOS 7.1,8.1,8.3和8.4进行了测试。

由于我了解在服务器上更改设置并不总是那么容易,因此对于使用AFNetworking 2和继承 AFHTTPSessionManager 的人,我有一个建议的解决方案。

您可以将您的子类添加为AFNetworking的 AFNetworkingTaskDidCompleteNotification 的观察者,其中包含您自己缓存响应所需的所有内容。这意味着:会话数据任务,响应对象和响应序列化程序处理之前的响应数据。

如果您的服务器仅对其少数响应使用分块编码,则可以在 - (void)didCompleteTask:中添加代码以仅选择性地缓存响应。因此,例如,您可以检查传输编码响应标头,或根据其他条件缓存响应。

下面的示例HTTPSessionManager子类缓存所有返回任何数据的响应:

<强> MyHTTPSessionManager.h

@interface MyHTTPSessionManager : AFHTTPSessionManager


@end

<强> MyHTTPSessionManager.m

#import "MyHTTPSessionManager.h"

@implementation MyHTTPSessionManager

+ (instancetype)sharedClient {
    static MyHTTPClient *_sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [[NSNotificationCenter defaultCenter] addObserver:_sharedClient selector:@selector(didCompleteTask:) name:AFNetworkingTaskDidCompleteNotification object:nil];
    });

    return _sharedClient;
}

- (void)didCompleteTask:(NSNotification *)notification {
    NSURLSessionDataTask *task = notification.object;
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;

    NSData *responseData = notification.userInfo[AFNetworkingTaskDidCompleteResponseDataKey];
    if (!responseData.length) {
        // Do not cache empty responses.
        // You could place additional checks above to cache responses selectively.
        return;
    }

    NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:responseData];
    [[NSURLCache sharedURLCache] storeCachedResponse:cachedResponse forRequest:task.currentRequest];
}

我试图想出一些更干净的解决方案,但似乎AFNetworking没有提供回调或委托方法,它足够早地返回我们需要的所有东西 - 也就是说,在它被响应序列化器序列化之前。

希望人们会发现这有用:)

答案 1 :(得分:0)

您是否尝试配置? NSURLRequestCachePolicy NSURLRequest

+ (id)requestWithURL:(NSURL *)theURL cachePolicy:(NSURLRequestCachePolicy)cachePolicy timeoutInterval:(NSTimeInterval)timeoutInterval

这些常量用于指定与缓存响应的交互。

enum
{
 NSURLRequestUseProtocolCachePolicy = 0,
 NSURLRequestReloadIgnoringLocalCacheData = 1,
 NSURLRequestReloadIgnoringLocalAndRemoteCacheData =4,
 NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
 NSURLRequestReturnCacheDataElseLoad = 2,
 NSURLRequestReturnCacheDataDontLoad = 3,
 NSURLRequestReloadRevalidatingCacheData = 5
};
typedef NSUInteger NSURLRequestCachePolicy;

答案 2 :(得分:0)

您可以通过继承NSURLProtocol并覆盖startLoading来调查您的缓存响应来自sharedURLCache:

在AppDelegate应用程序中添加:didFinishLaunchingWithOptions:

[NSURLProtocol registerClass:[CustomURLProtocol class]];

然后创建NSURLProtocol(CustomURLProtol)的子类并覆盖startLoading

- (void)startLoading
{

self.cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];

if (self.cachedResponse) {
    [self.client URLProtocol:self
          didReceiveResponse:[self.cachedResponse response]
          cacheStoragePolicy:[self.cachedResponse storagePolicy]];
    [self.client URLProtocol:self didLoadData:[self.cachedResponse data]];
}
[self.client URLProtocolDidFinishLoading:self];
}

self.cachedResponse是我添加的属性NSCachedURLResponse。您可以在此查看任何cachedResponse是否有任何问题。