hi all:
由于UIWebView无法实现离线缓存,因此想利用Archieve机制来实现文件形式的离线缓存机制。同时,由于NSURLRequest每一次对链接的请求,都将触发NSURLProtocol的回调,因此对NSURLProtocol合理应用可以很好的达到离线缓存的目的。
一、NSURLProtocol与NSURLProtocolClient简介:
首先,我先介绍一下NSURLProtocol与NSURLProtocolClient:
NSURLProtocol是一组方法,其中苹果文档是这样描述的:
NSURLProtocol is an abstract class which provides the basic structure for performing protocol-specific loading of URL data.
它是一个抽象类,为载入URL的data的一些特定协议提供基础的结构。要实现它里面的函数就必须继承它,因此小Potti将在后面创建一个MWURLProtocol类继承它,并实现它其中的一系列函数。
而NSURLProtocol其中有个成员就是NSURLProtocolClient的一个实例。因为NSURLProtocol是由一系列的回调函数构成的(注册函数除外),而要对URL的data进行各种操作时就到了调用NSURLProtocolClient实例的时候了,这就实现了一个钩子,去操作URL data。
NSURLProtocol有以下一系列的回调方法:
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client;
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;
其中canInitWithRequest是询问是否处理该请求的回调,如果不处理则后面所有函数都不会再调用。startLoading和stopLoading是分别对于loading开始从网页上抓取数据,从网页上抓取完数据的回调。其中startLoading称为我们可以重点利用的函数。
NSURLProtocolClient主要有以下方法:
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
其中wasRedirectedToRequest是重定向函数,cachedResponseIsValid是对cached的操作,didReceiveResponse是受到Response时的调用处理函数, didLoadData是load完数据时的调用,而后面的大致也与函数题目意思一样。而这些函数有个好处就是通通只需要我们调用它,系统就会做对应的事情,重载它也可以,不过一般不用这么麻烦。
看到这些函数是不是想到了NSURLConnectionDataDelegate中的回调呢?哈哈,其实小Potti将在后面对2者有一个很好的结合。
二、离线缓存算法的流程简介:
1.向NSURLProtocol注册MWURLProtocol;
2.系统回调canInitWithRequest和canonicalRequestForRequest;其中,为了防止后续startLoading中可能存在的死循环,因此在canInitWithRequest我们会检查是否是已经处理过的request。如果不是则返回YES,否则返回NO。
3.系统回调startLoading;如果当前网络不可用,直接读取文件中的data,request,response,构建一个假的请求并直接调用NSURLProtocolClient中的处理函数进行处理。结束。
若网络可用,则创建一个NSURLConnection,将处理的request与这个connection钩起来,同时实现NSConnectionDataDelegate的回调,这样,我们就将NSURLConnection与NSURLProtocol钩起来。钩起来后会执行4:
4.在statLoading调用之后系统将回调NSConnectionDataDelegate中的各个回调函数,在回调函数中,我们在各个位置分别调用合适的NSURLProtocolClient的方法,同时到下载完成后,我们就将下载到的Data,request等信息存储起来。
三、离线缓存算法源代码:
@interface NSURLRequest(MutableCopyWorkaround)
- (id) mutableCopyWorkaround;
@end
@implementation MWURLProtocol
static NSString *RNCachingURLHeader = @"PottiTest";
@synthesize connection = connection_;
@synthesize data = data_;
@synthesize response = response_;
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([[[request URL] scheme] isEqualToString:@"http"] &&
([request valueForHTTPHeaderField:RNCachingURLHeader] == nil)) {
return YES;
}
NSLog(@"%@",[request allHTTPHeaderFields]);
returnNO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (NSString *)cachePathForRequest:(NSURLRequest *)aRequest
{
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
return [cachesPath stringByAppendingPathComponent:[NSStringstringWithFormat:@"%x", [[[aRequest URL] absoluteString] hash]]];
}
- (void)startLoading
{
if([netWorkReachabilityconnectedToNetWork])
{
NSMutableURLRequest *connectionRequest = [[self request] mutableCopyWorkaround];
NSLog(@"%@",[connectionRequest HTTPBody]);
NSLog(@"%@",[connectionRequest URL]);
[connectionRequest setValue:@"" forHTTPHeaderField:RNCachingURLHeader];
NSLog(@"%@",[connectionRequest allHTTPHeaderFields]);
NSURLConnection *connection = [NSURLConnection connectionWithRequest:connectionRequest
delegate:self];
[self setConnection:connection];
}
else
{
MWCacheData *cacheData=[NSKeyedUnarchiver unarchiveObjectWithFile:[self cachePathForRequest:[self request]]];
if(cacheData)
{
NSData *data = [cacheData data];
NSURLResponse *response = [cacheData response];
NSURLRequest *redirectRequest = [cacheData request];
if(redirectRequest)
{
[[selfclient] URLProtocol:selfwasRedirectedToRequest:redirectRequest redirectResponse:response];
}
else
{
[[selfclient] URLProtocol:selfdidReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; // we handle caching ourselves.
[[self client] URLProtocol:self didLoadData:data];
[[selfclient] URLProtocolDidFinishLoading:self];
}
}
else
{
[[selfclient] URLProtocol:selfdidFailWithError:[NSErrorerrorWithDomain:NSURLErrorDomaincode:NSURLErrorCannotConnectToHostuserInfo:nil]];
}
}
}
- (void)stopLoading
{
[[self connection] cancel];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
if(response!=nil)
{
NSMutableURLRequest *redirectableRequest = [request mutableCopyWorkaround];
[redirectableRequest setValue:nil forHTTPHeaderField:RNCachingURLHeader];
MWCacheData *cacheData = [MWCacheData new];
[cacheData setData:[self data]];
[cacheData setResponse:response];
[cacheData setRequest:redirectableRequest];
[NSKeyedArchiver archiveRootObject:cacheData toFile:[self cachePathForRequest:[self request]]];
[[selfclient] URLProtocol:selfwasRedirectedToRequest:redirectableRequest redirectResponse:response];
return redirectableRequest ;
}
return request;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[[selfclient] URLProtocol:selfdidFailWithError:error];
[selfsetResponse:nil];
[self setData:nil];
[selfsetConnection:nil];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(@"didReceiveResponse");
[self setResponse:response];
[[selfclient] URLProtocol:selfdidReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)adata
{
NSLog(@"didReceiveData");
[[selfclient] URLProtocol:selfdidLoadData:adata];
if([self data] == nil) [self setData:[NSMutableData dataWithData: adata]];
else [[self data] appendData:adata];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
NSLog(@"willCacheResponse");
return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"connectionDidFinishLoading");
[[selfclient] URLProtocolDidFinishLoading:self];
MWCacheData *cacheData = [MWCacheData new];
[cacheData setData:[self data]];
[cacheData setResponse:[self response]];
[NSKeyedArchiverarchiveRootObject:cacheData toFile:[selfcachePathForRequest:[selfrequest]]];
[self setData:nil];
[selfsetConnection:nil];
[selfsetResponse:nil];
}
@end
@implementation NSURLRequest(MutableCopyWorkaround)
- (id) mutableCopyWorkaround {
NSMutableURLRequest *mutableURLRequest = [[NSMutableURLRequestalloc] initWithURL:[selfURL]
cachePolicy:[self cachePolicy]
timeoutInterval:[self timeoutInterval]];
[mutableURLRequest setAllHTTPHeaderFields:[selfallHTTPHeaderFields]];
NSLog(@"%@",[mutableURLRequest allHTTPHeaderFields]);
NSLog(@"%@",[mutableURLRequest valueForHTTPHeaderField:RNCachingURLHeader]);
NSLog(@"%@",[mutableURLRequest HTTPBody]);
NSLog(@"%@",[mutableURLRequest HTTPMethod]);
return mutableURLRequest;
}
@end
@implementation MWCacheData
@synthesize response=response_;
@synthesize request=request_;
@synthesize data=data_;
-(id) initWithCoder:(NSCoder *) aDecoder
{
self = [super init];
if(!self) returnnil;
[self setData:[aDecoder decodeObjectForKey:@"data"]];
[self setRequest:[aDecoder decodeObjectForKey:@"request"]];
[self setResponse:[aDecoder decodeObjectForKey:@"response"]];
returnself;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:[self data] forKey:@"data"];
[aCoder encodeObject:[self request] forKey:@"request"];
[aCoder encodeObject:[self response] forKey:@"response"];
}