拦截并加载本地资源包
NSURLProtocol
公司的项目从 UIWebView 迁移到了 WKWebView。WKWebView性能更优,占用内存更少。
对H5请求进行拦截并加载本地资源,自然想到NSURLProtocol这个神器了。
NSURLProtocol能拦截所有当前app下的网络请求,并且能自定义地进行处理。使用时要创建一个继承NSURLProtocol的子类,不应该直接实例化一个NSURLProtocol。
核心方法
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
判断当前protocol是否要对这个request进行处理(所有的网络请求都会走到这里)。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
可选方法,对于需要修改请求头的请求在该方法中修改,一般直接返回request即可。
- (void)startLoading
重点是这个方法,拦截请求后在此处理加载本地的资源并返回给webview。
- (void)startLoading
{
//标示该request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:self.request];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL
MIMEType:mimeType
expectedContentLength:data.length
textEncodingName:nil];
//硬编码 开始嵌入本地资源到web中
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
}
复制代码
- (void)stopLoading
对于拦截的请求,NSURLProtocol对象在停止加载时调用该方法。
注册
[NSURLProtocol registerClass:[NSURLProtocolCustom class]];
其中NSURLProtocolCustom就是继承NSURLProtocol的子类。
但是开发时发现NSURLProtocol核心的几个方法并不执行,难道WKWebview不支持NSURLProtocol?
原来由于网络请求是在非主进程里发起,所以 NSURLProtocol 无法拦截到网络请求。除非使用私有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]];
复制代码
毕竟使用苹果私有api,这是在玩火呀。这篇文章《让 WKWebView 支持 NSURLProtocol》有很好的说明。比如我使用私有api字串拆分,运行时在组合,绕过审核。还可以对字符串加解密等等。。。
实际问题
通过以上处理,可以正常拦截处理,但是又发现拦截不了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拦截的方案。
技术支持:
https://juejin.im/post/5c9c664ff265da611624764d
https://www.cnblogs.com/zhuchenglin/p/7528250.html
https://segmentfault.com/p/1210000016879239/read
https://blog.csdn.net/thelittleboy/article/details/84027881
https://www.jianshu.com/p/05616e9a1c7f