Menu
 

Handling the NSURLSession Caching

508
Moshe Netanel
 
Share Button
 

If you are an iOS developer using NSURLSession, you have probably noticed that something is out of whack with the expiration mechanism of the cache. We have too – and decided to get to the bottom of it to save you some time. But first, lets see how it works exactly.

What are cache headers?

Cache headers allow clients to store web server assets locally, improving server performance, using less bandwidth and keeping clients from continually asking resources from the server.

This means, that if the cache is too aggressive the client might serve stale content to users. If, on the other hand it is too timid (or missing), the server would get an increased load and traffic usage would rise.

If you own the server, the best practice would be to use a HTTP response header to set the cache expiration time, so it can be dynamically changed at will. (If you want to expand your knowledge about cache headers specifications please refer to W3-rfc2616.)

When using NSURLSessions the HTML header response defines “max-age” which tells the client when to recache. This process reduces workload and rationalizes resources.

Suggested solution

We at the Mobfox SDK Team have used the NSURLSessions cache policy to automatically update caching according to the “max-age” in the HTML header response by using the NSURLRequestUseProtocolCachePolicy cache policy.

NSURLRequestCachePolicy

According to Apple’s documentation when working with NSURLSession, you can configure one of the following policies:

  • NSURLRequestReloadIgnoringLocalCacheData
    The URL load should be loaded only from the originating source.
  • NSURLRequestReturnCacheDataDontLoad
    Use existing cache data, regardless or age or expiration date, and fail if no cached data is available.
  • NSURLRequestReturnCacheDataElseLoad
    Use existing cache data, regardless or age or expiration date, loading from originating source only if there is no cached data.
  • NSURLRequestUseProtocolCachePolicy
    Use the caching logic defined in the protocol implementation, if any, for a particular URL load request.
    It is implementing caching semantics as described in rfc 7234. For the HTTP and HTTPS protocols, NSURLRequestUseProtocolCachePolicy performs the following behavior:
    1. If a cached response does not exist for the request, the URL loading system fetches the data from the originating source.
    2. Otherwise, if the cached response does not indicate that it must be revalidated every time, and if the cached response is not stale (past its expiration date), the URL loading system returns the cached response.
    3. If the cached response is stale or requires revalidation, the URL loading system makes a HEAD request to the originating source to see if the resource has changed. If so, the URL loading system fetches the data from the originating source. Otherwise, it returns the cached response.

You can read more on the topic in Apple’s documentations here.

Code example:

#import "MainViewController.h"

@interface MainViewController () <NSURLSessionDataDelegate>

@property (nonatomic, strong, readwrite) NSURLSession * session;

@end

@implementation MainViewController

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
    #pragma unused(session)
    #pragma unused(task)
    NSLog(@"metrics");
    [metrics.transactionMetrics enumerateObjectsUsingBlock:^(NSURLSessionTaskTransactionMetrics * t, NSUInteger idx, BOOL * stop) {
        #pragma unused(stop)
        NSString * fetchType;
        switch (t.resourceFetchType) {
            case NSURLSessionTaskMetricsResourceFetchTypeUnknown: {
                fetchType = @"unknown";
            } break;
            case NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad: {
                fetchType = @"network load";
            } break;
            case NSURLSessionTaskMetricsResourceFetchTypeServerPush: {
                fetchType = @"server push";
            } break;
            case NSURLSessionTaskMetricsResourceFetchTypeLocalCache: {
                fetchType = @"local cache";
            } break;
            default: {
                fetchType = @"?";
            } break;
        }
        NSLog(@"  [%zu]: %@", (size_t) idx, fetchType);
    }];
}

 

- (IBAction) Test:(id)sender {

   NSURL * url = [NSURL URLWithString:@"https://sdk.starbolt.io/dist/engine/banner_ios.js?s=fe96717d9875b9da4339ea5367eff1ec&v=3.5.1_core&sub_bundle_id=com.mobfox.--"];

    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url
        cachePolicy:NSURLRequestUseProtocolCachePolicy
        timeoutInterval:60.0
    ];

    if (self.session == nil) {
        NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:NSOperationQueue.mainQueue];
    }
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        if (error != nil) {
            NSLog(@"task transport error %@ / %d", error.domain, (int) error.code);
            return;
        }

        NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse*) response;
        NSLog(@"task finished with status %d, bytes %zu", (int) httpResponse.statusCode, (size_t) data.length);
        
        NSDictionary * headers = httpResponse.allHeaderFields;
        NSLog(@"response-headers %@",headers);
    }];

    [task resume];
}
@end

Tackling the cache issue

1. We wrote the sample code that uses task metrics to print whether the request used the cache or not. This was later developed into a simple test application, consisting of a single “test” button that sends a request when tapped.

2. The cache was cleared — while making sure that all previous data was deleted in the test environment, to ensure that the cache was indeed empty.

3. Built (using Xcode 9.4) and ran (on the iPhone 11.4 simulator) the test app.

4. Issued a request for the first time. As expected, this hit the network:

… 12:42:22… metrics
… 12:42:22… [0]: network load
… 12:42:22… task finished with status 200, bytes 2810
… 12:42:22… response-headers {

}

5. Issued request again. This time it hit the cache:

… 12:42:25 metrics
… 12:42:25 [0]: local cache
… 12:42:25 task finished with status 200, bytes 2810
… 12:42:25 response-headers {

}

6. We repeated step 5 a few more times, just to be sure.

7. After waiting about half an hour we tapped Test again. It still hit the cache:

… 13:18:55… metrics
… 13:18:55… [0]: local cache
… 13:18:55… task finished with status 200, bytes 2810
… 13:18:55… response-headers {

}

Header reponse example:

:status
200
date
Mon, 30 Jul 2018 08:29:16 GMT
content-type
application/javascript; charset=utf-8
set-cookie
__cfduid=dac2b3d41a35b3dc2895ea452c24e243a1532939356; expires=Tue, 30-Jul-19 08:29:16 GMT; path=/; domain=.starbolt.io; HttpOnly
cache-control
cache-control public, max-age=60
etag
W/”afa-HGNjxwWr6QKVx9xDZEOCB4lRpyY”
x-powered-by
Express
cf-cache-status
HIT
vary
Accept-Encoding
expect-ct
server
cloudflare
cf-ray
442674df5e05ada5-TLV
content-encoding
gzip
 If not information still valid after 60 seconds make network request with respose: 304 Not Modified

 

:status
304
date
Mon, 30 Jul 2018 08:30:22 GMT
cache-control
cache-control public, max-age=60
etag
W/”afa-HGNjxwWr6QKVx9xDZEOCB4lRpyY”
x-powered-by
Express
cf-cache-status
HIT
vary
Accept-Encoding
expect-ct
server
cloudflare
cf-ray
4426767c793dada5-TLV

 

 

Share Button
 
Share Button

Leave a Reply

Your email address will not be published. Required fields are marked *

​​ Thanks for signing up to our blog!

Get the latest smart tips & news from our experts!