中国领先的IT技术网站
|
|

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

一个Hybrid APP,如何做离线缓存策略?也可以简单来说,你的APP只是一个壳,里面真正加载的内容是H5,如果优化加载内容的速度?

作者:佚名来源:张贵的站点|2017-11-29 11:14

开发者大赛路演 | 12月16日,技术创新,北京不见不散


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

  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
  2.     [NSURLProtocol registerClass:[ZGURLProtocol class]]; 
  3.     return YES; 

注册。接下来的所有操作就都是在我们自定义的ZGURLProtocol中操作了。先看一下registerClass的作用:

尝试注册一个NSURLProtocol的子类,使其对 URL Loading System 可见。这里的URL Loading System就是一组类和协议,允许你的应用程序访问由URL产生的内容,比如请求、接收内容和Cache等。当URL Load System开始加载一个请求的时候,每个注册的协议类都被依次去调用,以确定是否可以用指定的请求去初始化它。首先被调用的方法是:

  1. + (BOOL)canInitWithRequest:(NSURLRequest *)request; 

在该方法里面进行缓存过滤,比如你想只缓存js,那么判断request的path的后缀,如果是js,就返回YES,否则返回NO。

如果返回YES,那么就相当于该请求被自定义的URLProtocol来处理,这里不能保证所有的注册的NSURLProtocol都能被处理到。如果你定义了多个NSProtocol子类,这些子类将会以相反的顺序调用。也就是说如果你是这样写的:

  1. [NSURLProtocol registerClass:[ZGURLProtocol class]];  
  2. [NSURLProtocol registerClass:[ZProtocol class]]; 

那么最先执行的是ZProtocol,如果参initWithRequest:返回的为YES,则请求由ZProtocol进行处理,且不会再走ZGURLProtocol。如果ZProtocol的initWithRequest:返回的为NO,则请求继续向下传递由其他的NSURLProtocol子类处理。

一旦返回YES,那么请求将会由自己写的子类处理,首先会调用:

  1. + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 

这个是一个抽象的方法,子类必须对其实现。通常情况下,我们一般都是直接返回request,但是这里你也可以直接修改此request,包括header,hosts等。可以对指定request进行重定向操作。

在这里,我们只是将现有的request进行返回即可。

紧接着,便会开始请求:

  1. - (void)startLoading; 

该方法的作用就是开始请求protocol指定的请求。该方法也是protocol子类必须实现的方法。在这里所做的操作就是:

先判断是否有缓存数据,如果有,则自己创建NSURLResponse,然后将缓存数据放入,并进行client的一些操作,然后返回;如果没有缓存数据,则新建一个NSURLConnection,然后发送请求。

先说一下有缓存的情况下:

  1. if (model.data && model.MIMEType) { 
  2.         NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:model.MIMEType expectedContentLength:model.data.length textEncodingName:nil]; 
  3.         [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; 
  4.         [self.client URLProtocol:self didLoadData:model.data]; 
  5.         [self.client URLProtocolDidFinishLoading:self]; 
  6.         return
  7.     } 

(model是缓存数据)有缓存的情况下,直接使用缓存的数据和MIME类型,然后构建NSURLResponse,然后通过协议client调用代理方法。这里的client是一个protocol,如下:

  1. @protocol NSURLProtocolClient <NSObject>  
  2. - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; 
  3. - (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;  
  4. - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy; 
  5. - (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;  
  6. - (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;  
  7. - (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;  
  8. - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;  
  9. - (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;  
  10. @end 

该协议提供了NSURLProtocol子类与URL Loading System进行沟通的接口。一个APP一定不要去实现这个协议。有缓存的情况下调用回调方法,然后进行处理。

在没有缓存的情况下:

实例化一个connection,然后发起请求。在我们收到response的时候:

  1. - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 
  2.     self.responseData = [[NSMutableData alloc] init]; 
  3.     self.responseMIMEType = response.MIMEType; 
  4.     [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 

紧接着就是接收数据:

  1. - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 
  2.     [self.responseData appendData:data]; 
  3.     [self.client URLProtocol:self didLoadData:data]; 

接收完数据之后便调用了:

  1. - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 
  2.     ZGCacheModel *model = [ZGCacheModel new]; 
  3.     model.data = self.responseData; 
  4.     model.MIMEType = self.responseMIMEType; 
  5.     [self setMiType:model.MIMEType withKey:[self.request.URL path]];//userdefault存储MIMEtype 
  6.      
  7.      
  8.     [[ZGUIWebViewCache sharedWebViewCache] setCacheWithKey:self.request.URL.absoluteString value:model]; 
  9.    
  10.     [self.client URLProtocolDidFinishLoading:self]; 

这个方法是结束家在之后的调用,我们需要在这里将请求过来的数据进行缓存。这样我们本地就有了指定URL的返回数据。

这里还有一个重要的东西没有介绍,那就是

  1. [NSURLProtocol propertyForKey:ZGURLProtocolKey inRequest:request]  
  2. [NSURLProtocol setProperty:@YES forKey:ZGURLProtocolKey inRequest:mutableRequest]; 

这里的

  1. + (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. Android Bitmap缓存池使用详解
  2. WebView 缓存原理分析和应用
  3. 通过HTTP协议做Web缓存
  4. 如何手动清除Google Chrome DNS缓存
  5. 面对缓存,有哪些问题需要思考?
【责任编辑:未丽燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

Eclipse从入门到精通(第2版)

本书为《Eclipse从入门到精通》一书的全新改版。本书以最新的Eclipse 3.2作为写作版本。全书分为5篇:起步篇介绍了Eclipse及相关插件的安装...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