I have a test app setup and it successfully downloads content from the network even if the user switches apps while a download is in progress. Great, now I have background downloads in place. Now I want to add caching. There is no point to me downloading images more than once, b/c of system design, given an image URL I can tell you the content behind that URL will never change. So, now I want to cache the results of my download using apple's built in in-memory/on-disk cache that I've read so much about (as opposed to me saving the file manually in NSCachesDirectory and then checking there before making new request, ick). In an attempt to get caching working on top of this working code, I added the following code:
- (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;
}
When I create my session, I've added two NEW lines (URLCache and 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;
}
Then, just to be ultra redundant in an attempt to see caching success I switched my NSURLRequest line from
// NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; // Old line, I've replaced this with...
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:2*60]; // New line
Now, when I go to download the item a 2nd time, the experience is exaclty like the first!! Takes a long time to download and progress bar is animated slow and steady like an original download. I want the data in the cache immediately!! What am I missing???
----------------------------UPDATE----------------------------
Okay, thanks to Thorsten's answer, I've added the following two lines of code to my didFinishDownloadingToURL delegate method:
- (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
*/
}
This is great. It confirms the cache is growing. I presume since I'm using a downloadTask (downloads to file as opposed to memory), that that's why DiskCache is growing and not memory cache first? I figured everything would go to memory cache until that overflowed and then disk cache would be used and that maybe memory cache was written to disk before the OS kills the app in the background to free up memory. Am I misunderstanding how Apple's cache works?
This is a step forward for sure, but the 2nd time I download the file it takes just as long as the first time (maybe 10 seconds or so) and the following method DOES get executed again:
- (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?
}
What do you make of my edits above? Why am I not seeing the file returned very quickly on subsequent requests?
How can I confirm if the file is being served from the cache on the 2nd request?
Note that the following SO post helped me solve my problem: Is NSURLCache persistent across launches?
- (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!
}
In addition to the sleep(1) line, also note the size of my cache; 500MB.
According to docs you need a cache size that is way bigger than what you're trying to cache.
The response size is small enough to reasonably fit within the cache.
(For example, if you provide a disk cache, the response must be no
larger than about 5% of the disk cache size.)
So for example if you want to be able to cache a 10MB image, then a cache size of 10MB or even 20MB will not be enough. You need 200MB.
Honey's comment below is evidence that Apple is following this 5% rule. For an 8Mb he had to set his cache size to minimum 154MB.
Solution - first get all info u need it something like this
- (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;
}
After you need to download each file - in my case - make parsing of response and download images. Also before making request you need to check if cache already have response for your request - something like this
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];
}
After that you can also check result with xCode Network Analyzer.
Also note as mentionted by #jcaron and documented by Apple
NSURLSession won't attempt to cache a file larger than 5% of the cache
size
Result something like
Once you set the cache and the session, you should use the session-methods to download your data:
- (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
}
After the first call, the image is cached.
Related
I have a task to do without ARC. Previously I didn't use it (started studying ios development recently). I have a class that represents http request, it conforms to NSURLSessionDownloadDelegate protocol. And also I have following code:
-(void)executeWithRelativeUrl:(NSString *)relativeUrl andSuccessBlock: (void(^) (NSData*))successBlock {
NSURL *url = [[NSURL alloc] initWithString:relativeUrl relativeToURL:self.baseUrl];
[self setSuccessBlock:successBlock];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
[request release];
[url release];
}
that creates url session and starts download task. I'm dealing with task results in following method:
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSData *data = [NSData dataWithContentsOfURL:location];
dispatch_async(dispatch_get_main_queue(), ^{
self.successBlock(data);
});
}
Now the question is: do I need to release session, download task and location url in the end of the last method? Or it will be done for me? I'm asking this because I created it in the first method (except for url), and as I understand the one who is responsible for releasing the object is also me. Thanks!
The Golden Rule is very simple. Did you say alloc or copy or retain? No? Then you don't need to say release (and you must not do so).
(You need to release the url and the request for that reason, and you are doing so. So memory management is now complete.)
I found an issue with (possibly) NSURLCache today while inspecting request and response headers in Charles Proxy. The issue is a little perplexing, but I'm able to repro it consistently:
In a nutshell, the issue has to do with caching of NSURLRequests using iOS's native NSURLCache with the default policy. It turns out that the request is not cached whenever the response has the header transfer-encoding: chunked. But if the response header is content-length: xxx instead, caching works fine. Specifically, it seems that when the response is chunked, NSURLCache doesn't save the eTag and also neglects appending the if-none-match header to subsequent requests to the same url, and consequently, caching fails (as it should), i.e. a 200 is returned instead of a 304.
I'm testing on the iOS8.2 simulator. Even if you don't have a solution, I'd love to hear if you've experienced the same issue. I've found at least one similar report), and here's a related thread posted by my back-end engineer.
It should work if you manually add the response data to the cache. I've got an image loading class where I want to make sure everything is cached, so I do something like this:
- (void)getImageWithURL:(NSURL *)url onCompletion:(void (^)(UIImage *image, NSError *error))completion {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
UIImage *cachedImage = [self cachedImageForURLRequest:request];
if (cachedImage) {
NSLog(#"Got image from cache.");
completion(cachedImage, nil);
return;
}
[[[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Manually cache the response.
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
[[NSURLCache sharedURLCache] storeCachedResponse:cachedResponse forRequest:request];
NSLog(#"Got a fresh image.");
completion([UIImage imageWithData:data], error);
}] resume];
}
- (UIImage *)cachedImageForURLRequest:(NSURLRequest *)urlRequest {
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:urlRequest];
return [UIImage imageWithData:cachedResponse.data];
}
I'm having trouble getting a simple implementation of NSURLCache working on iOS8. It's my understanding that once a shared cache is created, it automatically caches data requests with the proper cache policy. No configuration required unless you want to customize behavior. Is this correct?
I've included a simplified version of my code below. The cache is created in AppDelegate, and the TableViewController that needs the data uses the APICaller object to make the call. The request is using NSURLRequestReturnCacheDataElseLoad, as this information doesn't need to be updated frequently.
If I'm way off the mark here. What's the next step? The data received is 95KB.
AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
return YES;
}
TableViewController:
- (void)viewDidLoad {
APICaller *apiCaller = [APICaller alloc] init];
[apiCaller makeAPICallWithCompletionHandler:^(NSArray *result, NSError *error){
if (error) {
// Handle error
} else {
self.property = [result mutableCopy];
[self.tableView reloadData];
}
}
}
APICaller:
- (void)makeAPICallWithCompletionHandler:(void(^)(NSArray *result, NSError *error))completionHandler
{
NSString *urlString = [NSString stringWithFormat#"https://api.apiwebsite.com/json/query?key=#", API_KEY];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10];
NSURLSessionConfiguration *config = [NSURLSession defaultSessionConfiguration];
self.urlSession = [URLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
NSURLSessionDataTask *dataTask = [self.URLSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(#"There was an error");
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, error);
});
} else {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
NSSortDescriptor *sortByName = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortByName];
NSArray *sortedResult = [dict[#"result"] sortedArrayUsingDescriptors:sortDescriptors];
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(sortedResponse, error);
});
}
}];
[dataTask resume];
}
According to this post and this post NSURLCache is broken in iOS 8 when using NSURLSession.
According to the first post I linked, NSURLConnection still seems to work. I remember seeing in one of the comments that this was still the case as of early Feb, but I haven't had a chance to investigate myself.
Best of luck.
I am experimenting with replacing some ancient networking code with NSUrlSession, but setting HTTPMaximumConnectionsPerHost to 1 is not having any effect. This request code is called 170 times but it makes 170 connections to the host (watching in CharlesProxy) before anything comes back, which is slamming the server. Am I missing something here?
All requests go to the same domain and url with only differences in parameters. Of course I can do something different but HTTPMaximumConnectionsPerHost seems like it should limit the connections.
At the moment I am compiling versus SDK 7 (due to having to support iOS 6 still) but if I can get this to work I can abandon iOS 6 and just support 7/8 and build vs 8. This is in an enterprise app BTW.
+ (NSURLSession*) sharedSession
{
static NSURLSession* session;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSURLSessionConfiguration * sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
sessionConfig.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:nil
delegateQueue:nil];
});
return session;
}
+ (void) createRequestWithPayload2:(HttpRequestPayload *)payload
success:(void (^)(CommunicationResponse * response))success
failure:(void (^)(NSError * error))failure
progress:(void (^)(TaskStatus status))progressStatus
{
NSURLSession* session = [RequestSender sharedSession];
NSString * url = [NSString stringWithFormat:#"%#/%#", payload.baseURL, payload.urlParams];
NSMutableURLRequest* request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
[request setHTTPMethod:payload.method];
[request setAllHTTPHeaderFields:payload.headers];
if ( payload.body )
{
[request setHTTPBody:payload.body];
}
//NSLog(#"Request:\n%#",request);
NSURLSessionDataTask * task =
[session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *resp, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),
^{
if ( error )
{
NSLog(#"%#",error);
failure(error); }
else
{
NSHTTPURLResponse *response = (NSHTTPURLResponse*) resp;
//NSLog(#"%#",response);
//NSLog(#"%#",data);
CommunicationResponse* cr = [CommunicationResponse new];
[cr set_commStatus:response.statusCode];
[cr set_response:data];
success(cr);
}
});
}];
[task resume];
}
Seems like you're not sharing, but creating new session each time. HTTPMaximumConnectionsPerHost only limit connections on the current session.
From the documentation
This limit is per session, so if you use multiple sessions, your app as a whole may exceed this limit.
NSURLSessionConfiguration : HTTPMaximumConnectionsPerHost
As alternative you can set discretionary property to YES (TRUE). Where it will limit all connections across all sessions to a reasonable number.
NSURLSessionConfiguration : discretionary
Maximum connection per host mean, that more then currently executed connections will be added to queue and wait while older will finish they work.
It limit not all count, but count at one time.
I have an app where I'm downloading quite a few images for display later on.
The app needs to function even when there's no internet connection, so the images are loaded at one point and persisted using NSURLCache.
This is a neat solution since I can use normal networking libraries and easily take advantage of custom cache settings.
I realize that this type of caching doesn't guarantee that the files are persisted until they expire since it's up to the system to release cache whenever it deems necessary. That's not a huge issue since the images should be able to be re-downloaded and hence re-persisted.
However, I've noticed that it decides to randomly release images from cache, and it doesn't seem to persist them when I download them again. I'm not sure where I'm going wrong.
First of all, I define the cache capacity like so:
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:50 * 1024 * 1024
diskCapacity:200 * 1024 * 1024
diskPath:#"netcache"];
[NSURLCache setSharedURLCache:URLCache];
(50MB in memory and 200MB on disk)
When downloading the images (using AFNetworking) I modify the response headers to set Cache-Control to max-age=31536000 which means it should cache the response for one year.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Download completed
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Download failed
}];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
NSURLResponse *response = cachedResponse.response;
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse*)response;
NSDictionary *headers = HTTPResponse.allHeaderFields;
NSMutableDictionary *modifiedHeaders = headers.mutableCopy;
modifiedHeaders[#"Cache-Control"] = #"max-age=31536000"; // 1 year in seconds
NSHTTPURLResponse *modifiedHTTPResponse = [[NSHTTPURLResponse alloc]
initWithURL:HTTPResponse.URL
statusCode:HTTPResponse.statusCode
HTTPVersion:#"HTTP/1.1"
headerFields:modifiedHeaders];
return [[NSCachedURLResponse alloc] initWithResponse:modifiedHTTPResponse data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed];
}];
[self.operationQueue addOperation:operation];
...and yet it seems like images are released and won't even get "re-cached" when downloaded again. (I believe they are reported as cached, even though they won't load when trying to display them when the device doesn't have any connection.)
The images are later displayed using AFNetworking's UIImageView+AFNetworking.h category, like so:
[self.imageView setImageWithURL:url];
Any ideas?
I have faced the same problem. First of all you can use AFURLSessionManager's method
- (void)setDataTaskWillCacheResponseBlock:(NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block;
to set cache block for all requests at the same time.
Second - check that 'Cache-Control' field in response headers is 'public'. For example:
[someAFHTTPSessionManager setDataTaskWillCacheResponseBlock:^NSCachedURLResponse *(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse)
{
NSHTTPURLResponse *resp = (NSHTTPURLResponse*)proposedResponse.response;
NSMutableDictionary *newHeaders = [[resp allHeaderFields] mutableCopy];
if (newHeaders[#"Cache-Control"] == nil) {
newHeaders[#"Cache-Control"] = #"public";
}
NSHTTPURLResponse *response2 = [[NSHTTPURLResponse alloc] initWithURL:resp.URL statusCode:resp.statusCode HTTPVersion:#"1.1" headerFields:newHeaders];
NSCachedURLResponse *cachedResponse2 = [[NSCachedURLResponse alloc] initWithResponse:response2
data:[proposedResponse data]
userInfo:[proposedResponse userInfo]
storagePolicy:NSURLCacheStorageAllowed];
return cachedResponse2;
}];
Third: If image's size is bigger than 5% of total cache space it will not be cached. Also it seems that there are some additional hidden rules.