iOS 中如何缓存服务器响应?

大致翻译自:How to cache server responses in iOS apps

通常与服务器通过 HTTP 进行通信的应用程序有两个特殊的要求: 尽可能的不让用户来等待数据和在没有网络的时候可以使用。Both are the source of much reinventing the wheel.

这些都是很常见的问题,所以不要吃惊。iOS有我们所需的APIs来实现响应缓存和离线模式。只需要很少量的代码,如果你的服务器处理cache header不错和你iOS目标是iOS7及以上,所需的代码还会更少。

共享的NSURLCache

共享的NSURLCache让我们“开箱即用”。如果我们的服务器使用了HTTP header的Cache-Control设置了响应的maximum age,NSURLSession(iOS7)和NSURLConnection 都会遵循这个maximum age,并在他们过期之前返回缓存的响应。我们不必编写任何代码来获取这个功能。
不幸的是,iOS的HTTP缓存目前还不怎么完善。

离线模式

如果响应已过期,app已离线,这时会发生什么呢?
默认情况下,如果没有互联网连接,NSURLSession 和NSURLConnection 是不会使用已过期的响应的。这是NSURLRequestUseProtocolCachePolicy的behavior,[NSURLRequest cachePolicy]的默认值会遵循HTTP协议的缓存头。
在大多数情况下,显示旧的数据比没有数据更好(除了例如天气、股票)。如果我们想离线模式始终会返回缓存的数据,那么我们的请求必须有个不同的缓存策略,这个缓存策略会忽略掉过期时间。NSURLRequestReturnCacheDataDontLoad 和 NSURLRequestReturnCacheDataElseLoad 都满足这一要求。尤其是,NSURLRequestReturnCacheDataElseLoad有如果没有找到缓存则会尝试网络的优势。
当然,当没有互联网连接时,我们将会使用缓存策略。如果我们可以监察到网络的状况,代码可能会如下所示:

NSURLRequest *request;
if (reachability.isReachable)
{
    request = [NSURLRequest requestWithURL:url];
}
else
{
    request = [NSURLRequest requestWithURL:url
                               cachePolicy:NSURLRequestReturnCacheDataElseLoad
                           timeoutInterval:60];
}

然后,把这个request给NSURLSession 或 NSURLConnection。
但是,这不是针对iOS6。在iOS6中有一个bug会阻止NSURLRequestReturnCacheDataDontLoad 和 NSURLRequestReturnCacheDataElseLoad不能正常工作。如果你是针对iOS6的,想实现一个真正的离线模式,你需要直接读取NSURLCache。

NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];

使用AFNetworking的离线模式

AFNetworking 2.0库提供了HTTP请求的managers,AFHTTPSessionManager (使用 NSURLSession) 和 AFHTTPRequestOperationManager (使用 NSURLConnection)。既可以通过子类化来自定义也可以重写创建data task或者操作的方法来自定义。

子类化AFHTTPSessionManager,通过重写dataTaskWithRequest:completionHandler:方法来修改URL请求的缓存策略:

@implementation MYHTTPSessionManager

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                            completionHandler:(void (^)(NSURLResponse *response,
                                                        id responseObject,
                                                        NSError *error))completionHandler
{
    NSMutableURLRequest *modifiedRequest = request.mutableCopy;
    AFNetworkReachabilityManager *reachability = self.reachabilityManager;
    if (!reachability.isReachable)
    {
         modifiedRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    }
    return [super dataTaskWithRequest:modifiedRequest 
                    completionHandler:completionHandler];
}

@end

同样地,在AFHTTPRequestOperationManager中:

@implementation MYHTTPRequestOperationManager

- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
                                                    success:(void (^)(AFHTTPRequestOperation *operation,
                                                                      id responseObject))success
                                                    failure:(void (^)(AFHTTPRequestOperation *operation,
                                                                      NSError *error))failure
{
    NSMutableURLRequest *modifiedRequest = request.mutableCopy;
    AFNetworkReachabilityManager *reachability = self.reachabilityManager;
    if (!reachability.isReachable)
    {
        modifiedRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    }
    return [super HTTPRequestOperationWithRequest:modifiedRequest
                                          success:success
                                          failure:failure];
}

@end

注意,在这两种情况下,我们都使用了AFNetworkReachabilityManager作为reachability的观察者,而不是自己实现。这也是使用AFNetworking的另一个优点。
AFNetworking并没有提供上面的iOS6bug的变通方法,并可能永远也不会提供。如果我们针对的是iOS6使用上述提供的缓存策略,还任然要直接读取缓存。

强制响应缓存

如果服务器没有设置cache headers会怎样呢?
好的服务器APIs是稀缺的,一致使用cache headers并不是给定的。即使在服务器上没有缓存策略,我们任然能够在客户端级别上通过添加或者替换cache headers来设置过期日期。这是在不能说服后台开发人员来添加cache headers时,作为最后的手段。

使用NSURLSession来强制响应缓存

