使用NSURLProtocol實現離線快取
一、說明:
NSURLProtocol可以攔截任何網路請求,包含UIWebView中發出的所有請求。但是在WKWebView中,只能攔截到最初始的請求,內嵌的資源下載攔截不到。比如通過WKWebView載入"http://www.baidu.com",則只能攔截到"http://www.baidu.com",網頁內部的資源載入攔截不到。頁面跳轉屬於最初始請求之內,可以攔截到。
二、建立NSURLProtocol的子類,通過下面的程式碼註冊此協議類:
[NSURLProtocolregisterClass:[MyURLProtocolclass]];
三、下面是此子類的程式碼:
#import "MyURLProtocol.h"
#define MyURLProtocolHandled @"MyURLProtocolHandled"
//建立archive資料模型,重寫編碼解碼協議
@interface MyCacheData : NSObject
@property(nonatomic,strong) NSURLRequest *request;
@property(nonatomic,strong) NSURLResponse *response;
@property(nonatomic,strong) NSData *data;
@end
@interface NSURLRequest (MutableCopyWorkaround)
- (id)mutableCopyWorkaround;
@end
@interfaceMyURLProtocol ()
@property(nonatomic,strong) NSURLConnection *connection;
@property(nonatomic,strong) NSMutableData *httpData;
@property(nonatomic,strong) NSURLResponse *response;
@end
@implementation MyURLProtocol
#pragma mark - 重寫NSURLProtocol子類方法
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
//如果此請求是攔截到請求之後,接管請求而發起的新請求,則不處理。
if ([request.URL.scheme isEqualToString:@"http"] &&
[request valueForHTTPHeaderField:MyURLProtocolHandled] == nil)
{
return YES;
}
returnNO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a
toRequest:(NSURLRequest *)b
{
return [superrequestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading
{
//如果發現已經存在此請求的快取資料,則返回快取資料,否則發起新的請求從服務求載入資料
MyCacheData *cacheData = [NSKeyedUnarchiverunarchiveObjectWithFile:
[self cachePathForRequest:self.request]];
if(cacheData != nil)
{
NSData *data = cacheData.data;
NSURLResponse *response = cacheData.response;
NSURLRequest *redirectRequest = cacheData.request;
//使用NSURLProtocolClient做請求轉向,直接將請求和資料轉發到之前的請求
if(redirectRequest != nil)
{
[[selfclient] URLProtocol:selfwasRedirectedToRequest:redirectRequest
redirectResponse:response];
}
else
{
[[selfclient] URLProtocol:selfdidReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[[selfclient] URLProtocol:selfdidLoadData:data];
[[selfclient] URLProtocolDidFinishLoading:self];
}
}
else
{
//接管此網路請求,發起一個新的請求,後續會將新請求拿到的資料交給之前的舊請求
NSMutableURLRequest *connectionRequest = [[self request] mutableCopyWorkaround];
//增加標記,標示是由我們接管而發出的請求
[connectionRequest setValue:@"Tag" forHTTPHeaderField:MyURLProtocolHandled];
self.connection = [NSURLConnection connectionWithRequest:connectionRequest
delegate:self];
}
}
- (void)stopLoading
{
[self.connectioncancel];
self.connection = nil;
}
#pragma mark - 網路請求代理
- (NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)response
{
if(response != nil)
{
NSMutableURLRequest *redirectableRequest = [request mutableCopyWorkaround];
//快取資料
MyCacheData *cacheData = [MyCacheData new];
[cacheData setData:self.httpData];
[cacheData setResponse:response];
[cacheData setRequest:redirectableRequest];
[NSKeyedArchiver archiveRootObject:cacheData
toFile:[self cachePathForRequest:[self request]]];
//將請求和快取的響應資料轉向到之前的請求
[[selfclient] URLProtocol:selfwasRedirectedToRequest:redirectableRequest
redirectResponse:response];
return redirectableRequest ;
}
return request;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection
{
returnYES;
}
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
[self.clientURLProtocol:selfdidReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
[self.clientURLProtocol:selfdidCancelAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
//儲存響應物件
self.response = response;
[self.clientURLProtocol:selfdidReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
[self.clientURLProtocol:selfdidLoadData:data];
//儲存伺服器返回的資料
if(self.httpData == nil) {
self.httpData = [NSMutableData dataWithData: data];
}
else
{
[self.httpData appendData:data];
}
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self.clientURLProtocolDidFinishLoading:self];
//請求載入完畢之後,將資料快取
MyCacheData *cacheData = [MyCacheData new];
[cacheData setData:self.httpData];
[cacheData setResponse:self.response];
[NSKeyedArchiverarchiveRootObject:cacheData
toFile:[self cachePathForRequest:self.request]];
self.connection = nil;
self.httpData = nil;
self.response = nil;
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[self.clientURLProtocol:selfdidFailWithError:error];
self.connection = nil;
self.httpData = nil;
self.response = nil;
}
#pragma mark - 為請求建立快取路徑
- (NSString *)cachePathForRequest:(NSURLRequest *)aRequest
{
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
NSUserDomainMask, YES) lastObject];
return [cachesPath stringByAppendingPathComponent:
[NSString stringWithFormat:@"%ld", [[[aRequest URL] absoluteString] hash]]];
}
@end
@implementation NSURLRequest (MutableCopyWorkaround)
- (id) mutableCopyWorkaround {
NSMutableURLRequest *mutableURLRequest = [[NSMutableURLRequestalloc]
initWithURL:[self URL]
cachePolicy:[self cachePolicy]
timeoutInterval:[self timeoutInterval]];
[mutableURLRequest setAllHTTPHeaderFields:[selfallHTTPHeaderFields]];
return mutableURLRequest;
}
@end
@implementation MyCacheData
-(id) initWithCoder:(NSCoder *) aDecoder
{
self = [super init];
if(!self) {
return nil;
}
[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"];
}
@end
部分程式碼轉自:http://tanlimin201.blog.163.com/blog/static/38171407201383032914736/