深入理解 WKWebView:代理方法与 WKWebView 生命周期的执行顺序

26 阅读4分钟

在 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

六、最佳实践建议

  1. 双失败回调都要实现,覆盖所有异常场景。
  2. 所有 decisionHandler 必须调用,避免页面卡死。
  3. 避免循环引用:delegate 使用 weak self,或在 dealloc 中置 nil。
  4. 真机测试异常网络:使用「设置 > 开发者 > 网络链接条件」模拟弱网/断网。
  5. 不要依赖 HTTP 状态码判断失败:404/500 仍会触发 didFinishNavigation

📌 延伸思考

  • iOS 15+ 新增 WKNavigationDelegate 的 frame 级回调(如 didFinishDocumentLoadForFrame:
  • 若需深度控制缓存策略,可结合 WKWebsiteDataStore 使用

如果你觉得本文对你有帮助,欢迎 点赞 ❤️、收藏 ⭐、评论 💬!也欢迎关注我,获取更多 iOS 底层与实战技巧。