我是如何一步一步实现网页离线缓存的?

问题

一个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

posted @ 2017-11-28 13:18  zhanggui  阅读(2638)  评论(1编辑  收藏  举报