WebKit URL Cache 与网络拦截

429 阅读3分钟

Network 进程 URL Cache

是在 WKNetworkSessionDelegate 使用 NSURLSession 处理网络请求,没有实现其 URLSession:dataTask:willCacheResponse:completionHandler:代理,底层会默认写 NSURLCache 缓存。

也就是说网络请求的缓存会存在于 Networking 进程里面。

SchemeHandler 如何网络拦截

  • 调用 -[WKWebViewConfiguration -setURLSchemeHandler:forURLScheme:] 注入处理 http / https 的自定义 handler ;
  • Hook -[WKWebView handlesURLScheme:] 允许处理 http / https 的 scheme;
  • 在自定义 handler 里面实现网络请求并回传;

SchemeHandler 底层链路

SchemeHandler 存储链路

-[WKWebViewConfiguration -setURLSchemeHandler:forURLScheme:]
调用:
PageConfiguration::setURLSchemeHandlerForURLScheme
存储到这里:
HashMap<WTF::String, Ref<WebKit::WebURLSchemeHandler>> m_urlSchemeHandlers;

网络请求发起链路

有网络请求时会走到 SchemeHandler 的 -webView:startURLSchemeTask: 方法,由此在 WebKit 找到调用链路:

//WebContent 进程
DocumentLoader::loadMainResource
CachedResourceLoader::requestResource
CachedResource::load
… WebLoaderStrategy::loadResource
WebLoaderStrategy::tryLoadingUsingURLSchemeHandler
WebURLSchemeHandlerProxy::startNewTask
WebURLSchemeTaskProxy::startLoading
IPC: Messages::WebPageProxy::StartURLSchemeTask
//APP进程
WebPageProxy::startURLSchemeTask
WebPageProxy::startURLSchemeTaskShared
WebURLSchemeHandler::startTask
WebURLSchemeHandlerCocoa::platformStartTask
-[WKURLSchemeHandler webView:startURLSchemeTask:]

自定义 WKURLSchemeHandler 网络回包后回传链路

//APP 进程
-[WKURLSchemeTask didReceiveResponse:]
WebURLSchemeTask::didReceiveResponse
IPC: Messages::WebPage::URLSchemeTaskDidReceiveResponse
//WebContent 进程
WebPage::URLSchemeTaskDidReceiveResponse
WebURLSchemeHandlerProxy::taskDidReceiveResponse
WebURLSchemeTaskProxy::didReceiveResponse
ResourceLoader::didReceiveResponse
ResourceLoadNotifier::didReceiveResponse
DocumentLoader::addResponse

看起来没有传送到 Networking 进程。

Web 进程资源缓存

加载网页资源时会在 CachedResourceLoader 寻找是否有可用缓存:

DocumentLoader::loadMainResource
CachedResourceLoader::requestResource
CachedResourceLoader::determineRevalidationPolicy

若没有命中缓存会执行:

…CachedResourceLoader::loadResource(…) {
…
    auto resource = createResource(type, WTFMove(request), sessionID, &cookieJar, settings);
    if (resource->allowsCaching())
        memoryCache.add(*resource);
…
}

createResource(…)实际上就会返回继承 CachedResource 抽象类的资源对象,能缓存则存入 memoryCache,比如 CachedImage / CachedCSSStyleSheet / CachedScript / CachedSVGDocument,后续链路会发起网络请求。

WKURLSchemeHandler 与 CachedResourceLoader 交叉点

由上面的源码分析知,CachedResourceLoader 会先查找 memoryCache,不可用时调用CachedResource::load加载资源,后续会走到 SchemeHandler 处理链路。

SchemeHandler 资源数据是否利用到了 Web 进程资源缓存

对于服务器返回的 response 和不拦截情况没差异,但如果是客户端构建的 response 可能会有一些问题,最直观的就是响应 http header 可能不一致。

所以我们需要知道客户端构建 response 回传到 Web 进程使用与缓存后,第二次访问到是否还有效。

导致 CachedResourceLoader 的 memoryCache 失效原因很多,在最后一部分发现了端倪(有些资源不会走到这个链路,比如图片资源多半没这个判定):

… CachedResourceLoader::determineRevalidationPolicy(…) {
…
    auto revalidationDecision = existingResource->makeRevalidationDecision(cachePolicy);
    // Check if the cache headers requires us to revalidate (cache expiration for example).
    if (revalidationDecision != CachedResource::RevalidationDecision::No) {
        // See if the resource has usable ETag or Last-modified headers.
        if (existingResource->canUseCacheValidator()) {
            return Revalidate;
        }
        // No, must reload.
        return Reload;
    }
return Use;
}

一看就是对重用缓存的有效性判定,客户端构建不会加 ETag / Last-modified 所以不用管,也就是说只要 revalidationDecision 不是 No 就得 Reload,无法返回 Use 让上层直接复用。看下如何判定的:

CachedResource::RevalidationDecision CachedResource::makeRevalidationDecision(CachePolicy cachePolicy) const {
…
        if (m_response.cacheControlContainsNoCache())
            return RevalidationDecision::YesDueToNoCache;
        // FIXME: Cache-Control:no-store should prevent storing, not reuse.
        if (m_response.cacheControlContainsNoStore())
            return RevalidationDecision::YesDueToNoStore;
        if (isExpired())
            return RevalidationDecision::YesDueToExpired;
        return RevalidationDecision::No;
…
}

isExpired()实际上就是根据响应头判定资源是否过期,获取响应头新鲜度的代码是这样的:

Seconds computeFreshnessLifetimeForHTTPFamily(…) {
…
    // Freshness Lifetime:
    // http://tools.ietf.org/html/rfc7234#section-4.2.1
    auto maxAge = response.cacheControlMaxAge();
    if (maxAge)
        return *maxAge;

    auto date = response.date();
    auto effectiveDate = date.value_or(responseTime);
    if (auto expires = response.expires())
        return *expires - effectiveDate;

    // Implicit lifetime.
    switch (response.httpStatusCode()) {
    case 301: // Moved Permanently
    case 410: // Gone
        // These are semantically permanent and so get long implicit lifetime.
        return 24_h * 365;
    default:
        // Heuristic Freshness:
        // http://tools.ietf.org/html/rfc7234#section-4.2.2
        if (auto lastModified = response.lastModified())
            return (effectiveDate - *lastModified) * 0.1;
        return 0_us;
    }
}

逻辑很清晰,先后获取了多个响应头:Cache-Control : max-age、Expires、Date,后面是特殊响应状态码判定,返回值都是有效时间戳与 responseTime 的时间差。

可见,若想让 URL 资源可被 WebContent 进程复用,客户端构建 response 需要设置有效的 max-age 或 Expires 或 Last-Modified 响应头即可。

总结

对于+[WKBrowsingContextController registerSchemeForCustomProtocol:]拦截方案,它是向 Network 进程的 NSURLSession 注册 NSURLProtocol,对于客户端构建的响应包,同样会由 Network 进程传递到 Web 进程,走上面的 CachedResourceLoader 链路。

明确了有无 WebKit 网络拦截时网络请求和缓存的链路,这将在我们做优化决策时起到作用。

比如通过客户端构建网络回包的优化方向,包括离线包方案、自定义 URL Cache、Web 资源预请求、前端 link preload 等。