深入理解 WKWebView(基础篇)-- 探究 WebKit 缓存

发表于 2年以前  | 总阅读数:363 次

1. 前言

缓存可以减少冗余的数据传输,解决网络瓶颈问题,降低服务端压力,提升页面加载速度。高效利用缓存可大幅提升页面加载速度,提升用户的浏览体验。WKWebView 使用缓存技术存储前后端资源,用户提高页面性能和用户体验。因为 WKWebView 的封闭性,我们无法针对原生 WKWebView 做较深度化的定制,但对于 WebKit 缓存源码的探究,将帮助我们更好的使用和理解缓存。本文将延续 [《iOS 端 webkit 源码调试与分析》] 的思路,结合源码枚举 WKWebView 中的各类缓存,并重点讲述其中的 HTTP 协议缓存,帮助读者更好的理解 WebKit 中缓存的设计思路。

2. 缓存简介

2.1 缓存类型

2.1.1 WebKit 标准缓存类型


// HTTP 磁盘缓存。
WKWebsiteDataTypeDiskCache,

// html离线Web应用程序缓存。
WKWebsiteDataTypeOfflineWebApplicationCache, 

// HTTP 内存缓存。
WKWebsiteDataTypeMemoryCache, 

// 会话存储:存储对数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。
// 因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储
WKWebsiteDataTypeSessionStorage, 

// 本地存储:localStorage 类似 sessionStorage,但其区别在于,存储在 localStorage 的数据可以长期保留.
WKWebsiteDataTypeLocalStorage, 

// Cookies存储:存储所有的cookie数据 
WKWebsiteDataTypeCookies, 

// IndexedDB数据库:IndexedDB是WebSQL数据库的取代品。IndexedDB是key-value型数据库,操作简单。
WKWebsiteDataTypeIndexedDBDatabases, 

// webSQL数据库:W3C组织在2010年11月18日废弃了webSql 数据库,该数据库接口操组复杂,对用户不友好。
WKWebsiteDataTypeWebSQLDatabases

通过数据分析,主要是 indexedDB 与 NetworkCache 占据较大比例,可达80%以上。WebKit 磁盘缓存分布如下表:

磁盘文件目录 缓存类型
Library/WebKit IndexedDB LocalStorage MediaKeys ResourceLoadStatistics
Library/Caches/WebKit CacheStorage NetworkCache offlineWebApplicationCache ServiceWorkers

2.1.2 前进后退缓存 – pageCache

在 WebKit 中,pageCache 其实就是对 WebBackForwardCache – 前进后退缓存的封装,本质上是浏览历史的一种记录,不属于上述标准缓存。前进后退缓存,将整个页面快照存入到内存中,下一次使用的时候,不用进行各类资源加载,甚至不用进行渲染工作。

通过源码查看,pageCache 大小会随着可使用内存大小动态变化:

手机可用内存a 可缓存page页数
a >= 512M 2
512M > a >= 256M 1
other 0

缓存策略源码如下所示:

// back/forward cache capacity (in pages)
if (memorySize >= 512)
    backForwardCacheCapacity = 2;
else if (memorySize >= 256)
    backForwardCacheCapacity = 1;
else
    backForwardCacheCapacity = 0;

资源的过期时间默认为30分钟。通过定时器触发任务,30分钟后自动清理过期的 page。源码如下:

static const Seconds expirationDelay { 30_min }; 
//通过定时器触发,到过期时间后,进行资源清理 
void WebBackForwardCacheEntry::expirationTimerFired() 
{
    RELEASE_LOG(BackForwardCache, "%p - WebBackForwardCacheEntry::expirationTimerFired backForwardItemID=%s, hasSuspendedPage=%d", this, m_backForwardItemID.string().utf8().data(), !!m_suspendedPage);
    ASSERT(m_backForwardItemID);
    auto* item = WebBackForwardListItem::itemForID(m_backForwardItemID);
    ASSERT(item);
    m_backForwardCache.removeEntry(*item); // Will destroy |this|.
}

因 pageCache 存储页面数量有限,因此当超出页面缓存上限时,需要通过如下 LRU 算法进行替换:

