在 iOS 开发中,WKWebView 是构建混合应用(Hybrid App)的核心组件。它基于现代 WebKit 引擎,性能优异、安全性高,但其复杂的生命周期机制也让不少开发者感到困惑——尤其是当页面加载失败时,错误回调到底在哪个阶段触发?
本文将深入解析 WKWebView 的完整生命周期,以 Objective-C 为开发语言,系统梳理 WKNavigationDelegate 中各代理方法的执行时机与调用顺序,并通过对比 正常加载成功 与 加载失败 两种典型场景,帮助你精准掌控 WebView 行为,避免常见陷阱。
✅ 适用系统:iOS 9+(建议 iOS 11+)
💬 开发语言:Objective-C
🧭 核心协议:WKNavigationDelegate
一、生命周期全景图
WKWebView 的导航过程由一系列代理方法串联而成。根据加载结果不同,可分为两条主路径:
✅ 成功路径(页面正常加载)
didStartProvisionalNavigation
→ decidePolicyForNavigationAction
→ didCommitNavigation
→ decidePolicyForNavigationResponse
→ didFinishNavigation
❌ 失败路径(加载中断)
didStartProvisionalNavigation
→ decidePolicyForNavigationAction(可能)
→ didFailProvisionalNavigation // 早期失败(如 DNS 错误)
或
→ didCommitNavigation
→ didFailNavigation // 提交后失败(如 SSL 证书无效)
⚠️ 重要认知:
- HTTP 404/500 不会触发 fail 回调!因为服务器已返回有效响应,属于“成功加载错误页”。
- 所有
decisionHandler必须被调用,否则 WebView 将卡死。
二、成功加载:五步走流程详解
当访问一个有效 URL(如 https://example.com)且网络通畅时,代理方法按以下顺序严格触发:
1. webView:didStartProvisionalNavigation:
- 页面开始尝试加载。
- URL 可能尚未最终确定(例如重定向前)。
- 适合启动 loading 动画。
- (void)webView:(WKWebView *)webView
didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"✅ Start provisional navigation to: %@", webView.URL);
[self showLoadingIndicator];
}
2. webView:decidePolicyForNavigationAction:decisionHandler:
- 决定是否允许此次跳转。
- 常用于拦截自定义 scheme(如
tel://,weixin://)。
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)action
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
NSURL *url = action.request.URL;
if ([[url scheme] isEqualToString:@"tel"]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
handler(WKNavigationActionPolicyCancel); // 拦截并交由系统处理
return;
}
handler(WKNavigationActionPolicyAllow); // 允许加载
}
🔥 必须调用
handler()!否则页面将永远处于“加载中”。
3. webView:didCommitNavigation:
- 浏览器已接收到响应头,开始接收 HTML 数据。
- DOM 开始构建,但未渲染完成。
- 此时
webView.URL已是最终地址(可用于埋点或日志)。
- (void)webView:(WKWebView *)webView
didCommitNavigation:(WKNavigation *)navigation {
NSLog(@"✅ Committed to final URL: %@", webView.URL);
}
4. webView:decidePolicyForNavigationResponse:decisionHandler:
- 针对服务器返回的响应(状态码、MIME 类型等)决定是否继续加载。
- 可用于拦截非 HTML 资源(如 PDF、ZIP 文件)。
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationResponse:(WKNavigationResponse *)response
decisionHandler:(void (^)(WKNavigationResponsePolicy))handler {
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response.response;
if ([httpResp.MIMEType isEqualToString:@"application/pdf"]) {
// 拦截 PDF 下载
handler(WKNavigationResponsePolicyCancel);
return;
}
handler(WKNavigationResponsePolicyAllow);
}
5. webView:didFinishNavigation:
- 所有资源(HTML、CSS、JS、图片等)加载完毕。
- 页面完全可交互。
- 隐藏 loading、注入 JS、执行业务逻辑的最佳时机。
- (void)webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"✅ Page fully loaded!");
[self hideLoadingIndicator];
// 可在此注入 JS 或通知上层
}
三、失败加载:两类错误路径剖析
加载失败分为 Provisional 阶段失败 与 Commit 后失败,需分别处理。
🔴 类型 1:Provisional 阶段失败
触发方法:didFailProvisionalNavigation:withError:
典型原因:
- DNS 解析失败(域名不存在)
- 无法连接服务器(断网、超时)
- URL 格式非法
✅ didStartProvisionalNavigation
✅ decidePolicyForNavigationAction
❌ didFailProvisionalNavigation: "A server with the specified hostname could not be found."
🔴 类型 2:Commit 后失败
触发方法:didFailNavigation:withError:
典型原因:
- SSL/TLS 证书无效或过期(iOS 默认拦截)
- 服务器在传输中途断开连接
✅ didStartProvisionalNavigation
✅ decidePolicyForNavigationAction
✅ didCommitNavigation
❌ didFailNavigation: "The certificate for this server is invalid."
❗ 关键提醒:只监听
didFailProvisionalNavigation会漏掉 SSL 错误!必须同时实现两个失败回调。
统一错误处理示例
- (void)webView:(WKWebView *)webView
didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
NSLog(@"❌ Provisional fail: %@", error.localizedDescription);
[self showErrorViewWithError:error];
}
- (void)webView:(WKWebView *)webView
didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
NSLog(@"❌ Navigation fail after commit: %@", error.localizedDescription);
[self showErrorViewWithError:error];
}
四、初始化与代理设置示例
// ViewController.h
@interface ViewController () <WKNavigationDelegate>
@property (nonatomic, strong) WKWebView *webView;
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
self.webView.navigationDelegate = self;
[self.view addSubview:self.webView];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com"]]];
}
💡 建议:若需共享 Cookie 或缓存,可复用
WKProcessPool。
五、总结:关键要点速查表
| 场景 | 触发方法 | 是否必须处理 |
|---|---|---|
| 页面开始加载 | didStartProvisionalNavigation | ✅ |
| 决策是否跳转 | decidePolicyForNavigationAction | ✅(必须调用 handler) |
| 页面提交(DOM 开始构建) | didCommitNavigation | ✅ |
| 决策是否接受响应 | decidePolicyForNavigationResponse | ✅(必须调用 handler) |
| 加载完成 | didFinishNavigation | ✅ |
| 早期失败(DNS/断网) | didFailProvisionalNavigation | ✅ |
| 提交后失败(SSL/中断) | didFailNavigation | ✅ |
六、最佳实践建议
- 双失败回调都要实现,覆盖所有异常场景。
- 所有
decisionHandler必须调用,避免页面卡死。 - 避免循环引用:delegate 使用 weak self,或在
dealloc中置 nil。 - 真机测试异常网络:使用「设置 > 开发者 > 网络链接条件」模拟弱网/断网。
- 不要依赖 HTTP 状态码判断失败:404/500 仍会触发
didFinishNavigation。
📌 延伸思考:
- iOS 15+ 新增
WKNavigationDelegate的 frame 级回调(如didFinishDocumentLoadForFrame:)- 若需深度控制缓存策略,可结合
WKWebsiteDataStore使用
如果你觉得本文对你有帮助,欢迎 点赞 ❤️、收藏 ⭐、评论 💬!也欢迎关注我,获取更多 iOS 底层与实战技巧。