「拒绝踩坑」拦截 WKWebView 请求后出现崩溃的原因

上文回顾:「拒绝踩坑」唯一一种拦截 WKWebView 资源请求的方式

背景

在上文,笔者介绍了如何拦截资源请求。但文中有些疏漏之处,也有读者在评论区里提到了,会遇到This task has already been stopped这样的崩溃,导致崩溃率变高。但我们其实已经通过取消请求机制处理了大部分,常规使用上,这个崩溃就没再遇到,导致忽视了。

这次也是通过线上崩溃分析,发现还是有类似的崩溃出现。通过堆栈分析,锁定到如下代码:

本质原因

闲话少说,先来聊一下出现这个崩溃的本质原因是什么:

究其根本,是因为WKWebView和我们自身的 App 所属2个不同的进程,这里的WKURLSchemeTask本质上是进程间通信的句柄。

进程间通信是一个耗时操作,且不能保证连接的双方生命周期一致。这不是线程通信,我们可以通过引用计数来持有对象。

这就导致,在传输前或者传输中,经过WKURLSchemeTask传输时,WKWebView所在进程被释放,就会导致出现This task has already been stopped异常抛出崩溃。

问题分析

传输前

如果是WKURLSchemeTask传输前,我们在上文中已经做了防护:

通过系统提供的有限的2个代理方法,我们可以拿到停止的时机:

Objectivec 复制代码
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
    [task cancel]; // 这里执行任务停止动作
}

传输中(本次遇到)

在传输前拦截已经可以杜绝大部分崩溃情况, 但有一种情况会导致在传输中发生崩溃:

Objectivec 复制代码
- (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,但确实能解决这个问题。

Objectivec 复制代码
    @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会导致打包后体积略微增大,但对比处理线上崩溃而言,这还是性价比比较高的。

其他方案

网上其实还有其他的解决方案,但或多或少不适合我们全面铺开使用的场景。

避免分片传输

网上很多解决方案都是用的如下方式:

Objectivec 复制代码
if (self.tasks[urlSchemeTask.description]) {
      if (error) {
          [urlSchemeTask didFailWithError:error];
      } else {
          [urlSchemeTask didReceiveResponse:response];
          [urlSchemeTask didReceiveData:data];
          [urlSchemeTask didFinish];
      }
 }

如果当前任务还存在,就整体执行传输操作,不再把data分片传输。

这虽然可以解决问题,但破坏了系统的设计,遇到大文件传输,会降低体验和劣化性能。

总结

系统提供的拦截方式真的太少了,但凡能提供一个安全调用的方式也好。但也可以从侧面说明,iOS 根本不想我们去做拦截操作,甚至不推荐使用WebView

但现实上跨端融合的趋势是不可避免的,拦截也是性能优化的一部分。从上文的评论区可以看到,遇到拦截问题的同学也不少。

还是希望大家能少踩坑 ~


感谢阅读,如果对你有用请点个赞 ❤️

相关推荐
Carry34515 小时前
不清楚的 .gitignore
前端·git
张鑫旭15 小时前
AI时代2025年下半年学的这些Web前端特性有没有用?
前端·ai编程
pinkQQx15 小时前
H5唤醒APP技术方案入门级介绍
前端
Lefan16 小时前
UniApp 隐私合规神器!一键搞定应用市场审核难题 - lf-auth 隐私合规助手
前端
Null15516 小时前
浏览器唤起桌面端应用(进阶篇)
前端·浏览器
Jing_Rainbow16 小时前
【Vue-2/Lesson62(2025-12-10)】模块化与 Node.js HTTP 服务器开发详解🧩
前端·vue.js·node.js
风度前端16 小时前
用了都说好的 uniapp 路由框架
前端
冴羽16 小时前
2026 年 Web 前端开发的 8 个趋势!
前端·javascript·vue.js
码银16 小时前
ruoyi的前端(vue)新增的时候给字典设置默认值 但不能正常
前端
凌览17 小时前
别再死磕 Nginx!http-proxy-middleware 低配置起飞
前端·后端