
1. 问题现象与影响范围WKWebView 白屏问题、request body 丢失问题这两个词放在一起基本上就是 iOS 端 Hybrid 应用开发者绕不过去的两道坎。我在做 App 内嵌 H5 业务改造的两年里这两类问题交替出现过,每回线上反馈进来都是带着用户截图和一串页面打不开表单提交没反应的吐槽。今天把它们放在一起聊是因为它们表面上是两个独立问题底层却都指向同一个根源——WKWebView 的进程模型与请求转发机制。简单说一下我实际遇到的典型场景。第一个场景是白屏。用户从 App 首页点进某个活动页页面在 Wi-Fi 弱网环境下加载到一半突然退出到后台再切回来整个 WebView 区域变成一片纯白底部的内容全部消失。杀掉 App 重进又好了。这个问题在低内存设备iPhone 7 及以下上尤其频繁系统日志里能看到WebContent process crashed之类的痕迹。第二个场景是 request body 丢失。业务方做了一个 H5 版的客户回访表单用户花十分钟填完一大段文字点提交前端把数据用 POST 请求发到后端结果服务端收到的 body 是空的。前端查 Network 面板明明 payload 里有完整 JSON。这个问题的诡异之处在于它不是必现的只在特定条件下触发比如用户在页面停留时间较长、内存紧张、或者 WebView 被系统回收又恢复之后。这两个问题单独看都是老掉牙的话题网上方案一搜一大把。但真正把它们放在一起系统性排查的时候你会发现很多网上的方案是互相矛盾的有的甚至会在解决一个问题的同时把另一个问题引入。我打算从底层原理到实际修复把这套完整的排查思路和最终落地方案整理出来。先说结论白屏问题的核心是 WebContent 进程被杀而 request body 丢失的核心是 WKWebView 在跨进程通信时对 HTTP body 的序列化限制。理解了这两点后面所有修复方案都是在跟系统机制做博弈。2. 白屏问题的根因分析2.1 WebContent 进程崩溃与系统回收机制WKWebView 从 iOS 8 开始就是多进程架构App 进程和 WebContent 渲染进程分开跑。这个设计的好处是网页卡死、崩溃不会直接带走 App 主进程坏处是 WebContent 进程随时可能被系统杀掉而且杀的时候不会提前打招呼。WebContent 进程被杀通常分成两类。第一类是真正的崩溃。页面 JS 执行了非法操作、加载了异常资源、或者 WebContent 内部 bug进程直接崩溃退出iOS 系统的崩溃日志里能看到com.apple.WebKit.WebContent的异常退出记录。这种崩溃在低版本系统上更容易触发尤其是 iOS 11 到 iOS 13 那一批 WebKit 的 bug比如音频播放、视频播放、Canvas 大规模渲染都可能导致 WebContent 崩溃。第二类是 Jetsam 内存回收。iOS 的内存管理机制是谁内存占用大就优先杀谁WebContent 进程是系统内存压力的主要清理对象。一个复杂的 H5 页面尤其是有大量图片、长列表、高频 DOM 操作的页面WebContent 进程的内存很容易冲到 300MB 以上。这时系统如果判定内存紧张会优先杀掉 WebContent 进程——注意它宁可杀 WebContent 也不杀你的 App 主进程因为主进程在用户感知里活着WebContent 进程挂了用户看到的只是白屏系统觉得这个代价最小。但这里有一个关键细节WebContent 进程被杀后WKWebView 并没有重新加载页面。App 进程里 WKWebView 对象还活着backForwardList 也还在但 webView 的渲染内容已经没了。如果没有做任何处理你看到的就直接是一块白屏。2.2 WKWebView 与 App 进程的通信桥接为了讲清楚白屏的恢复机制这里需要引入 WKWebView 的进程通信模型。WKWebView 的 UI 层在主进程App 进程渲染层在 WebContent 进程两者通过 XPC 通信。你在 JS 里执行window.webkit.messageHandlers.xxx.postMessage()这个调用会从 WebContent 进程通过 XPC 传到 App 进程反过来App 进程调用webView.evaluateJavaScript()内容是传到 WebContent 进程去执行的。问题就出在这条链路上。这条通信链路要求两边进程状态都正常任何一边挂了通信就断了。而系统并没有提供一个WebContent 进程挂了自动拉起并重载页面的机制——它只在主进程监听相关状态变化把异常抛给实现了WKNavigationDelegate的对象然后就不管了。另外WKWebView 在 iOS 13 之前没有公开的 API 告诉你 WebContent 进程的状态。常见的检测手段要么依赖webViewWebContentProcessDidTerminate:,要么在 iOS 14 及以后用WKUIDelegate的webViewWebContentProcessDidTerminate:方法苹果在 iOS 14 给这个方法加了新的调用时机要么通过加载一个空的 data URL 来观察是否触发导航回调。我早期踩过的一个坑是只处理了webViewWebContentProcessDidTerminate:回调就以为万事大吉结果线上还是有人反馈白屏。后来打日志才发现某些情况下 WebContent 进程被杀后这个方法根本没有被调用——系统逻辑是如果 WKWebView 本身已经不在窗口层级上比如被 App 切到后台它不会立刻回调等切回前台时页面已经白了但回调却没有触发。3. 白屏问题的检测与自动恢复方案3.1 白屏检测的三个信号源解决白屏问题的第一步是准确地检测到真的白屏了。这里我整理出三个信号源实际项目中需要组合使用单独依赖任何一个都不够可靠。第一个信号源是webViewWebContentProcessDidTerminate:回调。这是最直接的信号WebContent 进程挂了系统会调用它。但正如上面说的回调有延迟和遗漏的场景不能作为唯一依据。第二个信号源是WKNavigationDelegate的导航回调。如果 WebContent 进程挂了导航相关的回调会停在一个中间状态——比如didStartProvisionalNavigation:之后一直没有didFinish:。可以通过一个超时计时器比如超过 10 秒还没有 finish就触发一次白屏检测逻辑。第三个信号源是主动探测。用evaluateJavaScript执行一个空代码比如window.location.href正常情况下这段代码会在几百毫秒内回调如果 WebContent 进程挂了回调永远不会回来。可以封装一个带超时检测的探活函数每隔一段时间主动探测一次。我用主动探测比较多原因是在 iOS 14 之后webViewWebContentProcessDidTerminate:的调用时机变得非常不稳定而主动探测不受系统回调时机的限制。探测逻辑的代码大致是这样- (void)checkWebViewAlive { __weak typeof(self) weakSelf self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong typeof(self) strongSelf weakSelf; if (!strongSelf) return; dispatch_semaphore_t semaphore dispatch_semaphore_create(0); __block BOOL isAlive NO; [strongSelf.webView evaluateJavaScript:1 1 completionHandler:^(id result, NSError *error) { if (error nil) { isAlive YES; } dispatch_semaphore_signal(semaphore); }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!isAlive) { // WebContent 进程可能已挂触发白屏恢复逻辑 [strongSelf handleWebViewWhiteScreen]; } }); }); }这个方案的关键在于超时判定——evaluateJavaScript的回调在进程挂了的情况下永远不会触发所以用一个延迟 2 秒的检查来判断回调是否已经回来。我在 iOS 11 到 iOS 16 的机型上都验证过误判率很低因为正常情况下的 JS 执行回调不会超过 1 秒。3.2 页面恢复与状态保持策略检测到白屏之后恢复逻辑的处理直接决定用户体验。最简单的方案是直接reload但这里有一个问题如果页面有未提交的表单数据或者用户已经滚动到某个位置reload 之后这些状态全丢了用户会非常愤怒。我采用的是一套分级的恢复策略。第一步先尝试在 WebContent 进程重新启动后自动恢复页面。iOS 系统在 WebContent 进程崩溃后会自动重建进程但不会自动恢复页面内容。我的做法是在webViewWebContentProcessDidTerminate:回调里先拿到当前页面的 URL然后调用webView.reload()这样至少保证页面能回来。第二步如果页面有状态保存需求通过WKWebView的interactionState属性iOS 11 之后支持来保存页面滚动位置等状态。设置方法是webView.interactionState (scrollOffset)恢复时再取出来。第三步对于 H5 页面内容本身的状态通过 URL 参数传递。在 reload 的 URL 上追加一个fromTerminated1的参数H5 端检测到之后会主动从缓存或存储中恢复表单数据等状态。这一步需要 H5 侧配合但实际操作下来效果最好比 App 端强行注入 JS 去恢复状态要稳妥得多。还有一个细节值得注意reload 之前要判断页面当前是否处于前台。如果用户已经切到其他页面甚至切到后台白屏恢复的优先级应该降低等用户回到页面时再执行恢复逻辑否则在后台执行 reload 会白白浪费流量还可能触发一些业务埋点的重复上报。3.3 白屏预防机制的工程化实践除了被动检测和恢复主动预防也很重要。实战中发现优化 H5 页面的内存占用能显著降低 WebContent 进程被系统回收的概率。这里有几个可落地的措施。第一个是控制 WebView 的复用。不要在一个页面里无限创建新的 WKWebView用完即销毁。多个 WKWebView 同时存在时内存是叠加的系统杀 WebContent 的概率会成倍增长。第二个是配置WKWebViewConfiguration时把websiteDataStore设置为非持久化的。如果业务不需要 localStorage 跨 App 启动持久保存用WKWebsiteDataStore.nonPersistentDataStore()可以少一部分磁盘和内存开销。但要注意这个设置在 iOS 9 之后有坑非持久化 store 的 JS 执行上下文在某些版本上有 bug需要在测试机上验证你要支持的 iOS 版本。第三个是及时清理 WKWebView 的 backForwardList。长列表浏览场景下backForwardList 会累积大量的历史快照每个快照都包含页面内容占内存严重。我的做法是在导航完成时判断如果 backForwardList 超过 5 项就执行一次removeAllItems但这里需要权衡——如果业务需要支持返回上一页就不能清理得太频繁。第四个是给 WKWebView 设置合理的 frame 和透明背景。不要用复杂的 layer 动画频繁操作 webView 所在的父视图某些系统版本上对 WKWebView 做 transform 动画会导致渲染异常甚至崩溃。4. request body 丢失问题的来龙去脉4.1 WKWebView 对 POST 请求的特殊处理说完白屏来看第二个问题——request body 丢失。要理解这个问题先要搞明白 WKWebView 里一个不太为人知的行为WKWebView 在跨进程处理网络请求时对 HTTP body 的处理和 UIWebView 完全不同。UIWebView 时代网络请求是直接基于 App 进程的 NSURLProtocol 体系请求的 body 可以完整保留WKWebView 时代WebContent 进程和 App 进程隔离请求数据要跨进程传输为了效率WebKit 对请求做了一个瘦身处理。实际表现是当你通过WKNavigationDelegate的decidePolicyForNavigationAction拿到NSURLRequest时如果是 POST 请求httpBody很可能是 nil。这不是 bug而是 WebKit 的设计决定——它默认通过httpBodyStream来传输 body 数据而httpBodyStream在跨进程传递时会被消费掉你拿到手的时候 stream 已经被读过一遍没法再读了。这是一个非常隐蔽的问题。前端页面用XMLHttpRequest或fetch发 POST 请求数据在 WebContent 进程内部是正常的网络层也能正常发出。但如果有人想在 App 侧拦截、观察、修改这个 POST 请求就会遇到httpBody nil的尴尬局面。对于大多数普通业务来说这个行为不影响页面本身的请求——因为请求是 WebContent 进程自己发的body 数据在进程内完整存在。问题出在那些App 侧试图拦截请求做二次处理的场景。哪些场景会踩到这个坑App 侧注入统一的请求头比如给所有请求加上业务鉴权字段。如果你的实现方式是修改NSURLRequest然后重新发起那 POST body 就会在重发时丢失。用自定义NSURLProtocol拦截 WKWebView 的请求做全局处理。这个在 iOS 11 之后已经基本废掉了——WKWebView 默认不走 App 进程的 NSURLProtocol注册了也没用。用WKNavigationDelegate做统一的请求参数补充。在decidePolicyForNavigationAction里拿到 request因为是 GET 就把参数拼在 URL 上是 POST 就想读 body 再补充字段结果发现 body 是 nil。还有一个我遇到的具体场景为了统计 H5 的接口耗时我在 App 侧通过decidePolicyForNavigationAction观察所有导航请求想从 POST body 里解析出业务参数来辅助定位问题。结果 body 拿不到导致定位问题只能靠猜非常被动。4.2 body 丢失与页面白屏的关联场景这两个问题还会叠加出现而且叠加之后影响特别严重。我的一个真实案例是用户在 App 内嵌的 H5 页面填写了一个很长的问卷填到一半切到微信回了个消息再切回 App页面白屏了。用户被迫重新加载填写的内容全部丢失。但更麻烦的是这期间如果用户恰好点了提交前端请求已经发出去了请求体大概率在某个环节丢了或者后端收到了不完整的 payload。这类场景在低内存设备上特别常见。iOS 系统杀 WebContent 进程的时间点往往在用户切后台的几秒钟内。如果此时页面正在断点续传一个 POST 请求比如上传日志、提交表单请求的状态管理就全乱了。解决这类叠加问题的核心思路是白屏检测与恢复逻辑要尽早介入同时 App 侧和 H5 侧要建立一套请求状态同步机制。H5 在提交数据前先把 payload 保存在 localStorage 里提交成功后再删除。App 侧在检测到 WebContent 进程终止时通过 URL 参数通知 H5刚刚发生过白屏崩溃H5 读取 localStorage 中未完成的请求在页面恢复后自动重试。这套方案实现了之后至少让我负责的线上事故单从每周十几条降到了每周两三条。5. request body 丢失的完整解决方案5.1 方案 A使用 WKWebView 的 navigationAction.request 解决方案先看最常见的decidePolicyForNavigationAction拦截场景。- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSURLRequest *request navigationAction.request; if ([request.HTTPMethod isEqualToString:POST]) { // 这里 request.HTTPBody 很可能是 nil NSData *body request.HTTPBody; if (body nil request.HTTPBodyStream ! nil) { // 需要手动读取 stream NSInputStream *stream request.HTTPBodyStream; [stream open]; NSMutableData *data [NSMutableData data]; uint8_t buffer[1024]; NSInteger len 0; while ((len [stream read:buffer maxLength:sizeof(buffer)]) 0) { [data appendBytes:buffer length:len]; } [stream close]; body data; } // 业务处理 NSLog(POST body: %, [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]); } decisionHandler(WKNavigationActionPolicyAllow); }这个方案在同一个 runloop 周期内直接读取HTTPBodyStream通常能拿到完整的 body。但缺点是 stream 只能读一次如果你把这个 request 重新赋值给一个新的导航请求stream 已经消费过了请求体依然会丢。所以这个方案的适用场景是只读不改。如果你想在拦截后改 body 再发出去这个方案解决不了。5.2 方案 B通过自定义 NSURLProtocol 重新注册拦截仅适用于 iOS 11 以下或者特定配置很多人不知道WKWebView 其实支持通过私有 API 开启 NSURLProtocol 的注册让 WKWebView 的请求走App 进程的 NSURLProtocol 体系。但这个方案有两个前提iOS 11 之前的系统版本可以正常工作iOS 11 之后的系统版本WebKit 的请求走的是他自己的网络栈NSURLProtocol 根本不会被调用。为了兼容低版本系统可以在使用时做一个判断Class cls NSClassFromString(WKBrowsingContextController); // 通过私有 API 注册 scheme handler SEL sel NSSelectorFromString(registerSchemeForCustomProtocol:);需要说明的是这个方案非常脆弱。一方面它依赖系统私有 APIApple 审核存在风险另一方面iOS 11 之后注册了也只能拦截部分请求类型POST body 依然可能被 WebKit 吃掉一部分。我一般不推荐用了除非你的用户群体里有大量 iOS 10 及以下的设备那可以做一个防御性的兼容处理。5.3 方案 C在 WKWebView 初始化时注入脚本捕获请求体推荐方案既然系统层面对 body 的跨进程传输约束很多那换个思路直接在 WebContent 进程内部就拿原始请求数据。通过WKUserScript在页面加载前注入一段 JavaScript拦截 XHR 和 fetch 的请求。XHR 可以用override open/send的方式fetch 可以包一层。拦截到的 body 数据通过window.webkit.messageHandlers传到 App 进程。下面是 XHR 拦截的实际代码(function() { var originalOpen XMLHttpRequest.prototype.open; var originalSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open function(method, url) { this.__method method; this.__url url; originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send function(body) { this.__body typeof body string ? body : ; if (this.__body) { try { window.webkit.messageHandlers.intercept.postMessage({ type: xhr, method: this.__method, url: this.__url, body: this.__body }); } catch(e) {} } originalSend.apply(this, arguments); }; })();fetch 拦截类似(function() { var originalFetch window.fetch; window.fetch function(input, init) { var url typeof input string ? input : input.url; var method (init init.method) || GET; var body (init init.body) || ; if (method.toUpperCase() POST body typeof body string) { try { window.webkit.messageHandlers.intercept.postMessage({ type: fetch, method: method, url: url, body: body }); } catch(e) {} } return originalFetch.apply(this, arguments); }; })();注入脚本之后App 侧注册一个 message handlerWKUserContentController *userContentController [[WKUserContentController alloc] init]; [userContentController addScriptMessageHandler:self name:intercept]; WKWebViewConfiguration *config [[WKWebViewConfiguration alloc] init]; config.userContentController userContentController; WKUserScript *script [[WKUserScript alloc] initWithSource:integrationScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [userContentController addUserScript:script];这个方案的优点是直接拿到了原始请求体无论页面用的是 XHR 还是 fetch只要有 POST 请求就能监控到。缺点是需要 H5 侧配合接入注入脚本而且对页面的 JS 性能有一点点影响——好在拦截逻辑足够轻量实测对页面初始化性能的影响可以忽略不计。方案 C 是我目前在生产环境中主力使用的方案稳定性最好且不依赖私有 API适配所有 iOS 版本。5.4 方案 D通过 WKWebView 的 URL 和请求头来恢复 request body最后一个思路针对请求已经发出但 body 在服务器端没收到的情况。这种场景下能做的是补救和诊断。在 App 侧检测到导航失败或是网络请求异常时把当前页面的 URL、请求方法、WebContent 进程的崩溃日志、以及从注入脚本里缓存的最近几次 POST body 一起打包上报到服务端。服务端通过这些数据可以判断是前端请求本身就没发出去还是 App 侧拦截逻辑干扰了还是 WebContent 进程在请求过程中被杀导致 body 传输中断这套诊断链路建立之后排查效率提升非常明显。以前靠用户截图和口头描述定位问题现在看一眼后台的请求明细就知道原因。6. 常见问题排查与复盘6.1 必现白屏的排查流程如果你的白屏问题是必现的那大概率不是 Jetsam 回收而是某个特定操作触发了 WebContent 崩溃。排查路径如下。第一步拿崩溃日志。用 Xcode 打开 Window - Devices and Simulators查看设备日志里的com.apple.WebKit.WebContent相关条目定位崩溃的 stack frame。如果崩溃发生在 WebKit 内部基本确定是系统 bug如果崩溃发生在 JS 调用栈里可以通过堆栈定位到具体是哪个 JS 函数。第二步在 WKWebView 的 controller 里实现webViewWebContentProcessDidTerminate:打日志确认崩溃发生的时机。第三步逐项检查 H5 页面里是否有以下高风险操作WebGL 大规模渲染、Canvas 的离屏绘制、音视频播放这个在 iOS 12 之前崩溃率奇高、window.open打开新页面、alert弹窗阻塞主线程等。第四步用 Safari 的 Web Inspector 远程调试 H5 页面在操作步骤里逐步触发配合崩溃日志一步步缩小范围。6.2 POST body 获取为 nil 的排查思路如果注入脚本已经生效但某些请求仍然拿不到 body按下面顺序排查。首先确认请求是否真的走了注入脚本。因为注入脚本只对XMLHttpRequest和fetch两种方式生效如果页面用的是navigator.sendBeacon()或者form表单提交拦截不到。其次检查脚本注入时机。如果页面在DOMContentLoaded之后动态创建的 XHR 对象而你的脚本在atDocumentStart注入混乱的上下文不会影响拦截——因为你是 override 了原型方法任何时刻创建的对象都会走被修改过的方法。但如果页面用了某种方式隔离 JS 上下文比如 iframe 里执行mainFrameOnly 的设置就会让 iframe 的请求漏掉。最后检查 body 大小。如果 POST body 超过 1MBpostMessage传大字符串可能内存暴涨甚至卡死需要做数据压缩或者分段传输。6.3 白屏恢复后 JS 注入失效的坑这是我最想分享的一个坑。白屏恢复后直接reload我在测试时常发现恢复后的页面里注入的WKUserScript没有生效。排查发现reload之后WKUserScript确实会重新注入但如果注入的脚本里有异步逻辑比如setTimeout加载一个外部 JS再在外部 JS 里调用webkit.messageHandlers很可能消息处理器已经注册了但外部的注入脚本被页面 CSPContent Security Policy拦截。所以我的建议是白屏恢复后不要只是 reload要主动检查一下 WKWebView 的世界状态。如果页面用到了WKUserScript做桥接reload 后要用evaluateJavaScript主动探测一下桥对象是否存在typeof window.webkit.messageHandlers.xxx ! undefined。如果探测不到需要重新走一遍桥接注册流程。在 iOS 15 之后的系统里WKUserScript的注入机制没有大的变化但WKWebViewConfiguration的创建时机影响很大——必须在 WKWebView 实例初始化之前配置好。如果你的WKUserScript是懒加载创建的第一次进页面没问题但白屏恢复时如果代码走到了复用 WebView 但重新配置的逻辑就很容易出问题。6.4 内存优化的一线实测数据这里贴一组我实测的数据给大家一个量级概念。同一台 iPhone 8iOS 15.7加载同一个包含长列表和高清图片的 H5 页面配置方式WebContent 峰值内存白屏复现概率默认配置创建后不管理 backForwardList约 420MB高开启 nonPersistentDataStore定期清理 backForwardList约 280MB中等上述基础上 降级图片分辨率 懒加载长列表约 190MB低内存从 420MB 降到 190MB白屏概率从后台切换必现降到了偶尔出现。优化手段里H5 降内存的效果远大于 App 侧的 WebView 配置优化。所以如果你有白屏问题先让前端看一下页面的内存画像可能比你在 App 侧调配置效率高得多。7. 一套可落地的综合处理模板最后分享一个我在项目里沉淀下来的综合处理模板把白屏检测、恢复、body 拦截、请求重试这几件事做成一个统一的控制器。它需要你替换成你自己的业务逻辑核心思路可以直接参考。interface WebViewManager : NSObject WKNavigationDelegate, WKScriptMessageHandler property (nonatomic, strong) WKWebView *webView; property (nonatomic, assign) BOOL isRecovering; property (nonatomic, strong) NSMutableDictionary *pendingBodies; end implementation WebViewManager #pragma mark - 初始化 - (WKWebView *)createWebViewWithFrame:(CGRect)frame { WKWebViewConfiguration *config [[WKWebViewConfiguration alloc] init]; // 非持久化数据存储降低系统回收 WebContent 进程的概率 config.websiteDataStore [WKWebsiteDataStore nonPersistentDataStore]; // 缓存注入脚本 WKUserContentController *userController [[WKUserContentController alloc] init]; // 注入 HTTP body 捕获脚本 NSString *bodyCaptureScript [self bodyCaptureScript]; WKUserScript *script [[WKUserScript alloc] initWithSource:bodyCaptureScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; [userController addUserScript:script]; // 注册消息处理器 [userController addScriptMessageHandler:self name:httpBody]; config.userContentController userController; _webView [[WKWebView alloc] initWithFrame:frame configuration:config]; _webView.navigationDelegate self; return _webView; } #pragma mark - Navigation Delegate - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { // 页面加载完成后主动探测 WebContent 进程是否存活 __weak typeof(self) weakSelf self; [webView evaluateJavaScript:window.location.href completionHandler:^(id result, NSError *error) { if (error) { [weakSelf handleWhiteScreenForWebView:webView]; } }]; } - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { // WebContent 进程终止白屏处理入口 [self handleWhiteScreenForWebView:webView]; } #pragma mark - 白屏处理 - (void)handleWhiteScreenForWebView:(WKWebView *)webView { if (self.isRecovering) return; self.isRecovering YES; NSString *currentURL webView.URL.absoluteString; if (currentURL.length 0) { self.isRecovering NO; return; } // 保存当前交互状态 UIScrollView *scrollView webView.scrollView; CGPoint offset scrollView.contentOffset; webView.interactionState [NSValue valueWithCGPoint:offset]; // 恢复到前台时执行 reload避免后台白屏恢复造成额外消耗 __weak typeof(self) weakSelf self; dispatch_async(dispatch_get_main_queue(), ^{ if ([UIApplication sharedApplication].applicationState UIApplicationStateActive) { NSURLRequest *request [NSURLRequest requestWithURL:webView.URL]; [webView loadRequest:request]; } }); // 重置恢复标记带延迟避免频繁触发 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ weakSelf.isRecovering NO; }); } #pragma mark - Script Message Handler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:httpBody]) { NSDictionary *body message.body; // 存储 body用于请求重试或数据上报 [self.pendingBodies setObject:body forKey:[NSDate date]]; // 业务处理... } } #pragma mark - 注入脚本 - (NSString *)bodyCaptureScript { return (function() { ... })();; // 填入上述 XHR fetch 拦截脚本 } end这段代码的核心思路是把白屏检测、WebContent 进程终止探测、状态保存与恢复、HTTP body 拦截统一到同一个 manager 里避免多个 delegate 互相争抢。实际使用中你可以根据业务做裁剪但整体结构建议保留。8. 工程实践中的几点心得第一个心得不要迷信单一方案多信号源组合判断才是正道。白屏检测如果只依赖webViewWebContentProcessDidTerminate:会漏掉一些系统不回调的场景只依赖主动探测又会有 1 到 2 秒的检测延迟。把导航超时、JS 探活、系统回调三个信号源结合起来白屏恢复的准确率能做到 99% 以上漏报率和误报率都降到可接受范围。第二个心得HTTP body 丢失问题的根本解法在 H5 侧不在 App 侧。App 侧的注入脚本只是观察者真正要保证请求的可靠性H5 需要在发送请求前做好数据缓存和重试机制。我们最后的落地方案里重要表单提交前先把 payload 写到 localStorage提交成功后清理App 侧收到 WebContent 进程终止通知时带上参数white_screen_reload1H5 检测到这个参数后读取 localStorage 里的未完成请求并自动重发。这套联动机制上线后问卷类业务的提交失败率从 3% 降到了 0.2% 以下。第三个心得低内存设备上的 WKWebView 优化要系统化不能只看某一个指标。内存优化是一个全局工程H5 资源压缩、图片降级、列表懒加载、WebView 复用策略、backForwardList 清理、后台挂起时的 JS 暂停每个环节省一点综合下来 WebContent 进程的内存峰值就有明显改善。单一手段的效果都很有限组合起来才能解决问题。第四个心得调试这类问题一定要有线上日志和统一的诊断入口。我们早期吃了很多用户说不清、开发猜不出的亏。后来统一在 App 侧记录 WebView 的完整生命周期事件创建时间、导航开始/完成时长、进程终止时间、恢复时间、JS 探活结果、POST body 拦截结果所有日志打点到统一的日志平台支持按用户 ID 或设备 ID 检索。有了这套日志再遇到线上反馈白屏或者提交失败基本能在五分钟内定位到具体原因。最后再说一个小技巧如果你们的线上环境有专门用于灰度的小流量用户群建议在灰度阶段打开 WKWebView 的setValue:forKey:_diagnosticLoggingEnabled私有属性拿到 WebKit 内部的诊断日志很多莫名奇妙的白屏都能从里面找到线索。注意这只是调试用途不建议线上长期开着。WKWebView 的白屏和 request body 丢失问题技术上不算特别深但涉及的坑很多网上资料也比较分散。我写这篇的初衷就是把自己踩过的坑和最终验证过的方案整理成一份可以直接抄作业的参考希望能帮你少走一些弯路。如果你们的业务场景里有更特殊的表现欢迎一起交流讨论。