Как кэшировать с помощью NSURLSession и NSURLCache. Не работает

У меня есть тестовое приложение, и оно успешно загружает контент из сети, даже если пользователь переключает приложения во время загрузки. Отлично, теперь у меня есть фоновые загрузки. Теперь я хочу добавить кеширование. Мне нет смысла загружать изображения более одного раза из-за дизайна системы, учитывая URL-адрес изображения, я могу сказать вам, что содержимое этого URL-адреса никогда не изменится. Итак, теперь я хочу кешировать результаты моей загрузки, используя встроенный кеш Apple в памяти / на диске, о котором я так много читал (в отличие от того, что я сохраняю файл вручную в NSCachesDirectory, а затем проверяю там, прежде чем создавать новый просьба, ик). В попытке заставить кэширование работать поверх этого работающего кода я добавил следующий код:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.

    // Set app-wide shared cache (first number is megabyte value)
    [NSURLCache setSharedURLCache:[[NSURLCache alloc] initWithMemoryCapacity:60 * 1024 * 1024
                                                                diskCapacity:200 * 1024 * 1024
                                                                    diskPath:nil]];

    return YES;
}

Когда я создаю свой сеанс, я добавил две НОВЫЕ строки (URLCache и requestCachePolicy).

// Helper method to get a single session object
- (NSURLSession *)backgroundSession
{
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
        configuration.URLCache = [NSURLCache sharedURLCache]; // NEW LINE ON TOP OF OTHERWISE WORKING CODE
        configuration.requestCachePolicy = NSURLRequestReturnCacheDataElseLoad;  // NEW LINE ON TOP OF OTHERWISE WORKING CODE
        session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    });
    return session;
}

Затем, просто чтобы быть ультра избыточным в попытке увидеть успех кэширования, я переключил свою строку NSURLRequest с

// NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; // Old line, I've replaced this with...
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:2*60]; // New line

Теперь, когда я иду, чтобы загрузить элемент во второй раз, опыт точно такой же, как в первый раз! Загрузка занимает много времени, а индикатор выполнения медленно и ровно анимируется, как при исходной загрузке. Я хочу, чтобы данные были в кеше немедленно!! Что мне не хватает???

----------------------------ОБНОВИТЬ--------------------- -------

Хорошо, благодаря ответу Торстена я добавил следующие две строки кода в свой метод делегата didFinishDownloadingToURL:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL {

    // Added these lines...
    NSLog(@"DiskCache: %@ of %@", @([[NSURLCache sharedURLCache] currentDiskUsage]), @([[NSURLCache sharedURLCache] diskCapacity]));
    NSLog(@"MemoryCache: %@ of %@", @([[NSURLCache sharedURLCache] currentMemoryUsage]), @([[NSURLCache sharedURLCache] memoryCapacity]));
    /*
    OUTPUTS:
    DiskCache: 4096 of 209715200
    MemoryCache: 0 of 62914560
    */
}

Это здорово. Это подтверждает, что кеш растет. Я предполагаю, что, поскольку я использую downloadTask (загрузка в файл, а не в память), именно поэтому DiskCache растет, а не кеш памяти в первую очередь? Я полагал, что все будет помещаться в кеш памяти до тех пор, пока он не переполнится, а затем будет использоваться дисковый кеш и, возможно, кеш памяти будет записан на диск до того, как ОС убьет приложение в фоновом режиме, чтобы освободить память. Я неправильно понимаю, как работает кеш Apple?

Это, безусловно, шаг вперед, но во второй раз, когда я загружаю файл, это занимает столько же времени, сколько и в первый раз (может быть, 10 секунд или около того), и следующий метод ДЕЙСТВИТЕЛЬНО выполняется снова:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    // This shouldn't execute the second time around should it? Even if this is supposed to get executed a second time around then shouldn't it be lightning fast? It's not.
    // On all subsequent requests, it slowly iterates through the downloading of the content just as slow as the first time. No caching is apparent. What am I missing?
}

Что вы думаете о моих правках выше? Почему я не вижу, чтобы файл возвращался очень быстро при последующих запросах?

Как я могу подтвердить, что файл обслуживается из кеша по второму запросу?


person John Erck    schedule 22.02.2014    source источник


Ответы (3)


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

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Set app-wide shared cache (first number is megabyte value)
    NSUInteger cacheSizeMemory = 500*1024*1024; // 500 MB
    NSUInteger cacheSizeDisk = 500*1024*1024; // 500 MB
    NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:@"nsurlcache"];
    [NSURLCache setSharedURLCache:sharedCache];
    sleep(1); // Critically important line, sadly, but it's worth it!
}

В дополнение к строке sleep(1) также обратите внимание на размер моего кеша; 500 МБ.

Согласно документам, вам нужен размер кеша, который намного больше, чем то, что вы пытаетесь кэшировать.

Размер ответа достаточно мал, чтобы разумно поместиться в кэш. (Например, если вы предоставляете дисковый кеш, ответ не должен превышать примерно 5 % размера дискового кеша.)

Так, например, если вы хотите иметь возможность кэшировать изображение размером 10 МБ, то размера кэша 10 МБ или даже 20 МБ будет недостаточно. Вам нужно 200 МБ. Комментарий Хани ниже является доказательством того, что Apple следует этому правилу 5%. Для 8 МБ он должен был установить размер кэша не менее 154 МБ.

person John Erck    schedule 10.04.2014
comment
Я попытался использовать этот код, чтобы увидеть, будут ли маленькие эскизы возвращаться быстрее после загрузки хотя бы один раз из сети. Они не приходят быстрее. Мне интересно, действительно ли этот кеш работает. - person zumzum; 13.03.2015
comment
@zumzum Вы можете попробовать файл большего размера, чтобы увидеть, работает ли он. Большой файл дает четкое представление о том, правильно ли настроен и работает ли кеш. - person John Erck; 13.03.2015
comment
Чтобы быть более точным, документация Apple для URLSession:dataTask:willCacheResponse:completionHandler: (developer.apple.com/library/prerelease/ios/documentation/) явно указывает, что NSURLSession не будет пытаться кешировать файл размером более 5% от размера кеша - person jcaron; 26.05.2015
comment
@JohnErck Вы спите (1), потому что кеш 500 МБ? Это действительно необходимо? - person James Laurenstin; 19.06.2015
comment
@JamesLaurenstin Насколько я помню, я сплю, потому что я заметил, что без него кеш у меня не работает. Не уверен, связано ли это с размером кеша, как вы предлагаете. - person John Erck; 20.06.2015
comment
@JohnErck Любые дальнейшие разработки о том, почему sleep(1) необходим для правильной работы кеша? - person Drew Burnett; 04.02.2016
comment
@AndrewBurnett Нет, но это действительно похоже на хитрость. Дайте мне знать, если вы можете понять, почему это так. - person John Erck; 05.02.2016
comment
Спасибо @jcaron, что указали в правильном направлении. вопреки документации (указано 5% от размера дискового кеша) мне также пришлось увеличить размер памяти, чтобы заставить кеш работать для больших ответов. - person benrudhart; 24.03.2017
comment
Я кэшировал изображение размером 8 МБ, однако это не сработало бы, если бы размер моего кеша был меньше 154 МБ! Если бы я установил его на 153 МБ, он не смог бы добавить в кеш, а затем прочитать из кеша! Я не знаю, будет ли минимальный размер кеша отличаться в зависимости от памяти устройства/количества установленных приложений, но для меня явно недостаточно 100 МБ! - person Honey; 12.08.2017
comment
Извините, что беспокою. Не могли бы вы взглянуть на URL-ответ не извлекается после сохранение в кеше с помощью storeCachedResponse - person Honey; 24.10.2018
comment
Убедитесь, что ответ HTTP возвращает поле Cache-Control в заголовке, иначе кэширование не будет работать. - person Doug Voss; 04.05.2019

Решение - сначала получите всю информацию, которая вам нужна, примерно так

