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 символов ниже.

Любое предложение?

Спасибо, дем

3 ответа

Я наблюдаю нечто подобное с некоторыми ответами, которые не кэшируются NSURLCache, и у меня есть другая возможная причина:

В моем случае я смог удостовериться, что ответы, которые не кэшируются, - это те ответы, которые возвращаются с использованием Chunked-кодировки передачи. В другом месте я читал, что NSURLCache должен кэшировать их после iOS 6, но по некоторым причинам этого не происходит в моем случае (iOS 7.1 и 8.1).

Я вижу, что ваш пример ответа, показанный здесь, также имеет Transfer-Encoding: chunked заголовок.

Может ли быть так, что некоторые из ваших ответов возвращаются с чанкованным кодированием (те, которые не кэшируются), а некоторые нет (те, которые кэшируются)?

Мой сервер также работает с PHP на Apache, и я до сих пор не могу понять, почему он это делает... Возможно, какое-то расширение Apache...

Во всяком случае, я думаю, что это звучит более правдоподобно, чем сценарий длины URL запроса.


РЕДАКТИРОВАТЬ:

Прошло некоторое время, но я наконец могу подтвердить, что в нашем случае именно кодирование передачи по частям приводит к тому, что ответ не кэшируется. Я проверил это с iOS 7.1, 8.1, 8.3 и 8.4.

Поскольку я понимаю, что не всегда легко изменить этот параметр на вашем сервере, я предлагаю решение для людей, которые используют AFNetworking 2 и создают подклассы AFHTTPSessionManager.

Вы можете добавить свой подкласс в качестве наблюдателя для AFNetworkingTaskDidCompleteNotification от AFNetworking, который содержит все, что вам нужно для кэширования ответов самостоятельно. Это означает: задачу данных сеанса, объект ответа и данные ответа до его обработки сериализатором ответов.

Если ваш сервер использует кусочную кодировку только для нескольких своих ответов, вы можете добавить код в -(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 не обеспечивает обратный вызов или метод делегата, который возвращает все, что нам нужно, достаточно рано, то есть до того, как он был сериализован сериализатором ответов.

Надеюсь, люди найдут это полезным:)

Вы пытались настроить 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;

Вы можете выяснить, каков ваш кешированный ответ от sharedURLCache, создав подкласс NSURLProtocol и переопределив startLoading:

добавить в приложение 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 здесь.

Другие вопросы по тегам