void BackForwardCache::prune(PruningReason pruningReason) 
{
    while (pageCount() > maxSize()) { 
        auto oldestItem = m_items.takeFirst(); 
        oldestItem->setCachedPage(nullptr); 
        oldestItem->m_pruningReason = pruningReason; 
        RELEASE_LOG(BackForwardCache, "BackForwardCache::prune removing item: %s, size: %u / %u", oldestItem->identifier().string().utf8().data(), pageCount(), maxSize());
    }
}

缓存时机源码如下:

bool WebPageProxy::suspendCurrentPageIfPossible(...) {
    ...
    // If the source and the destination back / forward list items are the same, then this is a client-side redirect. In this case, 
    // there is no need to suspend the previous page as there will be no way to get back to it. 
    if (fromItem && fromItem == m_backForwardList->currentItem()) { 
        RELEASE_LOG_IF_ALLOWED(ProcessSwapping, "suspendCurrentPageIfPossible: Not suspending current page for process pid %i because this is a client-side redirect", m_process->processIdentifier()); 
        return false;
    }
    ...
    //创建 SuspendedPageProxy 变量,此时 m_suspendedPageCount 的值会加一 
    auto suspendedPage = makeUnique<SuspendedPageProxy>(*this, m_process.copyRef(), *mainFrameID, shouldDelayClosingUntilFirstLayerFlush); 
    m_lastSuspendedPage = makeWeakPtr(*suspendedPage); 
    ...
    //添加进历史栈缓存 
    backForwardCache().addEntry(*fromItem, WTFMove(suspendedPage)); 
    ...
}

可以看到,如果 WKWebView 切换页面时,发生 cross-site 且为 client-side redirect 时会清理当前 WebProgressProxy 关联的所有历史栈缓存,后续切换到这些历史栈时都需要重新请求网络。而其他类型都会正常存储,因此可以基于前进后退相关操作的页面性能考虑,可以减少前端重定向,多依赖后端进行重定向功能。

2.2 缓存清理方式

处于内存中的缓存,会随着进程的结束而消亡。而处于磁盘中的缓存,则可以通过如下方法进行手动清理,避免磁盘占用增长过大。

2.2.1 文件目录清理方式

webkit磁盘中的较多数据都是通过域名做为文件名的一部分,因此也可以通过域名、日期等方式匹配,进行文件删除:

NSString *libraryDir = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask, YES)[0];
 NSString *bundleId = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"];
 NSString *webkitFolderInLib = [NSString stringWithFormat:@"%@/WebKit",libraryDir];
 NSString *webKitFolderInCaches = [NSString stringWithFormat:@"%@/Caches/%@/WebKit",libraryDir,bundleId];
 NSError *error;
 [[NSFileManager defaultManager] removeItemAtPath:webKitFolderInCaches error:&error];
 [[NSFileManager defaultManager] removeItemAtPath:webkitFolderInLib error:nil];

localStorage 存储文件样例

2.2.2 缓存类型清理方式

iOS 9.0以后 , WebKit 清除缓存的API,测试来看必须在主线程进行操作。

NSSet *websiteDataTypes = [NSSet setWithArray:@[ 
WKWebsiteDataTypeDiskCache, 
WKWebsiteDataTypeOfflineWebApplicationCache, 
WKWebsiteDataTypeLocalStorage, 
WKWebsiteDataTypeCookies, 
WKWebsiteDataTypeSessionStorage, 
WKWebsiteDataTypeIndexedDBDatabases, 
WKWebsiteDataTypeWebSQLDatabases 
]];
NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; 
//dataTypes: 指定删除的网站数据类型,date: 在此日期之后修改的所有网站数据将被删除,completionHandler: 当网站数据被删除时调用的block。
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{ 
// 结束回调 
}];

3. HTTP 内存缓存

WKWebView 与 app 处于不同进程中,且内存与磁盘缓存也在不同进程中,其中,内存缓存位于 WebContentProcess 进程中,而磁盘缓存位于 NetworkProcess 进程中。且每个memoryCache 对应一个 webContent 进程,如图所示。

如上图所示,一个页面对应一个WebContentProcess 进程,当页面销毁时,其对应的内存缓存也被销毁。

3.1 内存缓存大小

虽然 WebKit 进程独立与 app 进程,但内存占用过大依旧会影响到 app 进程的性能,因此内存缓存根据手机当前缓存大小进行分配。

手机可用内存a 页面内存分配
a >= 2G 128M
2G > a >= 1.5G 96M
1.5G > a >= 1G 64M
1G > a >= 0.5G 32M
other 16M

