ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WKWebView白屏与POST body丢失:排查与恢复策略

WKWebView白屏与POST body丢失:排查与恢复策略 做 iOS 开发这些年WKWebView 替换 UIWebView 之后确实省了不少心内存占用、渲染性能都上了一个台阶。但省心不等于没坑我这两年被问得最多的两个问题一个是“页面莫名其妙白了怎么回事”另一个是“POST 请求的 body 在 WKWebView 里丢了”。这两个问题看着不相关但排查下来都跟 WKWebView 的多进程架构和请求转发机制有关。这篇文章把我自己的排查思路和最终落地的方案整理出来供遇到同样问题的同学参考。1. “白屏”不一定白屏先把三类误报剔除掉接手白屏问题第一件事不是改代码而是校准“白屏”的定义。因为我在实际工单里遇到的“白屏”有相当一部分根本不是 WebView 内核故障而是页面内容还没渲染出来、或者前端逻辑异常导致视觉上全白。如果一上来就盯着进程崩溃去排查很可能会走进死胡同。1.1 截图检测不可靠要看渲染树很多团队喜欢在测试阶段用截图来判断页面是否白屏。这个办法在 UI 自动化里用得挺多但它有一个明显的坑如果当前 WebView 刚好被系统弹窗、键盘遮挡或者还没渲染到关键帧截图确实可能是白的但这不代表页面加载失败了。还有一类情况是页面内容区域本身背景就是白色正文要等异步接口返回后才有内容你在接口返回前截图当然也是白的。我试下来比较靠谱的做法是“两层判断”第一层看页面加载状态。navigationDelegate的didFinish是否收到页面标题title是否从初始值变成了业务标题URL 是否从about:blank变成了目标地址。第二层注入 JavaScript 读取document.documentElement.scrollHeight、document.body.innerText.length这类指标判断页面骨架是不是真的渲染出来了。只要这两层都通过即使肉眼看起来白屏也不用归入内核故障。截图可以作为辅助记录但不能作为唯一判据。1.2 空 URL 和历史记录隐藏的白屏另一种容易误判的情况是页面没有崩溃、没有加载失败但webView.url拿到的值是空或者历史栈里只有一条about:blank记录。这种情况通常不是 WKWebView 的问题而是业务跳转逻辑把 URL 传错了或者前端路由在初始化时先加载了一次空页面。排查时不要只盯着业务代码里有没有调用loadRequest还要看 request 的 URL 在decidePolicyFor回调里到底是什么。我之前遇到过一个案例业务方在启动时先loadHTMLString一段空白占位 HTML再根据异步配置决定要不要加载真实 URL结果异步配置返回异常页面就一直停在空白占位页上用户看到的也是“白屏”但 WebView 状态完全正常。1.3 JS 注入探测的优缺点有些团队会用evaluateJavaScript去探测document.readyState和页面元素数量这个方法有效但要注意时机和跨域限制。页面里如果嵌了跨域 iframe主框架document可能是好的子框架自己崩了这时候 JS 探测不到子框架的异常只能靠 WebContent 进程级别的兜底。所以我的建议是JS 探测用来做前端侧的页面渲染健康度监控进程崩溃监听用来做内核侧兜底两者互补不要互相替代。2. 进程被杀导致的真白屏崩溃监听和恢复策略排除掉误报之后剩下的白屏里最常见的就是 WKWebView 的 WebContent 进程崩溃。这个问题的表现是App 还活着页面区域突然变白整个 WebView 跟死了一样点哪里都没反应。2.1 为什么 WKWebView 会“单独崩溃”而不影响主 AppWKWebView 从 iOS 8 开始采用多进程架构网页内容运行在独立的 WebContent 进程里和宿主 App 进程是分开的。这么做的好处是网页崩溃不会直接拖垮 App坏处是 WebContent 进程崩了之后WKWebView 显示的区域会直接变成一片白而你的 App 还正常跑着用户看到的就是“页面没了App 还活着”。触发 WebContent 进程崩溃的常见原因我总结下来有这么几类内存占用过高被系统清理这是占比最高的。同一页面里图片太多、WebGL 场景过大、复杂 CSS 动画长时间运行都会让 WebContent 进程内存暴涨。GPU 资源异常比如频繁切换 WebGL 上下文、视频播放和 canvas 同时跑。iOS 系统自身的 bug尤其是重大版本升级后的前两个小版本WKWebView 的稳定性问题会集中暴露。单个页面加载数据量过大比如几十 MB 的 HTML 或超大 JSON 注入。在低端机型和内存紧张场景下这类崩溃概率会明显上升。我这边线上监控数据显示iPhone 8 及以下机型的崩溃占比比当年新旗舰高出三倍以上。2.2 监听 webViewWebContentProcessDidTerminate 的正确时机从 iOS 11 开始监听入口统一在navigationDelegate的webViewWebContentProcessDidTerminate方法里。这个方法名看着长其实就是“WebContent 进程终止了”的意思。这里要提醒一句这个回调不一定会立刻触发。有时候进程被杀到页面白屏之间会有几秒甚至几十秒的间隔。很多人白屏半天了都没收到回调不是监听代码写错了而是系统本身的延迟。所以只靠回调做恢复体验上会有明显卡顿。iOS 9、10 上没有这个代理方法老代码里通常用UIApplicationDidReceiveMemoryWarning通知加webViewDidFinishLoad状态来辅助判断。但如果你的 App 最低支持版本已经在 iOS 11 以上可以直接放弃老逻辑统一走webViewWebContentProcessDidTerminate。另外一个细节这个回调可能在你 App 进入后台、系统回收内存的时候触发这时候即使你做了恢复用户回到前台看到的也可能还是白屏。所以恢复逻辑要同时处理“前台收到回调”和“从后台回到前台发现 WebView 已死”这两种情况。我的做法是在applicationDidBecomeActive里加一个轻量检查如果上次退出后台前 WebView 已经发生了进程终止回前台时直接走重建流程。2.3 恢复白屏时reload 不是万能解遇到进程崩溃后的白屏很多人第一反应是调用webView.reload()。实测下来这个方法有时有效有时无效。原因是崩溃后 WebContent 进程的状态已经不可信了reload可能还是基于一个半死状态的进程在工作表现为加载事件一直不回调或者页面仍然白屏。更稳妥的恢复流程是销毁旧的 WKWebView 实例创建一个新的 WKWebView重新加载原来的 URL。这里面有几个细节记录当前 URL 时要忽略about:blank。如果崩溃时页面还在about:blank恢复时直接加载业务首页。销毁旧 WebView 前先把它从父视图上移除并置nil避免内存残留。新建 WebView 时用同一个WKWebViewConfiguration。注意如果WKWebViewConfiguration里的websiteDataStore是新建的cookie 和缓存会丢建议用默认的WKWebsiteDataStore.default()或者自己持久化的WKWebsiteDataStore实例。恢复加载前手动清掉backForwardList否则用户从恢复后的页面点返回可能回到一个已经死掉的页面。我封装的恢复逻辑大致是func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { let currentURL webView.url let configuration webView.configuration webView.stopLoading() webView.removeFromSuperview() let newWebView WKWebView(frame: webView.frame, configuration: configuration) newWebView.navigationDelegate self newWebView.uiDelegate self // 把 newWebView 添加到原来的父视图并设置约束... self.webView newWebView if let url currentURL, url.absoluteString ! about:blank { newWebView.load(URLRequest(url: url)) } else { // 这里加载你自己的业务首页 } }这段代码的核心思想是不要在一个已经死掉的进程上反复尝试直接换新进程。3. request body 丢失不是 WKWebView“吃掉”了 body是路径上根本没传说完了白屏再来聊 request body 丢失。这个问题的现象更隐蔽页面发了一个 POST 请求服务端收到了但读到 body 是空的。前端和后端各自查了一遍都觉得不是自己的问题最后定位到 WKWebView 上。3.1 复现一次 body 丢失302 重定向是最大“凶手”最容易复现 body 丢失的场景就是 302 重定向。WKWebView 加载一个 POST 请求时如果服务端返回 302 重定向WebKit 会跟随重定向发起新的 GET 请求此时原来的 POST body 不会跟随到重定向请求里。这其实是 HTTP 协议层面的行为不是 WKWebView 独有的 bug。为什么在 WKWebView 里表现得更隐蔽因为 WebView 内部的请求转发链路不经过业务代码你看不到“浏览器帮你发了第二个请求”的过程。你只会看到前端明明用fetchPOST 了 JSON 数据后面某个接口收到的是空 body。更麻烦的是 307/308 这类状态码。理论上它们要求保留原始方法和 body 重定向但 WebKit 在部分版本上的处理并不一致。我遇到过 307 重定向后 body 被吞掉的 case所以不要想当然觉得“协议规定会保留就一定会保留”。3.2 跨进程传输对 POST body 的限制除了重定向还有一个跟 WKWebView 架构相关的点跨进程传输。WKWebView 的请求不是直接把NSURLRequest扔给网络层而是先经过 WebContent 进程再做进程间通信IPC传输。这套机制对请求体做序列化时在某些路径上会丢弃 body。尤其是当NSURLRequest的httpBodyStream是NSInputStream类型时WKWebView 基本不会帮你做流式重放。所以在 iOS 的 WKWebView 场景下尽量避免构造带NSInputStream的 POST 请求这是很现实的经验。另外如果你用了NSURLProtocol去拦截 WKWebView 的请求会发现在拦截回调里拿到的request.httpBody经常是nil。这不是你代码写错了而是 WKWebView 从 iOS 8 开始就不走NSURLProtocol的完整链路官方文档里也明确标注了“在 WKWebView 上不推荐使用 NSURLProtocol”。任务管理界的同事遇到问题就喜欢往 NSURLProtocol 上靠我只能说这条路越走越窄。3.3 可落地的几种修复方案针对 body 丢失我实践过几种方案各有适用场景。方案一改服务端把关键参数放到 URL 或 header 里。比如原本 POST JSON 里的用户 ID、token 这类不敏感数据可以放到自定义 header 里body 丢了对业务影响也不大。这个方案成本最低但只能解决“参数丢失”的表面问题不能解决所有 POST 场景。方案二前端不再用fetch发 POST改成动态创建form提交。表单提交的请求在 WKWebView 里的兼容性明显更好body 丢失的概率大幅下降。缺点是前端要配合改造而且如果服务端只接受 JSON需要额外转换。方案三在decidePolicyFor里拦截重定向请求手动把原始 body 拼回去。这个方案适合自己完全控制前后端的情况。大致思路是页面发起 POST 请求时先把 body 存到一个本地缓存对象里key 可以是 URL 的 hash在decidePolicyFor navigationAction里检测到重定向把缓存里的 body 重新构造成新的URLRequest用新的 request 发起加载。这个方案要处理的边界情况不少比如重定向可能发生多次body 缓存要设置过期时间还要考虑多 WebView 实例时 key 冲突。但好处是不用动前端一个插件逻辑就能解决问题。方案四使用WKUserScript注入一段 JavaScript把原始请求的 body 保存到window.name或 localStorage后端需要时再通过 JS 取回来。这个方案实际用起来比较绕但当前端完全不能改、服务端也不能改时是最后一个兜底。注意不要为了取回 body 去用私有 API比如WKBrowsingContextController那套注册 scheme 的办法。私有 API 一方面有审核风险另一方面每次 iOS 版本升级都可能失效线上维护成本高。4. 监控和线上定位让白屏和 body 丢失不再是“玄学”白屏和 body 丢失这类问题最烦人的地方是“偶现”。本地测不出来一上线上就频繁报。所以监控体系比修复代码更重要。没有监控你连问题发生范围都说不清楚更谈不上验证修复效果。4.1 需要采集哪些日志我建议至少采集这么几类数据场景关注点推荐记录项白屏WebContent 进程终止时间、webView.url、是否前台、机型、系统版本、内存水位白屏页面加载超时didFinish 是否收到、didFail 错误码、首字节时间body 丢失POST 请求重定向业务标识、原始 URL、重定向目标 URL、httpMethod、header 字段body 丢失请求体为空业务标识、URL、httpMethod、Content-Type、Content-Length通用内存状态App 内存占用、WebContent 内存占用如果能拿到日志里必须带上用户标识或设备标识否则出了线上问题你连回放都没有。我一般用 sessionId 加 requestId 串起一次完整页面生命周期。4.2 关键指标设定FCP、进程状态、请求成功率除了日志还要有指标。粗粒度指标适合做告警细粒度日志适合做排查。白屏率计算公式可以定义为“收到 didFinish 但页面渲染关键指标不正常的次数 / 总加载次数”。这里的关键指标建议用前端埋点上报比如firstContentfulPaint时间、domContentLoadedEventEnd时间。WebContent 崩溃率webViewWebContentProcessDidTerminate触发次数 / WebView 创建次数。这个指标可以按系统版本和机型维度拆分方便判断是系统问题还是业务问题。POST 请求成功率加一个自定义 header 标记请求是否携带 body后端收到后回传状态前端再上报。这样即使 body 丢了也能通过“发送端有 body接收端没 body”这个差值定位到 WebView。4.3 一个可参考的恢复流程我把线上恢复流程整理成了固定步骤每次触发白屏告警就按这个顺序走确认是不是误报。先看页面加载日志didFinish收到、title 有值、JS 渲染指标正常就不用继续查。确认是不是 WebContent 进程崩溃。看webViewWebContentProcessDidTerminate回调有没有上报以及崩溃前后 WebView 是否收到新的加载请求。确认崩溃前内存水位。如果在崩溃前 App 或 WebContent 进程内存已处在高危区间优先考虑业务侧减负比如分批加载图片、关掉不必要的 canvas 定时器。确认崩溃是否与特定页面有关。如果某个页面崩溃率明显偏高直接让前端拉取页面性能数据重点看资源总大小和 JS 执行耗时。确认修复是否生效。发布新版本后对比同一指标的前后数据观察至少一个完整版本周期。这个流程看着简单但能帮你避免“拍脑袋改一行代码碰运气”的尴尬。5. 我的一些补充经验与建议最后补充一些散点经验单独拿出来说可能不够撑一个章节但实战中踩到过。5.1 内存、缓存和 cookie 策略WKWebView 的WKWebsiteDataStore默认是持久化的也就是说 cookie、localStorage 这些数据 App 杀掉重开会保留。这个特性平时是好事但在白屏恢复和 body 丢失排查时容易出问题。比如 WebView 重建后 cookie 没同步导致新页面请求没带上登录态可能被服务端重定向到登录页登录页如果也是 WebView 加载就又可能触发重定向 body 丢失。所以我的习惯是业务里所有 WebView 使用同一个WKWebsiteDataStore实例不要每次新建 WebView 时都重新初始化配置避免 cookie 和缓存被重置。白屏恢复时也一样重建 WebView 前先确认websiteDataStore没有被替换成新的。前面那段恢复代码里我用webView.configuration直接拿原配置就是为了避免这个问题。5.2 兼容性备忘iOS 12 以下对webViewWebContentProcessDidTerminate的支持不完整如果最低支持版本较低建议保留老版本通知监听。iOS 14 之后部分 WebGL 页面的内存回收机制有变化白屏问题表现会和旧版本不同不要用旧的经验套新版本。分屏、画中画、后台转前台这些场景也会触发 WebKit 的进程回收线上监控统计时要把这些情况过滤掉否则白屏率会被非真实故障拉高。5.3 复现 body 丢失的一个小技巧如果要在本地复现 body 丢失最快的方法不是单测而是用 Charles 或 Wireshark 抓包。我在 Charles 里配置一个简单的映射规则把某个 POST 请求重定向到另一个接口然后看抓包记录里第二次请求的 body 是否为空。只要复现一次就能确认是不是 302 重定向导致的。如果是前端fetch发起的请求还可以在 WKWebView 里打开远程调试直接用 Safari 的开发者工具查看网络面板能看到 WebKit 实际发出的请求头和 body比在 App 层猜要直观得多。白屏和 body 丢失这两个问题说到底是 WKWebView 把一个网页浏览器塞进 App 进程后带来的复杂度。理解它的多进程架构和请求转发机制比背一百个“解决方案”更有用。排查问题的时候先想清楚链路再去看代码往往能少走很多弯路。
返回列表