iOS7 brings NSURLSession
, with the help of NSURLSessionConfigure
, we can customize URLCache, so i tried it, but with no luck, it seems my URLCache is not used at all.
- (void)testURLCache
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:1024 * 100 diskCapacity:1024 * 100 diskPath:@"test.urlcache"];
NSLog(@"usage:%lu", (unsigned long)[cache currentDiskUsage]);
config.URLCache = cache;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
for (int i = 0; i < 40; i++) {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js?%d", i]]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"error:%@", error);
} else {
NSLog(@"done:%d", i);
}
}];
[task resume];
[NSThread sleepForTimeInterval:0.5];
}
NSLog(@"usage:%lu", (unsigned long)[cache currentDiskUsage]);
}
something i observed:
1) a test.urlcache
folder is created under Caches
folder, and with 3 files Cache.db
, Cache.db-shm
, Cache.db-wal
, the size of Cache.db
is 4KB, no records in it.
2) every time run the code, the first output of usage is always 0, after for loop ended, usage becomes 4096, the next time usage becomes 0 again.
Seems like there are two potential issues:
Your cache is too small. Historically, it wouldn't cache if the item being cached exceeded 10% of the total cache size.
You're checking the disk usage 0.5 seconds after the last request. That might be too soon for the cache to be written to persistent storage.
Below I use a bigger cache and wait 10 seconds before final check of disk usage, and the cache works fine. I also use GCD to queue the serial tasks to eliminate the arbitrary 0.5 second delay, which is both safer, and makes it more obvious when it's retrieving data over network and when it's using the cache.
- (void)testURLCache
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
// this is too small and won't cache
// NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:100 * 1024 diskCapacity:100 * 1024 diskPath:@"small.urlcache"];
// use bigger cache
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:10 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:@"big.urlcache"];
config.URLCache = cache;
// I'm going to add tasks to serial queue so that (a) I don't block main queue;
// and (b) I don't need arbitrary delay, but I can just signal when it's done.
dispatch_queue_t queue = dispatch_queue_create("com.domain.app.datatasks", 0);
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
for (int i = 0; i < 40; i++) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js?%d", i]]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"error:%@", error);
} else {
NSLog(@"done:%d", i);
}
dispatch_semaphore_signal(semaphore); // signal dispatched block that request is done
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // wait until task is complete
});
}
// when the queued tasks finish, then check disk usage both immediately and 10 seconds later
dispatch_async(queue, ^{
// check cache size immediately after the data tasks finish
NSLog(@"usage:%lu", (unsigned long)[cache currentDiskUsage]);
// check again in 10 seconds
double delayInSeconds = 10.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(@"usage:%lu", (unsigned long)[cache currentDiskUsage]);
});
});
}
As an aside, for network tasks like this, I'd generally use operation queue rather than dispatch queue and then wrap my data tasks in concurrent NSOperation
subclass. Then I can use dependencies to dictate the final completion operation, but at the same time both permit concurrency but also dictate the degree of concurrency, but this seemed outside the scope of the original question and I wanted to keep this as simple as possible.
This is iOS7 issue. With iOS8.1 it works correctly, I'm not sure about iOS8. Please notice also you can still use iOS7 SDK to compile your app, and it'll work correctly on iOS8.1 devices.
However you may still use global cache, instead of the one assigned to NSURLSessionConfiguration
:
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:1024 * 100 diskCapacity:1024 * 100 diskPath:@"test.urlcache"];
[NSURLCache setSharedURLCache:cache];
BTW, in iOS8 there are 3 not yet documented methods in NSURLCache
class (actually it is a NSURLSessionTaskAdditions
category of NSURLCache
, check NSURLCache.h file):
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forDataTask:(NSURLSessionDataTask *)dataTask
- (void)getCachedResponseForDataTask:(NSURLSessionDataTask *)dataTask completionHandler:(void (^) (NSCachedURLResponse *cachedResponse))completionHandler
- (void)removeCachedResponseForDataTask:(NSURLSessionDataTask *)dataTask
...however it's not neccessary to touch them is you want to change caching mechanics ;)