So I am creating a custom cache file via NSURLCache via the following method in my AppDelegate didFinishLaunchingWithOptions method:
NSURLCache *result = [[NSURLCache alloc] initWithMemoryCapacity:MBYTE(20) diskCapacity:MBYTE(200) diskPath:#"URLCache"];
[NSURLCache setSharedURLCache:result];
where MBYTE is defined as :
#ifndef MBYTE
#define MBYTE(x) ((x) << 20)
#endif
Then later on I am saving a NSCachedURLResponse in the NSURLCache in the following method:
NSURLCache *sharedCache = [NSURLCache sharedURLCache];
[sharedCache storeCachedResponse:cachedResponse forRequest:theRequest];
When I run the code I seem to be able to pull up the response and find it in cache and it seems to work. However, when I try and see how much cache has been used on launch via:
[result currentDiskUsage] or [result currentMemoryUsage]
the NSLog always returns 0. Even though I am able to load the response via the cache.
So I am wondering if this is something I should worry about, maybe
Related
I have seen several posts for this question and I cant figure out how to get this working. For example, this post NSCachedURLResponse returns object, but UIWebView does not interprets content
suggests that we subclass and override cachedResponseForRequest and modify the NSURLRequest before returning a cached response (not sure why this needs to be done).
This is the code i have in my NSURLCache subclass:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSCachedURLResponse *resp = [super cachedResponseForRequest:request];
if (resp != nil && resp.data.length > 0) {
NSDictionary *headers = #{#"Access-Control-Allow-Origin" : #"*", #"Access-Control-Allow-Headers" : #"Content-Type"};
NSHTTPURLResponse *urlresponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:#"1.1" headerFields:headers];
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:urlresponse data:resp.data];
return cachedResponse;
} else {
return nil;
}
}
Now when i load the request via the webview and I'm offline, the delegate didFailLoadWithError gets called (it was not able to load), but before that delegate method is called, my cachedResponseForRequest method gets called and the cached response has some html however my didFailLoadWithError method still gets called. So my question is, should the webViewDidFinishLoad should get called if we return the cached response from the NSURLCache, if not how do i make this work?
I have this in my didFinishLaunchingWithOptions:
NSUInteger cacheSizeMemory = 500*1024*1024; // 500 MB
NSUInteger cacheSizeDisk = 500*1024*1024; // 500 MB
NSURLCache *sharedCache = [[CustomCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[NSURLCache setSharedURLCache:sharedCache];
This is the flow i want working:
Launch the app.
Hit some url (lets say slickdeals.net)
Go offline kill the application
Launch the application
UIWebView attempts to hit url but device is offline and a returned cached response is returned.
We display cached response.
Any thoughts on what needs to change in my code is appreciated!
NSURLCache is designed for HTTP conform caching, and often behaves unexpectly. You must wrote your own cache for your use case, and implement a custom NSURLProtocol to use it. The protocol put the responses into your cache, and provides them from cache when offline. Rob Napier wrotes an excellent article about this.
I initialized NSURLCache in my app delegate like so:
int cacheSizeMemory = 4*1024*1024; // 4MB
int cacheSizeDisk = 32*1024*1024; // 32MB
MyServerCache *sharedCache = [[MyServerCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[MyServerCache setSharedURLCache:sharedCache];
Then I create a request with default cache policy and call:
[NSURLConnection sendSynchronousRequest: urlRequest
returningResponse: &response
error: pError ];
The response is of type "application/json" and has "Cache-Control" set to "private, max-age=600". The response is only 874 bytes (shouldn't be too large to cache), however NSURLCache does not seem to work.
I have run this on a simulator and looked for the on-disk cache which doesn't seem to exist, so I subclassed NSURLCache (MyServerCache) and overrode the following methods to see if they were being called:
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSLog(#"Returning cached response for request: %#", request);
return [super cachedResponseForRequest:request];
}
-(void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request
{
NSLog(#"Caching response for request: %#", request);
[super storeCachedResponse:cachedResponse forRequest:request];
}
No matter what I do (and I've tried several different API's and responses), NSURLCache never seems to cache anything. I can never find the on-disk cache in the file system, and the methods above are never called on my subclass of NSURLCache (MyServerCache).
I'd really like to use this rather than rolling my own cache, but I have no idea why it's not working or how to get it to work. Please help.
You have to create your request with the NSURLRequestUseProtocolCachePolicy and it works perfectly out-of-the box with NSURLCache.
NSURLRequestCachePolicy cachePolicy = NSURLRequestUseProtocolCachePolicy;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:cachePolicy
You can read about the NSURLRequestCachePolicy here: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLRequest_Class/#//apple_ref/c/tdef/NSURLRequestCachePolicy
A little bit confusing ist that iOS always returns a 200 - Success so you will never get a 304 - Resource not Modified when a response is coming from cache.
So it´s more easy to see the cache policy working when monitoring the server side.
You can use https://github.com/PaulEhrhardt/Simple-Cache-Test to validate cache-control, expires and ETag with a little local ruby server. It´s just a few lines to set-up. I tested both successfully with 7.1 and 8.4 iOS SDK.
We have set up a simple NSURLConnection and NSURLCache as per the abbreviated code snippet below. We have made sure, that the server (localhost:9615) returns the following caching headers:
ETag : abcdefgh
Cache-Control : public, max-age=60
However, the willCacheResponse delegate method is never called. Any idea?
Code:
// In the app delegate
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
// Triggered by a UIButton
- (IBAction)sendRequest:(UIButton *)sender {
NSLog(#"sendRequest");
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://localhost:9615"] cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
NSMutableData *receivedData = [NSMutableData dataWithCapacity: 0];
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (!theConnection) {
receivedData = nil;
}
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
NSLog(#"willCacheResponse");
// ...
}
Cocoa applies all sorts of criteria to determine whether it can cache. For example, in my experience, you will not see willCacheResponse called if the size of the response exceeds roughly 5% of the persistent storage cache size. I've also seen others claim that if max-age is smaller than 1835400, it won't cache either (this is not my experience, but perhaps older iOS versions suffered from this). Obviously, the request has to be a http or https request, not ftp or file request.
If my cache is large enough (and my response correctly supplies Cache-Control and Content-Type), I find that the cache works properly. I only wish that Apple would clearly articulate the criteria being applied (either in documentation or return codes) as I and others have wasted hours diagnosing cache failures only to realize that Cocoa was applying some secret rules.
Note, if NSURLConnection was, itself, satisfied by retrieving it from cache, willCacheResponse will not be called. And, of course ensure that didFailWithError was not called.
I am having a problem with AFNetworking.
I am requesting a lot of JSON data from my Server via GET using this:
[[SessionResponseClient sharedClient] getPath:kURIRateList parameters:#{...} success:^(AFHTTPRequestOperation *operation, id JSON) {
[_delegate receivedRateResponse:JSON];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[_delegate receivedRateResponse:nil];
}];
When this is called around 10-20 times, CFData seems to take most of the used memory of my app.
Memory Usage
When I investigate both CFData and CFData (store), I get the following:
CFData (store)
CFData
Side note: My app is using ARC and everything else seems to perform fine.
My question is now, as I didn't see any pending issues on AFNetworking's GitHub Page and almost no other complaints about that on the internet, what the heck am I doing wrong? Is anybody else facing this issue or am I the only one? Could you please post your code, as I don't think my Code looks that wrongs...
Update 1
The code for interface:
#protocol RateRequestsDelegate;
#interface RateRequests : NSObject {
id<RateRequestsDelegate> _delegate;
}
#property (nonatomic, strong) id<RateRequestsDelegate> delegate;
- (void)fetchRateList;
#end
#protocol RateRequestsDelegate <NSObject>
- (void)receivedRateResponse:(NSDictionary *)response;
#end
SessionResponseClient is just an extended AFHtttpClient instance, as all the AFNetworking examples demonstrate, see: Interact with an API
The code for receivedRateResponse:
- (void)receivedRateResponse:(NSDictionary *)json {
if (!json) {
return;
}
self.moviesToInsert = [NSMutableArray arrayWithArray:[json objectForKey:#"rated"]];
[self.tableView reloadData];
}
Update 2
I changed the callback now to a block-based function instead of using delegates and it improved a little, but its barely mentionable and could also be caused by just a side effect of something else. How do you guys implement this? Try fetching data, display it in a table- or scrollview and then pull these data many times from the server and refresh the views
Many thanks! Paul
Thanks dude - I had the exact same problem - your solution worked.
However, I thought I'd comment that in your solution, you're essentially turning the NSURLCache off once you receive a memory warning. I'm not sure exactly how AFNetworking uses the NSURLCache, but I'm assuming their implementation won't work as effectively if you turn it off (i.e. by setting its capacity to 0). This may slow down the request speed after your first memory warning onwards. As an extreme edge case, you might even experience unexpected behaviour after a few days of using your app, as it might not manifest until the first memory warning.
I instead suggest implementing something that removes the cached responses, but keeps the cache active:
-(void)didReceiveMemoryWarning
{
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
The only issue is that AFNetworking may need to build up its cache again, but I'm assuming this won't affect the framework quite as much.
Lastly, just in case it's useful to you or anyone - in order to register your own method to a memory warning notification, you can do this:
[[NSNotificationCenter defaultCenter] addObserver:self (or pointer to your singleton)
selector:#selector(myMethod)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
I hope that helps!
Okay, the solution was that CFData (store) seems to be responsible for holding the url cache in memory when using NSURLConnection's.
So all I had to do was call...
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
...to clear the cache, once I received a memory warning...
Or you can just call it whenever you want(didn't time profile that function though)...
Hope this helps someone who encounters a similar problem.
Maybe you could give #autoreleasepool a try.
For example:
#autoreleasepool {
// your codes below
[[SessionResponseClient sharedClient] getPath:kURIRateList parameters:#{...} success:^(AFHTTPRequestOperation *operation, id JSON) {
[_delegate receivedRateResponse:JSON];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[_delegate receivedRateResponse:nil];
}];
// your codes above
}
I'm currently trying to log all URL connections made in my app. Is there a debug switch, or network activity monitor tool I can use to do this?
Or is my only alternative to include NSLog statements throughout the source base?
Thanks,
Ash
All NSURLConnections of your application use a shared cache class for default
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSURLCache_Class/Reference/Reference.html
So, one thing you could do is subclass the default cache, and then at the cachedResponseForRequest NSURLCache method, you can to track you requests.
#interface CustomNSURLCache : NSURLCache {
}
#end
#implementation CustomNSURLCache
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSLog(#"connection will send request for url: %#", request);
return [super cachedResponseForRequest:request];
}
#end
At your AppDelegate didFinishLaunchingWithOptionsmethod, set the shared cache to an instance of your cache.
CustomNSURLCache *customCache = [[CustomNSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:51200 diskPath:nil];
[NSURLCache setSharedURLCache:customCache];
[customCache release];
(being 0 the default value for MemoryCapacity and 512000 for DiskCapacity)
Now when you create a new connection
NSURLRequest *request1 = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:#"http://stackoverflow.com"]];
[[NSURLConnection alloc] initWithRequest:request1 delegate:self];
you should see something like this at your console
connection will send request for url: <NSURLRequest https://stackoverflow.com/>
In Instruments there is a Network Activity Monitor under the System Instruments. I have not personally used it though so I don't know if it does what you want.