背景
在上文,笔者介绍了如何拦截资源请求。但文中有些疏漏之处,也有读者在评论区里提到了,会遇到This task has already been stopped这样的崩溃,导致崩溃率变高。但我们其实已经通过取消请求机制处理了大部分,常规使用上,这个崩溃就没再遇到,导致忽视了。
这次也是通过线上崩溃分析,发现还是有类似的崩溃出现。通过堆栈分析,锁定到如下代码:
本质原因
闲话少说,先来聊一下出现这个崩溃的本质原因是什么:
究其根本,是因为WKWebView
和我们自身的 App 所属2个不同的进程,这里的WKURLSchemeTask
本质上是进程间通信的句柄。
进程间通信是一个耗时操作,且不能保证连接的双方生命周期一致。这不是线程通信,我们可以通过引用计数来持有对象。
这就导致,在传输前或者传输中,经过WKURLSchemeTask
传输时,WKWebView
所在进程被释放,就会导致出现This task has already been stopped异常抛出崩溃。
问题分析
传输前
如果是WKURLSchemeTask
传输前,我们在上文中已经做了防护:
通过系统提供的有限的2个代理方法,我们可以拿到停止的时机:
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
[task cancel]; // 这里执行任务停止动作
}
传输中(本次遇到)
在传输前拦截已经可以杜绝大部分崩溃情况, 但有一种情况会导致在传输中发生崩溃:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
...
[self.schemeTask didReceiveData:data]; // 崩溃出现在这里
}
原因是返回的数据是分片的,这里的data
不是完整数据,代理方法会分片返回多次data
。
复现方式也被我们找到了:在加载大文件时,马上退出WKWebView
,极易出现该问题。
这在系统设计上也是合理的,但现象是我们的WKURLSchemeTask
数据传输无法良好的停下来,我们尝试过在[self.schemeTask didReceiveData:data]
前增加各种isCanceled
的判断也然并卵。
经过排查发现,崩溃的不是当次的传输,而是上一次未完成的传输。上文也说道,这个进程间通信是耗时操作,在数据量小的情况下极不易发生崩溃,而数据量大(都产生分片了)的情况下,就很有可能在传输过程中产生中断,导致崩溃。
解决方案
方案一:持有WKWebView
保证它生命周期
通过上文的分析,我们知道原因是WKWebView
的释放让WKURLSchemeTask
猝不及防。
那一个简单的思路就是我们让WKWebView
释放不要那么及时。
做法上,不建议让请求拦截与WKWebView
实例耦合,这样会破坏单一职责原则。
好的做法是构建WKWebView
容器池,使用容器复用的方式,不再释放容器,而是循环利用当前的容器,天然就避免了这个崩溃的产生。
方案二:增加 try catch
当然,虽然我们了解了问题的本质,但也可以头疼医头、脚疼医脚的方式解决问题。
我们发现它虽然是崩溃,但它好在是一个被抛出的Exception
,而不是内存指针崩溃。
那虽然基本没人在 iOS 上使用try catch
,但确实能解决这个问题。
@try {
[self.schemeTask didReceiveData:data];
} @catch (NSException *exception) {}
...
@try {
[self.schemeTask didReceiveResponse:response];
completionHandler(NSURLSessionResponseAllow);
} @catch (NSException *exception) {
completionHandler(NSURLSessionResponseCancel);
}
...
@try {
if (error != nil) {
[self.schemeTask didFailWithError:error];
} else {
[self.schemeTask didFinish];
}
} @catch (NSException *exception) {
} @finally {
...
}
最好在所有使用WKURLSchemeTask
的地方都加上try catch
。
当然,增加了try catch
会导致打包后体积略微增大,但对比处理线上崩溃而言,这还是性价比比较高的。
其他方案
网上其实还有其他的解决方案,但或多或少不适合我们全面铺开使用的场景。
避免分片传输
网上很多解决方案都是用的如下方式:
if (self.tasks[urlSchemeTask.description]) {
if (error) {
[urlSchemeTask didFailWithError:error];
} else {
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
}
}
如果当前任务还存在,就整体执行传输操作,不再把data
分片传输。
这虽然可以解决问题,但破坏了系统的设计,遇到大文件传输,会降低体验和劣化性能。
总结
系统提供的拦截方式真的太少了,但凡能提供一个安全调用的方式也好。但也可以从侧面说明,iOS 根本不想我们去做拦截操作,甚至不推荐使用WebView
。
但现实上跨端融合的趋势是不可避免的,拦截也是性能优化的一部分。从上文的评论区可以看到,遇到拦截问题的同学也不少。
还是希望大家能少踩坑 ~
感谢阅读,如果对你有用请点个赞 ❤️