WKWebView 高级缓存策略技术文档

1. 概述 #

本文档旨在详细阐述为 iOS WKWebView 设计并实现的一套高级、智能的缓存解决方案。该方案的目标是提升H5页面的加载性能、优化用户体验,并为复杂的Web应用(尤其是采用Webpack等现代前端工具构建的单页应用)提供强大的缓存控制能力。

核心目标 #

  • 提升性能:最大化利用缓存,减少不必要的网络请求,加快页面和资源的加载速度。
  • 永久缓存静态资源:对于文件名中带有hash值(如 app.a1b2c3d4.js)的不可变资源,实现“一次加载,永久使用”的效果。
  • 智能决策:根据资源类型(HTML, JS, CSS)、URL特征(是否带hash)、网络状态(在线/离线)和手动配置,自动选择最优的缓存策略。
  • 完全控制:实现对页面所有子资源(包括JS动态加载的资源)的请求拦截和缓存控制。

2. 缓存策略设计与权衡 #

我们探讨并实现了一套分层、优先的缓存决策逻辑,并对关键技术点进行了权衡。

2.1. 缓存决策的优先级 #

核心方法 getSmartCachePolicy 遵循以下优先级顺序进行决策:

  1. 最高优先级:网络状态

    • 如果设备离线,无条件使用缓存 (.returnCacheDataDontLoad),保证基础的离线访问能力。
  2. 次高优先级:手动配置规则

    • 开发者可以预设强制刷新或永久缓存的URL路径前缀,实现对特定API或目录的精准控制。
  3. 标准优先级:智能识别规则

    • 根据URL和资源类型自动判断,如带hash的资源、普通静态资源、HTML页面等。
  4. 最低优先级:默认协议

    • 对于不匹配任何上述规则的请求,遵循HTTP协议本身定义的缓存行为 (.useProtocolCachePolicy)。

2.2. HTML页面的缓存策略权衡 #

我们重点讨论了HTML页面应采用何种策略:

  • .reloadIgnoringLocalCacheData:强制刷新,最安全但性能较低。能确保用户总能获取最新的HTML,避免因HTML陈旧导致资源引用错误。
  • .useProtocolCachePolicy:遵循服务器协议,性能更高但依赖后端配置。如果服务器为HTML配置了 Cache-Control: no-cacheETag,则可实现高效的304验证,是最佳实践。

最终决策:考虑到前端服务器已配置 Cache-Control: no-cache,我们选择 .useProtocolCachePolicy。这使得App具备了“面向未来”的适应性——一旦服务器增加了ETag支持,App无需更新即可自动获得性能提升。


3. 核心实现:从导航拦截到完全拦截 #

我们通过两个阶段的迭代,最终实现了对所有资源的完全拦截。

阶段一:导航拦截 (WKNavigationDelegate) #

此方案通过WKNavigationDelegate拦截主文档的加载请求,实现简单、风险低,能解决大部分问题。

3.1.1. 核心组件 #

BaseWebViewController+Caching.swift (核心逻辑)

import UIKit
import WebKit

extension BaseWebViewController {

    /// 获取完整的智能缓存策略
    private func getSmartCachePolicy(for url: URL) -> URLRequest.CachePolicy {
        print("--- [SmartCache] Evaluating URL: \(url.absoluteString)")

        // 1. 最高优先级:网络状态
        if !NetworkReachability.shared.isConnected {
            print("--- [SmartCache] Rule: Offline. Policy: returnCacheDataDontLoad")
            return .returnCacheDataDontLoad
        }

        let path = url.path

        // 2. 次高优先级:手动配置规则
        for noCachePath in CacheConfig.noCachePaths {
            if path.hasPrefix(noCachePath) {
                print("--- [SmartCache] Rule: Matched no-cache path '\(noCachePath)'. Policy: reloadIgnoringLocalCacheData")
                return .reloadIgnoringLocalCacheData
            }
        }
        for permanentPath in CacheConfig.permanentCachePaths {
            if path.hasPrefix(permanentPath) {
                print("--- [SmartCache] Rule: Matched permanent cache path '\(permanentPath)'. Policy: returnCacheDataElseLoad")
                return .returnCacheDataElseLoad
            }
        }

        // 3. 标准优先级:智能识别规则
        if url.hasHashInFilename {
            print("--- [SmartCache] Rule: Has hash in filename. Policy: returnCacheDataElseLoad")
            return .returnCacheDataElseLoad
        }

        if url.isStaticResource {
            print("--- [SmartCache] Rule: Is static resource. Policy: returnCacheDataElseLoad")
            return .returnCacheDataElseLoad
        }

        // 最终采纳的HTML页面策略
        if url.isHTMLPage {
            print("--- [SmartCache] Rule: Is HTML page. Policy: useProtocolCachePolicy")
            return .useProtocolCachePolicy
        }

        // 4. 最低优先级:默认
        print("--- [SmartCache] Rule: No specific rule matched. Policy: useProtocolCachePolicy")
        return .useProtocolCachePolicy
    }
    