- (void)loadData
{
    if (!self.commonDataSource) {
        self.commonDataSource = [[NSArray alloc] init];
    }

    [self setSharedCacheForImages];

    NSURLSession *session = [self prepareSessionForRequest];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[BaseURLString stringByAppendingPathComponent:@"app.json"]]];
    [request setHTTPMethod:@"GET"];
    __weak typeof(self) weakSelf = self;
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSArray *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
            weakSelf.commonDataSource = jsonResponse;
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf updateDataSource];
            });
        }
    }];
    [dataTask resume];
}

- (void)setSharedCacheForImages
{
    NSUInteger cashSize = 250 * 1024 * 1024;
    NSUInteger cashDiskSize = 250 * 1024 * 1024;
    NSURLCache *imageCache = [[NSURLCache alloc] initWithMemoryCapacity:cashSize diskCapacity:cashDiskSize diskPath:@"someCachePath"];
    [NSURLCache setSharedURLCache:imageCache];
}

- (NSURLSession *)prepareSessionForRequest
{
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    [sessionConfiguration setHTTPAdditionalHeaders:@{@"Content-Type": @"application/json", @"Accept": @"application/json"}];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
    return session;
}

После того как нужно скачать каждый файл - в моем случае - сделать парсинг ответа и скачать картинки. Также перед тем, как сделать запрос, вам нужно проверить, есть ли в кеше ответ на ваш запрос - что-то вроде этого

NSString *imageURL = [NSString stringWithFormat:@"%@%@", BaseURLString ,sourceDictionary[@"thumb_path"]];
NSURLSession *session = [self prepareSessionForRequest];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
[request setHTTPMethod:@"GET"];

NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (cachedResponse.data) {
    UIImage *downloadedImage = [UIImage imageWithData:cachedResponse.data];
    dispatch_async(dispatch_get_main_queue(), ^{
        cell.thumbnailImageView.image = downloadedImage;
    });
} else {
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (!error) {
        UIImage *downloadedImage = [UIImage imageWithData:data];
        dispatch_async(dispatch_get_main_queue(), ^{
            cell.thumbnailImageView.image = downloadedImage;
        });
    }
}];
    [dataTask resume];
}

После этого вы также можете проверить результат с помощью xCode Network Analyzer.

Также обратите внимание, как указано @jcaron и задокументировано Apple

NSURLSession не будет пытаться кэшировать файл размером более 5% от размера кеша.

Результат что-то вроде

введите здесь описание изображения

person hbk    schedule 30.06.2015
comment
Извините, что беспокою. Не могли бы вы взглянуть на URL-ответ не извлекается после сохранение в кеше с помощью storeCachedResponse - person Honey; 24.10.2018

После того, как вы установили кеш и сеанс, вы должны использовать методы сеанса для загрузки ваших данных:

- (IBAction)btnClicked:(id)sender {
    NSString *imageUrl = @"http://placekitten.com/1000/1000";
    NSURLSessionDataTask* loadDataTask = [session dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        UIImage *downloadedImage = [UIImage imageWithData:data];
        NSLog(@"ImageSize: %f, %f", downloadedImage.size.width, downloadedImage.size.height);
        NSLog(@"DiskCache: %i of %i", [[NSURLCache sharedURLCache] currentDiskUsage], [[NSURLCache sharedURLCache] diskCapacity]);
        NSLog(@"MemoryCache: %i of %i", [[NSURLCache sharedURLCache] currentMemoryUsage], [[NSURLCache sharedURLCache] memoryCapacity]);
    }];
    [loadDataTask resume]; //start request
}

После первого вызова изображение кэшируется.

person Thorsten    schedule 23.02.2014
comment
Спасибо за ваш ответ! Пожалуйста, ознакомьтесь с изменениями, которые я внес в свой вопрос выше. Мои правки включают ваш ответ. Ваш код подтверждает, что данные сохраняются в кеш, но я все еще не могу получить данные для обслуживания из кеша при всех последующих запросах. Я что-то пропустил? - person John Erck; 24.02.2014
comment
Попробуйте NSURLSessionDataTask вместо NSURLSessionDownloadTask. 4096 байт используемого дискового кеша выглядит так, будто кеша вообще нет. Я предполагаю, что NSURLSessionDownloadTask использует другую политику кэширования, чем NSURLSessionDataTask. - person Thorsten; 24.02.2014