缓存大小计算策略源码如下:

case CacheModel::PrimaryWebBrowser: { 
    // back/forward cache capacity (in pages) 
    if (memorySize >= 512) 
        backForwardCacheCapacity = 2; 
    else if (memorySize >= 256) 
        backForwardCacheCapacity = 1; 
    else 
        backForwardCacheCapacity = 0; 
    // Object cache capacities (in bytes) 
    // (Testing indicates that value / MB depends heavily on content and 
    // browsing pattern. Even growth above 128MB can have substantial 
    // value / MB for some content / browsing patterns.) 
    if (memorySize >= 2048) 
        cacheTotalCapacity = 128 * MB; 
    else if (memorySize >= 1536) 
        cacheTotalCapacity = 96 * MB; 
    else if (memorySize >= 1024) 
        cacheTotalCapacity = 64 * MB; 
    else if (memorySize >= 512) 
        cacheTotalCapacity = 32 * MB; 
    else 
        cacheTotalCapacity = 16 * MB; 
    cacheMinDeadCapacity = cacheTotalCapacity / 4; 
    cacheMaxDeadCapacity = cacheTotalCapacity / 2; 
    // This code is here to avoid a PLT regression. We can remove it if we 
    // can prove that the overall system gain would justify the regression. 
    cacheMaxDeadCapacity = std::max(24u, cacheMaxDeadCapacity); 
    deadDecodedDataDeletionInterval = 60_s; 
    break; 
}

3.2 内存缓存策略

使用 map 字典,在内存中使用 url 为 key,resource 资源为 value,对当前页面的所有 HTTP 网络请求资源进行存储。

3.2.1 内存缓存添加

bool MemoryCache::add(CachedResource& resource) 
{
    if (disabled()) 
        return false; 
    if (resource.resourceRequest().httpMethod() != "GET") 
        return false; 
    ASSERT(WTF::isMainThread()); 
    auto key = std::make_pair(resource.url(), resource.cachePartition()); 
    ensureSessionResourceMap(resource.sessionID()).set(key, &resource); 
    resource.setInCache(true); 
    resourceAccessed(resource); 
    LOG(ResourceLoading, "MemoryCache::add Added '%.255s', resource %p\n", resource.url().string().latin1().data(), &resource); 
    return true; 
}

3.2.2 内存缓存读取


CachedResource* MemoryCache::resourceForRequest(const ResourceRequest& request, PAL::SessionID sessionID) 
{
    // FIXME: Change all clients to make sure HTTP(s) URLs have no fragment identifiers before calling here. 
    // CachedResourceLoader is now doing this. Add an assertion once all other clients are doing it too. 
    auto* resources = sessionResourceMap(sessionID); 
    if (!resources) 
        return nullptr; 
    return resourceForRequestImpl(request, *resources); 
}

CachedResource* MemoryCache::resourceForRequestImpl(const ResourceRequest& request, CachedResourceMap& resources) 
{
    ASSERT(WTF::isMainThread()); 
    URL url = removeFragmentIdentifierIfNeeded(request.url()); 
    auto key = std::make_pair(url, request.cachePartition()); 
    return resources.get(key); 
}

3.2.3 HTTP 内存缓存读取策略

HTTP 内存缓存读取时机不同于磁盘缓存,它并不完全遵守 HTTP 标准协议,而是根据浏览器所加载的资源策略来进行的。例如:

  1. 前进后退历史中的页面的资源请求,可以直接读取内存缓存。
  2. 图片资源使用同一 url 加载的时候,后续的资源会 block 住,等待首个图片资源的返回,直接使用缓存。
  3. preload 资源可直接读取缓存,不必进行任何判断。
// 网络请求加载是否使用内存缓存有如下策略:
enum RevalidationPolicy { 
Use, // 直接使用
Revalidate, // 需要经过 HTTP 缓存协议校验
Reload, // 重新加载,清理内存缓存,并重新请求
Load // 直接从网络加载
};

RevalidationPolicy policy = determineRevalidationPolicy(type, request, resource.get(), forPreload, imageLoading);

4. HTTP 磁盘缓存

磁盘缓存的设计完全遵循 HTTP 标准缓存协议。所有的网络请求都经过 NetWorkProcess 进程发出,请求在发出之前,则会经过缓存协议检验,根据 HTTP 协议进行相应操作(读取缓存/协商检验/不使用缓存等)。当服务端返回请求内容后,NetworkProcess 模块也会做出对应的判断,决定内容是否进行缓存或更新,如下所示。

