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.
Related
I'm creating an app where I would like to use cached responses from time to time. I ran into a weird issue related to NSURLCache, more spicifically, if I set NSURLRequestReturnCacheDataDontLoad on my request, I don't get a cached response on iOS 8. Here's the code I'm using with AFNetworking to get this working:
// Define web request.
void (^simpleBlock)(void) = ^{
GTSessionManager.manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
[GTSessionManager.manager POST:string parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *responseJson = responseObject;
[self parseJsonSuccess:responseJson];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if (error.userInfo[JSONResponseSerializerWithDataKey]) {
NSData *data = error.userInfo[JSONResponseSerializerWithDataKey];
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:&error];
[self parseJsonError:json];
}
else
[self parseJsonError:nil];
}];
};
if (self.shouldUseCache) {
GTSessionManager.manager.requestSerializer.cachePolicy = NSURLRequestReturnCacheDataDontLoad;
[GTSessionManager.manager POST:string parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *responseJson = responseObject;
[self parseJsonCacheSuccess:responseJson];
// Load second request.
simpleBlock();
} failure:^(NSURLSessionDataTask *task, NSError *error) {
simpleBlock();
}];
}
else
simpleBlock();
The main idea here is that if the the client wants to use a cache, the first request should try to load it, pass the result to the handler and start reloading the request to refresh the cache.
This approach works well on iOS 7 but doesn't work on iOS 8+. I've setup the NSURLCache with the following call.
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:500 * 1024 * 1024
diskCapacity:500 * 1024 * 1024
diskPath:#"cache" /**Tried nil here as well. */];
[NSURLCache setSharedURLCache:sharedCache];
I read a few articles and other SO questions about this but I just can't get this to work. Am I missing something? Could it be related to the fact that I'm using a POST request?
EDIT
Forgot to mention that I'm not using a cache policy on my server. I also tried setting a cache policy using Cache-Control=max-age=604800, public but I got the same behavior.
Alright, so after some further investigation, I concluded that the problem is somewhere in NSURLSession. I sent the exact same requests as above but using AFHTTPRequestOperation instead of AFHTTPSessionManager and the cache loads perfectly on iOS 8+.
Not quite sure where the problem is still though, I think it has something to do with the cache policies but for now I'm sticking with NSURLConnection.
I new to AFNetworking and less experience in iOS development.
I use AFNetworking to download image from the internet, here is the code:
- (void)dowloadPoject {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
BOOL __block responseFromCache = YES; // yes by default
void (^requestSuccessBlock)(AFHTTPRequestOperation *operation, id responseObject) = ^(AFHTTPRequestOperation *operation, id responseObject) {
if (responseFromCache) {
// response was returned from cache
NSLog(#"RESPONSE FROM CACHE: %#", responseObject);
}
else {
// response was returned from the server, not from cache
NSLog(#"RESPONSE From Server: %#", responseObject);
}
UIImage *downloadedImage = [[UIImage alloc] init];
downloadedImage = responseObject;
// Add & Reload Data
[self.projectImage addObject:downloadedImage];
[self.projectname addObject:#"ABC"];
[self reloadComingSoonProject];
[self reloadNewReleaseProject];
};
void (^requestFailureBlock)(AFHTTPRequestOperation *operation, NSError *error) = ^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"ERROR: %#", error);
};
AFHTTPRequestOperation *operation = [manager GET:#"http://i359.photobucket.com/albums/oo34/SenaSLA/walls/Forward-Arrow-Button.png"
parameters:nil
success:requestSuccessBlock
failure:requestFailureBlock];
operation.responseSerializer = [AFImageResponseSerializer serializer];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(#"bytesRead: %u, totalBytesRead: %lld, totalBytesExpectedToRead: %lld", bytesRead, totalBytesRead, totalBytesExpectedToRead);
[self.viewNavBar.progressbar setProgress:(totalBytesRead/totalBytesExpectedToRead) animated:YES];
}];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
// this will be called whenever server returns status code 200, not 304
responseFromCache = NO;
return cachedResponse;
}];
}
I have been searching on internet and i still no get the right solution. i have tried this solution from rckoenes but this no work for me.
I have succeeded to cache an image like 4kb, but when i'm trying with image like 103kb it doesn't cache. Thank you.
You can try increasing the cache size. Last time I tested this, networking code wouldn't cache if the received data exceeded 5% of the total cache size (though, annoyingly, I have yet to see Apple clearly articulate the rules that are applied during caching).
Anyway, if you look at the app delegate in the sample AFNetworking project, that shows and example of how to specify the cache size:
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:cache];
It looks like the default RAM cache is only 0.5mb and the disk cache is 10mb. By increasing that RAM cache to 4mb, like shown above, or larger, and you should be able to cache your 103kb image (assuming that all the other criteria, such as header fields of the response and the like, permit it).
I am developing an app that download data from the web through json. I set the cache policy to use the cache as I have seen in tutorials and posts. Well, when the app starts, it downloads the data once and if I close the view and reopen it in flight mode, the data is loaded from cached. But if I close the app and then restart it, do not load from cache and request the server again (or show error if flight mode is on). I need caching data on disk for offline usage... help please
here is my code:
AppDelegate.m
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024
diskCapacity:100 * 1024 * 1024
diskPath:#"nsurlcache"];
[NSURLCache setSharedURLCache:sharedCache];
and the request:
NSString *string = #"http://********.es/api/get_posts/?post_type=listing";
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
NSCachedURLResponse * newCachedResponse = [[NSCachedURLResponse alloc]
initWithResponse:cachedResponse.response
data:cachedResponse.data
userInfo:cachedResponse.userInfo
storagePolicy:NSURLCacheStorageAllowed];
return newCachedResponse;
}];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
_lista = responseObject[#"posts"];
[self.tableView reloadData];
//NSLog(#"%#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Error Retrieving Data"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[alertView show];
}];
[operation start];
I have tried a lot of solutions on the web and no one works as I would like, so I think the problem is something wrong in my code...
I think the functionality you are wanting here is going to require using core data or writing your own caching using NSFileManager. Figure out the best approach for your app. There is a pretty cool article about NSFileManager by Mattt Thompson here
I have a problem when users use my app and they lost connection or use airplane mode.
My app server side doesn't set any cache policy and for the time being I can't change it. I migrated from AFNetworking 1.x to 2.0 and now I'm using AFHTTPRequestOperationManager when making requests. The problem is that because I have no cache policy on the server side, every request is made to the server (which for now is fine) but if the user is not able to connect to my server, it doesn't load the cached request.
So, I'm trying the following, using AFHTTPRequestOperation directly like this:
NSURL *URL = [NSURL URLWithString:filePath];
NSMutableURLRequest *request = [[NSURLRequest requestWithURL:URL] mutableCopy];
if (![[AFNetworkReachabilityManager sharedManager] isReachable]) {
[request setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
}
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
[[NSOperationQueue mainQueue] addOperation:op];
This way, if AFNetworkReachabilityManager tells me there's no connectivity, I configured the cache policy for the request and it is loaded from the cache correctly.
The question is, is this the right approach to this situation?
You just need to subclass the AFHTTPSessionManager and check if the client is offline. Then you can change the cache policy or force the app to use cached data.
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.