NSURLSession的代理会接收到URLSession:dataTask:willCacheResponse:completionHandler。这提供了很好的机会来在缓存之前来修改它。如果服务器没有提供cache headers,我们可以这样做:

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    NSURLResponse *response = proposedResponse.response;
    NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse*)response;
    NSDictionary *headers = HTTPResponse.allHeaderFields;

    NSCachedURLResponse *cachedResponse;
    if (headers[@"Cache-Control"])
    {
        NSMutableDictionary *modifiedHeaders = headers.mutableCopy;
        [modifiedHeaders setObject:@"max-age=60" forKey:@"Cache-Control"];
        NSHTTPURLResponse *modifiedResponse = [[NSHTTPURLResponse alloc]
                                               initWithURL:HTTPResponse.URL
                                               statusCode:HTTPResponse.statusCode
                                               HTTPVersion:@"HTTP/1.1"
                                               headerFields:modifiedHeaders];

        cachedResponse = [[NSCachedURLResponse alloc]
                          initWithResponse:modifiedResponse
                          data:proposedResponse.data
                          userInfo:proposedResponse.userInfo
                          storagePolicy:proposedResponse.storagePolicy];

    }
    else
    {
        cachedResponse = proposedResponse;
    }

    completionHandler(cachedResponse);
}
使用NSURLConnection来强制响应缓存

同样地,NSURLConnection代理接收connection:willCacheResponse:,这是NSURLConnectionDataDelegate协议的一部分。在缓存响应前来修改响应头,如下:

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    NSURLResponse *response = cachedResponse.response;
    if ([response isKindOfClass:NSHTTPURLResponse.class]) return cachedResponse;

    NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse*)response;
    NSDictionary *headers = HTTPResponse.allHeaderFields;
    if (headers[@"Cache-Control"]) return cachedResponse;

    NSMutableDictionary *modifiedHeaders = headers.mutableCopy;
    modifiedHeaders[@"Cache-Control"] = @"max-age=60";
    NSHTTPURLResponse *modifiedResponse = [[NSHTTPURLResponse alloc]
                                           initWithURL:HTTPResponse.URL
                                           statusCode:HTTPResponse.statusCode
                                           HTTPVersion:@"HTTP/1.1"
                                           headerFields:modifiedHeaders];

    cachedResponse = [[NSCachedURLResponse alloc]
                      initWithResponse:modifiedResponse
                      data:cachedResponse.data
                      userInfo:cachedResponse.userInfo
                      storagePolicy:cachedResponse.storagePolicy];
    return cachedResponse;
}
使用AFNetworking来强制响应缓存

当使用AFNetworking时,代码因我们使用的哪个HTTP manager而不同。AFHTTPSessionManager作为NSURLSession的代理,我们只要在子类中实现URLSession:dataTask:willCacheResponse:completionHandler:方法,并调用其super方法,就可以了。

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    NSURLResponse *response = proposedResponse.response;
    NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse*)response;
    NSDictionary *headers = HTTPResponse.allHeaderFields;

    if (headers[@"Cache-Control"])
    {
        NSMutableDictionary *modifiedHeaders = headers.mutableCopy;
        modifiedHeaders[@"Cache-Control"] = @"max-age=60";
        NSHTTPURLResponse *modifiedHTTPResponse = [[NSHTTPURLResponse alloc]
                                                   initWithURL:HTTPResponse.URL
                                                   statusCode:HTTPResponse.statusCode
                                                   HTTPVersion:@"HTTP/1.1"
                                                   headerFields:modifiedHeaders];

        proposedResponse = [[NSCachedURLResponse alloc] initWithResponse:modifiedHTTPResponse
                                                                    data:proposedResponse.data
                                                                userInfo:proposedResponse.userInfo
                                                           storagePolicy:proposedResponse.storagePolicy];
    }

    [super URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
}

在AFHTTPRequestOperationManager中,AFHTTPRequestOperation有一个方便的block来设置connection:willCacheResponse:。这将使我们能够把我们的缓存代码放在重写的HTTPRequestOperationWithRequest:success:failure的方法中:

- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
                                                    success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                                                    failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
    NSMutableURLRequest *modifiedRequest = request.mutableCopy;

    AFNetworkReachabilityManager *reachability = self.reachabilityManager;
    if (!reachability.isReachable)
    {
        modifiedRequest.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    }

    AFHTTPRequestOperation *operation = [super HTTPRequestOperationWithRequest:modifiedRequest
                                                                       success:success
                                                                       failure:failure];

    [operation setCacheResponseBlock: ^NSCachedURLResponse *(NSURLConnection *connection,
                                                             NSCachedURLResponse *cachedResponse)
    {
        // Modify cache header as shown above
    }];
    return operation;
}

其他注意事项

NSURLCache 缓存响应,并不是分析它们的结果。复杂的响应可能需要相当长的时间,在这种情况下,最好使用自定义的缓存来缓存解析的对象,来代替NSURLCache。
此外,你可能想在缓存之前来验证缓存。服务器偶尔会返回错误(例如,“太多连接”),缓存这种响应会使用户得到同样地错误,直至缓存过期。

Further reading

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值