前言
接著上一篇文章《優(yōu)化 WebView 的加載速度實例》,記錄一下本地緩存加載以及在沒有緩存的情況下重定向請求線上資源的處理邏輯。
思路
為了保證網(wǎng)頁正常加載,在沒有緩存的情況下,不僅需要進行緩存模板的下載(為下次加載頁面做準備),還需要同時在此刻加載線上的網(wǎng)頁已達到當前頁面正常展示的目的。流程如下:
加載邏輯
在加載一個 HTML 頁面的時候,頁面會加載一些資源文件,所以對于每個加載的資源文件我們都需要判斷本地是否存在其緩存模板,如果沒有則需要請求線上資源。所以對于 webView 中的每一個 request 請求,我們需要進行過濾,對 request 進行攔截轉(zhuǎn)發(fā)處理,如果請求的資源文件本地存在緩存,那么加載本地資源,否則請求線上資源內(nèi)容。
實現(xiàn)
為了保證每次請求的 request 都能夠捕獲攔截,可以通過繼承NSURLProctol 方式創(chuàng)建系統(tǒng)攔截,通過查看該類介紹可以看出來該類為一個抽象類,介紹如下:
/*!
@class NSURLProtocol
@abstract NSURLProtocol is an abstract class which provides the
basic structure for performing protocol-specific loading of URL
data. Concrete subclasses handle the specifics associated with one
or more protocols or URL schemes.
*/
大致意思就是系統(tǒng)在每次進行 URL 請求的時候會去實例一個 NSURLProtocol 對象進而處理 URL 請求內(nèi)容。如果不去實例這個對象,系統(tǒng)會默認去實例一個默認的對象去處理URL 請求。
所以為了去攔截每次的 URL,我們可以去繼承這個抽象類,進而達到攔截過濾處理的目的。
- 繼承
NSURLProtocol
#import <Foundation/Foundation.h>
@interface LCWebCacheURLProtocol : NSURLProtocol
@end
- 實現(xiàn)抽象類的提供的方法
查看NSURLProtocol介紹,要求我們必須實現(xiàn)標記之間的所有方法,所以,我們直接實現(xiàn)就好了,介紹如下
/*======================================================================
Begin responsibilities for protocol implementors
The methods between this set of begin-end markers must be
implemented in order to create a working protocol.
======================================================================*/
#import "LCWebCacheURLProtocol.h"
@implementation LCWebCacheURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b{
return YES;
}
- (void)startLoading{
}
- (void)stopLoading{
}
@end
這里說一下這幾個方法的含義
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
該實現(xiàn)決定當前請求是否需要進行處理,如果需要處理返回Yes即可,不需要直接返回No。這里需要注意一下,該方法是將當前頁面的所有請求都遍歷攔截一遍之后,才會去對后續(xù)的攔截方法進行回調(diào)。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
這個方法是在上一個方法返回 Yes 之后,對需要處理的 request進行操作的方法,但是由于形式參數(shù)與類中定義request 名字一樣,這里操作 request 會引發(fā)歧義,為了避免出錯,盡量不要在這里進行 request 的加工,這里直接返回 request
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
這個方法主要用來判斷兩個請求是否是同一個請求,如果是,則可以選擇使用緩存數(shù)據(jù),這里一般都是默認支持的,所以直接返回 Yes,且不做數(shù)據(jù)處理如果說前面是對于請求的處理以及配置,那么以下的兩個方法則是數(shù)據(jù)請求的過程了
- (void)startLoading
在這里可以對 request 進行轉(zhuǎn)發(fā),包括NSURLSession、NSURLConnection甚至使用AFNetworking等二次轉(zhuǎn)發(fā)請求,最后將數(shù)據(jù)請求的結果回傳給Client來響應請求的發(fā)起者
- (void)stopLoading
看名字就知道這個與startLoading是對應的,這個方法是用來斷開Connection鏈接的,當請求的發(fā)起者(NSURLProtocolClient)的協(xié)議方法都回調(diào)完畢后,該方法會執(zhí)行
實現(xiàn)上面的方法之后,在使用的時候通過注冊的形式去將自定義NSURLProtocol給系統(tǒng),這樣系統(tǒng)在進行 request 請求的時候就能通過注冊傳遞的對象找到自定義的攔截內(nèi)容了。
[NSURLProtocol registerClass:[LCWebCacheURLProtocol class]];
通過NSURLProtocol的 registerClass方法來進行注冊,最后攔截轉(zhuǎn)發(fā)內(nèi)容邏輯統(tǒng)一在startLoading進行操作即可。整體請求的處理也就變成了醬紫。
#import "LCWebCacheURLProtocol.h"
static NSString * const filiterKeyword = @"gc";
static NSString * const replaceKeyword = @"gc://";
static NSString * const dealRequestKey = @"dealRequestKey";
@interface LCWebCacheURLProtocol()<NSURLSessionDelegate>
@property (nonatomic,strong) NSURLSession *session;
@property (nonatomic, strong) NSURLConnection *connection;
@property (atomic, strong) NSMutableData *data;
//是否處于下載模板中
@property (nonatomic, assign) BOOL isDownloadingTemplate;
@end
@implementation LCWebCacheURLProtocol
+ (void)registCacheUrlProtocol{
[self registerClass:[self class]];
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
NSString *scheme = request.URL.scheme;
//如果處理過請求,放行
if ([NSURLProtocol propertyForKey:dealRequestKey inRequest:request]) {
NSLog(@"已經(jīng)處理,放行");
return NO;
}
//攔截帶有特定標識的請求
if ([scheme isEqualToString:filiterKeyword]) {
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b{
return YES;
}
- (void)startLoading{
NSString *scheme = self.request.URL.scheme;
NSMutableURLRequest *modifityRequest = [self.request mutableCopy];
NSString *url = modifityRequest.URL.absoluteString;
//處理需要處理的請求
if ([scheme isEqualToString:filiterKeyword]) {
/*** 獲取項目中的文件路徑 ***/
NSString *file = [url lastPathComponent];
//擴展名
NSString *suffix = [file pathExtension];
//文件名字
NSString *fileName = [file stringByDeletingPathExtension];
NSString *modifiyPath = [[NSBundle mainBundle] pathForResource:fileName ofType:suffix];
//判斷本地文件是否存在
BOOL isVaild = [[NSURL fileURLWithPath:modifiyPath] checkResourceIsReachableAndReturnError:nil];
if (isVaild) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:modifiyPath]];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL fileURLWithPath:modifiyPath] MIMEType:suffix expectedContentLength:data.length textEncodingName:nil];
//響應請求者數(shù)據(jù)
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
}
//文件不存在,加載線上地址
else{
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
NSMutableURLRequest *changeRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
NSLog(@"modifityURL:%@",changeRequest.URL.description);
//設置當前處理的請求的標志,防止重復處理請求
[NSURLProtocol setProperty:@YES
forKey:dealRequestKey
inRequest:changeRequest];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:self.request];
[task resume];
/*********** 以下是處理異步下載模板的邏輯(為下次加載當前頁面的模板做準備)****/
/*
self.isDownloadingTemplate = YES;
//異步下載模板
WeakObj(self);
[GCHtmlTemplateTool updateDetailHtmlStyle:^{
NSLog(@"下載模板成功");
selfWeak.isDownloadingTemplate = NO;
}];
*/
}
}
}
- (void)stopLoading{
[self.session invalidateAndCancel];
}
#pragma -mark- NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
completionHandler(NSURLSessionResponseAllow);
self.data = [NSMutableData new];
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
[self.data appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
[self.client URLProtocol:self didFailWithError:error];
}
else {
[self.client URLProtocol:self didLoadData:self.data];
[self.client URLProtocolDidFinishLoading:self];
}
}
@end
通過URL 的攔截可以過濾需要處理的請求,進而將請求替換成本地緩存的內(nèi)容,沒有緩存直接去加載線上的文件內(nèi)容(保證本次網(wǎng)頁內(nèi)容的正常加載),以這樣攔截的方式結合本地模板加載使網(wǎng)頁加載變得更為靈活不是嗎?
