一個Hybrid APP,如何做離線緩存策略?也可以簡單來說,你的APP只是一個殼,裡面真正載入的內容是H5,如果優化載入內容的速度? ...
問題
一個Hybrid APP,如何做離線緩存策略?也可以簡單來說,你的APP只是一個殼,裡面真正載入的內容是H5,如果優化載入內容的速度?
先瞭解一下NSURLProtocol
從字面意思看它是一個協議,但是它其實是一個類,而且繼承自NSObject。它的作用是處理特定URL協議的載入。它本身是一個抽象類,提供了使用特性URL方案處理URL的基礎結構。你可以自己創建NSURLProtocol的子類,來讓自己的應用支持自定義的協議或者URL方案。
應用程式永遠不需要直接實例化一個NSURLProtocol子類。當一個下載開始的時候,系統創建一個合適的protoco對象來響應URL請求。你要做的就是自己定義一個你自己的protocol,然後在APP啟動的時候調用registerClass:,讓系統知道你的協議。
這裡需要註意:你不能在watchOS 2以及更高版本中自定義URL scheme和協議。
為了支持特定的自定義請求,你最好定義NSURLRequest 或者NSMutableURLRequest。讓自定義的這些對象來實現請求,這裡需要使用NSURLProtocol的propertyForKey:inRequest:和setProperty:forKey:inRequest,然後你可以自定義NSURLResponse類來模擬返回信息。
接下來就開始對UIWebView進行離線緩存處理。
UIWebView的離線緩存處理
首先,我們需要自定義一個NSURLProtocol的子類,並且在AppDelegate.m的
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[ZGURLProtocol class]];
return YES;
}
註冊。接下來的所有操作就都是在我們自定義的ZGURLProtocol中操作了。先看一下registerClass的作用:
嘗試註冊一個NSURLProtocol的子類,使其對URL Loading System可見。這裡的URL Loading System就是一組類和協議,允許你的應用程式訪問由URL產生的內容,比如請求、接收內容和Cache等。當URL Load System開始載入一個請求的時候,每個註冊的協議類都被依次去調用,以確定是否可以用指定的請求去初始化它。首先被調用的方法是:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
在該方法裡面進行緩存過濾,比如你想只緩存js,那麼判斷request的path的尾碼,如果是js,就返回YES,否則返回NO。
如果返回YES,那麼就相當於該請求被自定義的URLProtocol來處理,這裡不能保證所有的註冊的NSURLProtocol都能被處理到。如果你定義了多個NSProtocol子類,這些子類將會以相反的順序調用。也就是說如果你是這樣寫的:
[NSURLProtocol registerClass:[ZGURLProtocol class]];
[NSURLProtocol registerClass:[ZProtocol class]];
那麼最先執行的是ZProtocol,如果參initWithRequest:返回的為YES,則請求由ZProtocol進行處理,且不會再走ZGURLProtocol。如果ZProtocol的initWithRequest:返回的為NO,則請求繼續向下傳遞由其他的NSURLProtocol子類處理。
一旦返回YES,那麼請求將會由自己寫的子類處理,首先會調用:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
這個是一個抽象的方法,子類必須對其實現。通常情況下,我們一般都是直接返回request,但是這裡你也可以直接修改此request,包括header,hosts等。可以對指定request進行重定向操作。
在這裡,我們只是將現有的request進行返回即可。
緊接著,便會開始請求:
- (void)startLoading;
該方法的作用就是開始請求protocol指定的請求。該方法也是protocol子類必須實現的方法。在這裡所做的操作就是:
先判斷是否有緩存數據,如果有,則自己創建NSURLResponse,然後將緩存數據放入,併進行client的一些操作,然後返回;如果沒有緩存數據,則新建一個NSURLConnection,然後發送請求。
先說一下有緩存的情況下:
if (model.data && model.MIMEType) {
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:model.MIMEType expectedContentLength:model.data.length textEncodingName:nil];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
[self.client URLProtocol:self didLoadData:model.data];
[self.client URLProtocolDidFinishLoading:self];
return;
}
(model是緩存數據)有緩存的情況下,直接使用緩存的數據和MIME類型,然後構建NSURLResponse,然後通過協議client調用代理方法。這裡的client是一個protocol,如下:
@protocol NSURLProtocolClient <NSObject>
- (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;
@end
該協議提供了NSURLProtocol子類與URL Loading System進行溝通的介面。一個APP一定不要去實現這個協議。有緩存的情況下調用回調方法,然後進行處理。
在沒有緩存的情況下:
實例化一個connection,然後發起請求。在我們收到response的時候:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.responseData = [[NSMutableData alloc] init];
self.responseMIMEType = response.MIMEType;
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
緊接著就是接收數據:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
[self.client URLProtocol:self didLoadData:data];
}
接收完數據之後便調用了:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
ZGCacheModel *model = [ZGCacheModel new];
model.data = self.responseData;
model.MIMEType = self.responseMIMEType;
[self setMiType:model.MIMEType withKey:[self.request.URL path]];//userdefault存儲MIMEtype
[[ZGUIWebViewCache sharedWebViewCache] setCacheWithKey:self.request.URL.absoluteString value:model];
[self.client URLProtocolDidFinishLoading:self];
}
這個方法是結束家在之後的調用,我們需要在這裡將請求過來的數據進行緩存。這樣我們本地就有了指定URL的返回數據。
這裡還有一個重要的東西沒有介紹,那就是
[NSURLProtocol propertyForKey:ZGURLProtocolKey inRequest:request]
[NSURLProtocol setProperty:@YES forKey:ZGURLProtocolKey inRequest:mutableRequest];
這裡的
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
作用是在指定的請求中設置與特定的鍵值相關聯。防止多次調用一個request。
這樣,我們就完成了UIWebView的離線緩存。在這裡我封裝了一個ZGUIWebViewCache。感興趣的可以看一下。
WKWebView的離線緩存處理
WKWebView離線緩存和UIWebView緩存類似,只不過使用WKWebView除了一開始調用一下NSURLProtocol的canInitWithRequest:方法之後,之後的請求似乎就和NSURLProtocol完全無關了,網上都說WKWebView的請求是在獨立的進程里,所以不走NSURLProtocol。這裡是通過NSURLProtocol+WKWebView類進行處理的,詳情可參見:ZGWKWebViewCache。
剩下的處理過程就和UIWebView緩存處理類似了。
以上便是對網頁離線緩存的實現。
如有問題,歡迎留言溝通!
參考
1.讓 WKWebView 支持 NSURLProtocol