H5侧加载优化
先了解下webview加载一个页面干了哪些事情:
初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片

页面从初始化webview开始到DOM渲染页面过程就是用户加载等待的过程,DOM渲染是将CSS/HTML转化成用户可见的页面,如果提高这个速度就能提升页面的加载速度。
H5 页面从发起请求到最终渲染完成的完整流程可分为 网络请求、HTML 解析、资源加载、DOM 构建、渲染 几个阶段

所以从H5段分析来看,页面显示需要满足HTML 文档完全解析(DOM 树构建完成)、所有同步的 <script> 执行完毕(除非标记为 async 或 defer)才能正常渲染页面,所以我们可以采用以下措施:
1、iframe加载
2、异步加载的 JS(<script async> 或 <script defer>)。
3、降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
4、加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
5、缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
6、渲染:JS/CSS优化,加载顺序,服务端渲染模板直出。
实际项目中前端能做的很有限,其实H5的主要消耗就在HTML、CSS、IMage、JS等资源的下载,如果我们将这些资源本地化到客户端就能解决资源加载的问题,这种离线包方案网上有很多现成的方案可以直接使用。
客户端加载方案:
这里我们介绍下自己实现离线包方案:

离线包的存放和更新机制
可以使用CDN存放静态资源,同时包含资源信息的配置json文件
//配置json文件内容
{
"version":"********",
"files": [
"https://xxx/index.html",
"https://xxx.png",
"https://xxx.js",
"https://xxx.css",
"https://xxx.json"
]
}
当APP冷启动是请求CDN的json文件, 对比json中间中version和本地不一致时需要重新请求files中的资源,下载完成后替换本地资源。
本地包更新完成后需要拦截并将本地包内容显示给H5
拦截并加载本地资源包
NSURLProtocol
NSURLProtocol是Foundation框架提供的网络请求拦截抽象类,可以拦截大多数基于URL Loading System的请求
拦截能力取决于具体使用的网络框架和连接方式
某些特殊请求和连接方式无法被拦截
使用方式:使用时要创建一个继承NSURLProtocol的子类,不应该直接实例化一个NSURLProtocol。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
判断当前protocol是否要对这个request进行处理(所有的网络请求都会走到这里)。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
可选方法,对于需要修改请求头的请求在该方法中修改,一般直接返回request即可。
- (void)startLoading
重点是这个方法,拦截请求后在此处理加载本地的资源并返回给webview。
- (void)stopLoading
对于拦截的请求,NSURLProtocol对象在停止加载时调用该方法
- (void)startLoading {
//标示该request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:self.request];
NSData *data = [NSData dataWithContentsOfFile:filePath];
//硬编码 开始嵌入本地资源到web中
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
}
但是在使用时我们发现WKWebView中需要网络请求并没有被拦截到,原因是WKWebView 使用独立的网络进程(iOS 11+),与传统的 URL Loading System 分离,还有WKWebView使用ajax请求页是无法被拦截。
当然在调试是我们也可以通过拦截苹果私有API方法WKBrowsingContextController和registerSchemeForCustomProtocol通过反射的方式拿到了私有的 class/selector。通过把注册把 http 和 https 请求交给 NSURLProtocol 处理,不过这种方式是不允许上架到苹果商店的。
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 请求交给 NSURLProtocol 处理
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
}
// 这下 NSURLProtocolCustom 就可以用啦
[NSURLProtocol registerClass:[NSURLProtocolCustom class]];
通过以上处理,可以正常拦截处理,但是又发现拦截不了post请求(拦截到的post请求body体为空),即使在canInitWithRequest:方法中设置对于POST请求的request不处理也不能解决问题。内流。。。
经了解,算是 WebKit 的一个缺陷吧。首先 WebKit 进程是独立于 app 进程之外的,两个进程之间使用消息队列的方式进行进程间通信。比如 app 想使用 WKWebView 加载一个请求,就要把请求的参数打包成一个 Message,然后通过 IPC 把 Message 交给 WebKit 去加载,反过来 WebKit 的请求想传到 app 进程的话(比如 URLProtocol ),也要打包成 Message 走 IPC。出于性能的原因,打包的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了,这个可以参考 WebKit 的源码,这就导致 -[WKWebView loadRequest:] 传出的 HTTPBody 和 NSURLProtocol 传回的 HTTPBody 全都被丢弃掉了。 所以如果通过 NSURLProtocol 注册拦截 http scheme,那么由 WebKit 发起的所有 http POST 请求就全都无效了,这个从原理上就是无解的。
当然网上也出现一些解决方案,但是本人尝试没有成功。同时拦截后对ATS支持不好。再结合又使用了苹果私有API有被拒风险,最终决定弃用NSURLProtocol拦截的方案。
WKURLSchemeHandler
iOS11以后苹果推出了WKURLSchemeHandler来拦截资源请求。
使用前要与前端统一URL-Scheme,如:myScheme,资源定义成myScheme://xxx/path/xxxx.css。native端使用时,先注册myScheme,WKWebView请求加载网页,遇到myScheme的资源,就会被hock住,然后使用本地已下载好的资源进行加载。
//本例中WKWebView将把URLScheme为customScheme的请求交由CustomURLSchemeHandler类的实例处理
//在WKWebview发起loadRequest之前注册
[configuration setURLSchemeHandler:[MySchemeHandler new] forURLScheme: @"myScheme"];
注意:
setURLSchemeHandler注册时机只能在WKWebView创建WKWebViewConfiguration时注册。
WKWebView 只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。
【补充】WKWebView加载网页前,要在user-agent添加个标志,H5遇到这个标识就使用customScheme,否则就是用原来的http或https。
在MySchemeHandler中将拦截到的请求改成本地资源放回给H5,如果没有请求到本地资源就转成http、https发送给H5.
在联调本地 H5 页面过程中,发现首次加载页面时间比后续打开时间都慢很多,原因预计是 webView 首次初始化时候需要启动资源和服务较多,于是尝试客户端在预先初始化 webView 方案,果然这样第一次打开页面时候就变快了。同时为了 H5 在第一次打开时能直接展示数据,客户端在页面打开前就预拉取数据并缓存,这样来减少请求数据时间导致的白屏。详细教程请参考: