优化 WebView 的加载速度之URL 拦截替换
前言
接着上一篇文章《优化 WebView 的加载速度实例》,记录一下本地缓存加载以及在没有缓存的情况下重定向请求线上资源的处理逻辑。
思路
为了保证网页正常加载,在没有缓存的情况下,不仅需要进行缓存模板的下载(为下次加载页面做准备),还需要同时在此刻加载线上的网页已达到当前页面正常展示的目的。流程如下:
加载逻辑
在加载一个 HTML 页面的时候,页面会加载一些资源文件,所以对于每个加载的资源文件我们都需要判断本地是否存在其缓存模板,如果没有则需要请求线上资源。所以对于 webView 中的每一个 request 请求,我们需要进行过滤,对 request 进行拦截转发处理,如果请求的资源文件本地存在缓存,那么加载本地资源,否则请求线上资源内容。
实现
为了保证每次请求的 request 都能够捕获拦截,可以通过继承NSURLProctol
方式创建系统拦截,通过查看该类介绍可以看出来该类为一个抽象类,介绍如下:
/*!
@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.
*/
大致意思就是系统在每次进行 URL 请求的时候会去实例一个 NSURLProtocol
对象进而处理 URL 请求内容。如果不去实例这个对象,系统会默认去实例一个默认的对象去处理URL 请求。
所以为了去拦截每次的 URL,我们可以去继承这个抽象类,进而达到拦截过滤处理的目的。
- 继承
NSURLProtocol
#import <Foundation/Foundation.h>
@interface LCWebCacheURLProtocol : NSURLProtocol
@end
- 实现抽象类的提供的方法
查看NSURLProtocol
介绍,要求我们必须实现标记之间的所有方法,所以,我们直接实现就好了,介绍如下
/*======================================================================
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
该实现决定当前请求是否需要进行处理,如果需要处理返回Yes
即可,不需要直接返回No
。这里需要注意一下,该方法是将当前页面的所有请求都遍历拦截一遍之后,才会去对后续的拦截方法进行回调。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
这个方法是在上一个方法返回 Yes 之后,对需要处理的 request进行操作的方法,但是由于形式参数与类中定义request 名字一样,这里操作 request 会引发歧义,为了避免出错,尽量不要在这里进行 request 的加工,这里直接返回 request
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
这个方法主要用来判断两个请求是否是同一个请求,如果是,则可以选择使用缓存数据,这里一般都是默认支持的,所以直接返回 Yes,且不做数据处理如果说前面是对于请求的处理以及配置,那么以下的两个方法则是数据请求的过程了
- (void)startLoading
在这里可以对 request 进行转发,包括NSURLSession
、NSURLConnection
甚至使用AFNetworking
等二次转发请求,最后将数据请求的结果回传给Client
来响应请求的发起者
- (void)stopLoading
看名字就知道这个与startLoading
是对应的,这个方法是用来断开Connection
链接的,当请求的发起者(NSURLProtocolClient
)的协议方法都回调完毕后,该方法会执行
实现上面的方法之后,在使用的时候通过注册的形式去将自定义NSURLProtocol
给系统,这样系统在进行 request 请求的时候就能通过注册传递的对象找到自定义的拦截内容了。
[NSURLProtocol registerClass:[LCWebCacheURLProtocol class]];
通过NSURLProtocol
的 registerClass
方法来进行注册,最后拦截转发内容逻辑统一在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(@"已经处理,放行");
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];
//响应请求者数据
[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 的拦截可以过滤需要处理的请求,进而将请求替换成本地缓存的内容,没有缓存直接去加载线上的文件内容(保证本次网页内容的正常加载),以这样拦截的方式结合本地模板加载使网页加载变得更为灵活不是吗?