4.1 HTTP 缓存处理流程图

4.2 HTTP 磁盘缓存大小

磁盘缓存存入到指定的文件目录中,其中默认为:Library/Caches/WebKit/NetworkCache。可以通过如下方法进行指定:

case CacheModel::PrimaryWebBrowser: { 

        // Disk cache capacity (in bytes) 
        if (diskFreeSize >= 16384) 
            urlCacheDiskCapacity = 1 * GB; 
        else if (diskFreeSize >= 8192) 
            urlCacheDiskCapacity = 500 * MB; 
        else if (diskFreeSize >= 4096) 
            urlCacheDiskCapacity = 250 * MB; 
        else if (diskFreeSize >= 2048) 
            urlCacheDiskCapacity = 200 * MB; 
        else if (diskFreeSize >= 1024) 
            urlCacheDiskCapacity = 150 * MB; 
        else 
            urlCacheDiskCapacity = 100 * MB; 

        break; 
    }
    default: 
        ASSERT_NOT_REACHED(); 
    };

****4.3 HTTP 存入缓存校验

本部分主要根据请求和响应来判断是否需要存储到缓存中。主要判断 scheme、method 以及资源的缓存策略。

// WebKit/Source/WebKit/NetworkProcess/cache/NetworkCache.cpp
static StoreDecision makeStoreDecision(const WebCore::ResourceRequest& originalRequest, const WebCore::ResourceResponse& response, size_t bodySize)
{
    if (!originalRequest.url().protocolIsInHTTPFamily() || !response.isInHTTPFamily())
        return StoreDecision::NoDueToProtocol;


    if (originalRequest.httpMethod() != "GET")
        return StoreDecision::NoDueToHTTPMethod;


    auto requestDirectives = WebCore::parseCacheControlDirectives(originalRequest.httpHeaderFields());
    if (requestDirectives.noStore)
        return StoreDecision::NoDueToNoStoreRequest;


    if (response.cacheControlContainsNoStore())
        return StoreDecision::NoDueToNoStoreResponse;


    if (!WebCore::isStatusCodeCacheableByDefault(response.httpStatusCode())) {
        // http://tools.ietf.org/html/rfc7234#section-4.3.2
        bool hasExpirationHeaders = response.expires() || response.cacheControlMaxAge();
        bool expirationHeadersAllowCaching = WebCore::isStatusCodePotentiallyCacheable(response.httpStatusCode()) && hasExpirationHeaders;
        if (!expirationHeadersAllowCaching)
            return StoreDecision::NoDueToHTTPStatusCode;
    }


    bool isMainResource = originalRequest.requester() == WebCore::ResourceRequest::Requester::Main;
    bool storeUnconditionallyForHistoryNavigation = isMainResource || originalRequest.priority() == WebCore::ResourceLoadPriority::VeryHigh; 
    if (!storeUnconditionallyForHistoryNavigation) {
        auto now = WallTime::now();
        Seconds allowedStale { 0_ms };
#if ENABLE(NETWORK_CACHE_STALE_WHILE_REVALIDATE)
        if (auto value = response.cacheControlStaleWhileRevalidate())
            allowedStale = value.value();
#endif
        bool hasNonZeroLifetime = !response.cacheControlContainsNoCache() && (WebCore::computeFreshnessLifetimeForHTTPFamily(response, now) > 0_ms || allowedStale > 0_ms);
        bool possiblyReusable = response.hasCacheValidatorFields() || hasNonZeroLifetime;
        if (!possiblyReusable)
            return StoreDecision::NoDueToUnlikelyToReuse;
    }


    // Media loaded via XHR is likely being used for MSE streaming (YouTube and Netflix for example).
    // Streaming media fills the cache quickly and is unlikely to be reused.
    // FIXME: We should introduce a separate media cache partition that doesn't affect other resources.
    // FIXME: We should also make sure make the MSE paths are copy-free so we can use mapped buffers from disk effectively.
    auto requester = originalRequest.requester();
    bool isDefinitelyStreamingMedia = requester == WebCore::ResourceRequest::Requester::Media;
    bool isLikelyStreamingMedia = requester == WebCore::ResourceRequest::Requester::XHR && isMediaMIMEType(response.mimeType());
    if (isLikelyStreamingMedia || isDefinitelyStreamingMedia)
        return StoreDecision::NoDueToStreamingMedia;


    return StoreDecision::Yes;
}

4.4 HTTP 读取缓存校验

本部分主要根据请求来判断是否去缓存中读取缓存。主要判断 scheme、method 以及资源的缓存策略。

// WebKit/Source/WebKit/NetworkProcess/cache/NetworkCache.cpp 
static RetrieveDecision makeRetrieveDecision(const WebCore::ResourceRequest& request) 
{
    ASSERT(request.cachePolicy() != WebCore::ResourceRequestCachePolicy::DoNotUseAnyCache); 


    // FIXME: Support HEAD requests. 
    if (request.httpMethod() != "GET") 
        return RetrieveDecision::NoDueToHTTPMethod; 
    if (request.cachePolicy() == WebCore::ResourceRequestCachePolicy::ReloadIgnoringCacheData && !request.isConditional()) 
        return RetrieveDecision::NoDueToReloadIgnoringCache; 


    return RetrieveDecision::Yes; 
}

4.5 HTTP 使用缓存校验

本部分主要根据请求和响应来判断缓存是否可以直接使用。主要根据缓存字段计算当前的资源是否过期。

// WebKit/Source/WebKit/NetworkProcess/cache/NetworkCache.cpp 
static UseDecision makeUseDecision(NetworkProcess& networkProcess, const PAL::SessionID& sessionID, const Entry& entry, const WebCore::ResourceRequest& request) 
{
    // The request is conditional so we force revalidation from the network. We merely check the disk cache 
    // so we can update the cache entry. 
    // 条件请求判断 | bool ResourceRequestBase::isConditional 
    if (request.isConditional() && !entry.redirectRequest()) 
        return UseDecision::Validate; 


    // 校验变化的请求头 | verifyVaryingRequestHeaders 
    if (!WebCore::verifyVaryingRequestHeaders(networkProcess.storageSession(sessionID), entry.varyingRequestHeaders(), request)) 
        return UseDecision::NoDueToVaryingHeaderMismatch; 


    // We never revalidate in the case of a history navigation. 
    // 校验缓存是否过期 | cachePolicyAllowsExpired 
    if (cachePolicyAllowsExpired(request.cachePolicy()))  
        return UseDecision::Use;


    // 验证请求是否过期 
    auto decision = responseNeedsRevalidation(*networkProcess.networkSession(sessionID), entry.response(), request, entry.timeStamp()); 
    if (decision != UseDecision::Validate) 
        return decision; 


    // 验证缓存有效字端(Etag等) | bool ResourceResponseBase::hasCacheValidatorFields() 
    if (!entry.response().hasCacheValidatorFields()) 
        return UseDecision::NoDueToMissingValidatorFields; 


    return entry.redirectRequest() ? UseDecision::NoDueToExpiredRedirect : UseDecision::Validate; 
}

4.5.1 HTTP 缓存新鲜度计算

本部分主要根据缓存字段计算当前的资源的新鲜度。

// WebKit/Source/WebCore/platform/network/CacheValidation.cpp 
Seconds computeFreshnessLifetimeForHTTPFamily(const ResourceResponse& response, WallTime responseTime) 
{
    if (!response.url().protocolIsInHTTPFamily())
        return 0_us; 


    // 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.valueOr(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; 
    }
}

4.5.2 HTTP 缓存新鲜度计算

本部分主要根据缓存字段计算当前的资源是否过期。