    // ... loadWithSmartCache 方法 ...
}

CacheConfiguration.swift (手动配置)

import Foundation

struct CacheConfig {
    static let permanentCachePaths: [String] = ["/static/", "/assets/", "/dist/"]
    static let noCachePaths: [String] = ["/api/", "/user/", "/realtime/"]
}

URL+CacheHelpers.swift (URL辅助判断)

import Foundation

extension URL {
    var isStaticResource: Bool { ... }
    var hasHashInFilename: Bool { ... }
    var isHTMLPage: Bool { ... }
}

3.1.2. 缺点 #

此方案无法拦截页面通过JavaScript动态加载的子资源(如 import()fetch 加载的模块)。

阶段二:完全拦截 (WKURLSchemeHandler) #

为了实现对所有资源的完全控制,我们引入了WKURLSchemeHandler,这是Apple官方推荐的终极方案。

3.2.1. 工作流程 #

  1. 注入JS:通过WKUserScript在页面加载前注入一个url_rewriter.js脚本。
  2. 重写URL:该脚本通过MutationObserver和“猴子补丁”技术,将页面中所有符合条件的资源URL(JS/CSS)从https://重写为自定义的app-cache-https://协议。
  3. 原生处理WKWebView将所有自定义协议的请求交给一个我们实现的CacheSchemeHandler对象。
  4. 应用策略:在CacheSchemeHandler中,我们将URL还原,并可调用getSmartCachePolicy来应用缓存策略,然后手动发起网络请求或从本地缓存读取数据,最后将结果回调给WebView。

3.2.2. 核心组件 #

url_rewriter.js (注入脚本)

该脚本是实现完全拦截的关键,它结合了DOM监控和函数重写,以覆盖绝大多数资源加载场景。

(function() {
    const newScheme = 'app-cache-https';

    function rewriteUrl(originalUrl) { ... }

    // --- 1. 拦截基于 DOM 的加载 ---
    function rewriteDomElement(element) { ... }
    const observer = new MutationObserver(...);
    observer.observe(document, { childList: true, subtree: true });

    // --- 2. 拦截基于 fetch 的加载 ---
    const originalFetch = window.fetch;
    window.fetch = function(input, init) { ... };

    // --- 3. 拦截基于 XMLHttpRequest 的加载 ---
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url, async, user, password) { ... };

})();

CacheSchemeHandler.swift (自定义协议处理器)

import WebKit

class CacheSchemeHandler: NSObject, WKURLSchemeHandler {
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeT) {
        // 1. 还原 URL
        // 2. 调用 getSmartCachePolicy 获取缓存策略
        // 3. 手动发起 URLSession 请求
        // 4. 将 response 和 data 回传给 urlSchemeTask
    }

    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
        // 取消网络请求
    }
}

BaseWebViewController (配置注入)

// 在 WKWebViewConfiguration 的初始化过程中

// 1. 读取JS文件
if let jsPath = Bundle.main.path(forResource: "url_rewriter", ofType: "js"),
   let jsSource = try? String(contentsOfFile: jsPath) {
    // 2. 创建并添加 WKUserScript
    let script = WKUserScript(source: jsSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
    userContentController.add(script)
}

// 3. 注册 Scheme Handler
config.setURLSchemeHandler(CacheSchemeHandler(), forURLScheme: "app-cache-https")

4. 安全性与稳定性分析 #

  • 安全性:可靠

    • JS来源可信:注入的JS脚本由我们自己控制,并非来自外部,杜绝了恶意代码风险。
    • 协议本地有效:自定义的app-cache-https协议仅在当前WKWebView实例内部有效,不会暴露给系统或其他App,无法被外部调用。
  • 稳定性:中等风险

    • 主要风险在于JS的“猴子补丁”技术可能与某些前端框架或特定网站的JS代码不兼容,从而导致功能性问题。因此,必须在目标网页上进行充分的兼容性测试

5. 结论 #

通过结合WKNavigationDelegateWKURLSchemeHandler,我们构建了一套强大、灵活且安全的WKWebView缓存体系。该体系不仅满足了对带hash值的静态资源进行永久缓存的核心需求,还通过JS注入实现了对动态加载资源的完全拦截和控制,为深度优化H5性能提供了坚实的基础。