// WebKit/Source/WebKit/NetworkProcess/cache/NetworkCache.cpp 
static UseDecision responseNeedsRevalidation(NetworkSession& networkSession, const WebCore::ResourceResponse& response, WallTime timestamp, Optional<Seconds> maxStale) 
{
    if (response.cacheControlContainsNoCache()) 
        return UseDecision::Validate; 


    // 当前过去的时间 = 当前时间 - 资源时间 | computeCurrentAge 
    auto age = WebCore::computeCurrentAge(response, timestamp); 
    // 调用资源有效时间计算 | computeFreshnessLifetimeForHTTPFamily 
    auto lifetime = WebCore::computeFreshnessLifetimeForHTTPFamily(response, timestamp); 


    // 资源允许过期时间 
    auto maximumStaleness = maxStale ? maxStale.value() : 0_ms; 
    // qy6_detail 资源是否超期 | 当前过去的时间 - 资源有效时间 - 允许过期时间 > 0 => 资源过期了 
    bool hasExpired = age - lifetime > maximumStaleness; 
#if ENABLE(NETWORK_CACHE_STALE_WHILE_REVALIDATE) 
    if (hasExpired && !maxStale && networkSession.isStaleWhileRevalidateEnabled()) { 
        auto responseMaxStaleness = response.cacheControlStaleWhileRevalidate(); 
        maximumStaleness += responseMaxStaleness ? responseMaxStaleness.value() : 0_ms; 
        bool inResponseStaleness = age - lifetime < maximumStaleness; 
        if (inResponseStaleness) 
            return UseDecision::AsyncRevalidate; 
    }
#endif 


    if (hasExpired) { 
#ifndef LOG_DISABLED 
        LOG(NetworkCache, "(NetworkProcess) needsRevalidation hasExpired age=%f lifetime=%f max-staleness=%f", age, lifetime, maximumStaleness); 
#endif 
        return UseDecision::Validate; 
    }


    return UseDecision::Use; 
}

4.6 HTTP 服务器资源校验

过期资源需要从服务器判断是否可用,需要构造一个条件请求去服务端验证当前过期资源是否可用。

// WebKit/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp 
void NetworkResourceLoader::validateCacheEntry(std::unique_ptr<NetworkCache::Entry> entry) 
{
    RELEASE_LOG_IF_ALLOWED("validateCacheEntry:"); 
    ASSERT(!m_networkLoad); 


    // If the request is already conditional then the revalidation was not triggered by the disk cache 
    // and we should not overwrite the existing conditional headers. 
    // 如果请求为条件请求,不修改 HEADER 中条件请求属性 
    ResourceRequest revalidationRequest = originalRequest(); 
    if (!revalidationRequest.isConditional()) { 
        String eTag = entry->response().httpHeaderField(HTTPHeaderName::ETag); 
        String lastModified = entry->response().httpHeaderField(HTTPHeaderName::LastModified); 
        // qy6_detail 新增缓存校验请求头,IfNoneMatch 和 IfModifiedSince 
        if (!eTag.isEmpty())
            revalidationRequest.setHTTPHeaderField(HTTPHeaderName::IfNoneMatch, eTag); 
        if (!lastModified.isEmpty()) 
            revalidationRequest.setHTTPHeaderField(HTTPHeaderName::IfModifiedSince, lastModified); 
    }


    m_cacheEntryForValidation = WTFMove(entry); 


    // qy6_detail 发起请求 
    startNetworkLoad(WTFMove(revalidationRequest), FirstLoad::Yes); 
}

4.7 HTTP 缓存资源更新

当服务器验证通过后,需要对现有的缓存资源进行更新,缓存资源更新后返回给客户端。

// WebKit/Source/WebCore/platform/network/CacheValidation.cpp 
void updateResponseHeadersAfterRevalidation(ResourceResponse& response, const ResourceResponse& validatingResponse) 
{
    // Freshening stored response upon validation: 
    // http://tools.ietf.org/html/rfc7234#section-4.3.4 
    for (const auto& header : validatingResponse.httpHeaderFields()) { 
        // Entity headers should not be sent by servers when generating a 304 
        // response; misconfigured servers send them anyway. We shouldn't allow 
        // such headers to update the original request. We'll base this on the 
        // list defined by RFC2616 7.1, with a few additions for extension headers 
        // we care about. 
        // 是否应该更新请求头 
        if (!shouldUpdateHeaderAfterRevalidation(header.key)) 
            continue; 
        response.setHTTPHeaderField(header.key, header.value); 
    }
}

**参考资料**

1. 《HTTP 权威指南》

2. "HTTP 缓存 - HTTP | MDN"

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ

3. "Cache-Control - HTTP | MDN"

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control

4. "Message Syntax and Routing"

https://tools.ietf.org/html/rfc7230

5. "Semantics and Content"

https://tools.ietf.org/html/rfc7231

6. "Conditional Requests"

https://tools.ietf.org/html/rfc7232

7. "Range Requests"

https://tools.ietf.org/html/rfc7233

8. "Caching"

https://tools.ietf.org/html/rfc7234

9. "Authentication"

https://tools.ietf.org/html/rfc7235

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/IQxMIv5Q4h_6MFlMYZokyA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237228次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录