<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          WebKit 歷史棧緩存策略探索

          共 9787字,需瀏覽 20分鐘

           ·

          2021-07-21 15:47

          文章由作者 波兒菜 授權(quán)發(fā)布,原文發(fā)表于掘金,點擊閱讀原文查看作者更多精彩文章

          https://juejin.cn/post/6986283172522622983


          背景


          在一個新的業(yè)務(wù)方案實施過程中,發(fā)現(xiàn)數(shù)據(jù)上存在較大的差異,而這個差異是 WKWebView 的應(yīng)用方式不同帶來的。通過手工測試和上層代碼能模糊的解釋一些現(xiàn)象,但想要鐵板釘釘?shù)淖C明這些現(xiàn)象就得從 WebKit 源碼去分析,便于將來準(zhǔn)確的決策這些場景是對齊還是變更策略,或許還能從技術(shù)角度發(fā)現(xiàn)一些優(yōu)化點從而反哺業(yè)務(wù)。

          現(xiàn)在面臨兩個問題:
          1. WebKit 的常規(guī)歷史棧緩存策略是怎樣的?
          2. WebKit 在跨域、重定向等場景下,歷史棧緩存策略有怎樣的變化?

          其中,第 2 點是比較詭異的,不看 WebKit 源碼的情況下難以找到規(guī)律,下不了定論。


          涉及 WebKit 基礎(chǔ)概念


          App 內(nèi) WKWebView 運行時有三種進程協(xié)同工作:UIProcess 進程、WebContent 進程、Networking 進程。

          WebContent 進程

          網(wǎng)頁 DOM 及 JS 所處進程。進程數(shù)量可能有多個,取決于一些細節(jié)策略。

          在該進程初始化時會創(chuàng)建唯一的 WebProcess 實例,并且作為 IPC::Connection 的 client,與其它進程通信的代理,需要關(guān)注的內(nèi)存結(jié)構(gòu):

          -m_frameMap(WebFrame)(樹結(jié)構(gòu),在創(chuàng)建 WebPage 時創(chuàng)建)
          -m_pageMap(WebPage)(UIProcess 進程創(chuàng)建 WebPageProxy 時 IPC 通知過來創(chuàng)建)
          -m_mainFrame(WebFrame)

          UIProcess 進程

          應(yīng)用程序?qū)?yīng)的進程。

          初始化 WKWebView 時,需關(guān)注的內(nèi)存結(jié)構(gòu):

          -_processPool(WebProcessPool) 
          -m_processes (WebProcessProxy 數(shù)組)
          -_page(WebPageProxy,通過 WebProcessProxy 實例創(chuàng)建,WKWebView 實例唯一一個)
          -m_process(WebProcessProxy,會動態(tài)切換)

          初始化后,WebPageProxy 做為了 IPC::Connection 的 client,與其它進程通信的代理。

          WebPageProxy / WebProcessProxy 分別對應(yīng)了 WebContent 進程的 WebPage / WebProcess。

          WebProcessPool(關(guān)聯(lián) WKWebViewConfiguration 的 WKProcessPool 對象)抽象了 WebContent 進程池,也就是說一個 WKWebView 是可以對應(yīng)多個 WebContent 進程。

          Networking 進程

          負(fù)責(zé)網(wǎng)絡(luò)相關(guān)處理,創(chuàng)建多個 WKWebView 也僅只有一個進程,本文不關(guān)注該進程。


          歷史棧緩存策略簡述


          WKWebView 可以通過goBack/goForward接口進行歷史棧的切換,切換時有一套緩存策略,命中時能省去請求網(wǎng)絡(luò)的時間。

          WebContent 進程的 BackForwardCache 是一個單例,管理著歷史棧緩存。

          UIProcess 進程的 WebProcessPool 抽象了 WebContent 進程池,每一個 WebProcessPool 都有唯一的 WebBackForwardCache 表示歷史棧緩存,對應(yīng)著 WebContent 進程池子里的各個 BackForwardCache 單例。

          BackForwardCache 用了一個有序 hash 表存儲緩存元素,并設(shè)定了最大緩存數(shù)量:

          ListHashSet<RefPtr<HistoryItem>> m_items;
          unsigned m_maxSize {0};

          緩存淘汰策略

          BackForwardCache 和 WebBackForwardCache 的策略基本一致,現(xiàn)以 BackForwardCache 為例說明。

          WebContent 進程 在切換頁面時,會將當(dāng)前頁面通過BackForwardCache::singleton().addIfCacheable(...);添加緩存:

          bool BackForwardCache::addIfCacheable(HistoryItem& item, Page* page) {
          ...
          item.setCachedPage(makeUnique<CachedPage>(*page));
          item.m_pruningReason = PruningReason::None;
          m_items.add(&item);
          ...
          }

          最大緩存數(shù)量具體代碼如下:

          namespace WebKit {
          void calculateMemoryCacheSizes(...) {
          uint64_t memorySize = ramSize() / MB;
          ...
          // back/forward cache capacity (in pages)
          if (memorySize >= 512)
          backForwardCacheCapacity = 2;
          else if (memorySize >= 256)
          backForwardCacheCapacity = 1;
          else
          backForwardCacheCapacity = 0;
          ...
          }
          ...

          基本可以認(rèn)為 iPhone 上一個 WebContent 進程最多兩個歷史棧緩存。

          在歷史棧緩存發(fā)生變化的地方,都會命中一個修剪邏輯:

          void BackForwardCache::prune(PruningReason pruningReason) {
          while (pageCount() > maxSize()) {
          auto oldestItem = m_items.takeFirst();
          oldestItem->setCachedPage(nullptr);
          oldestItem->m_pruningReason = pruningReason;
          }
          }

          可以看出是實現(xiàn)了一個簡單的 LRU 淘汰策略。

          最大緩存數(shù)量

          前面說到 WebContent 進程最多兩個歷史棧緩存,實際上這個緩存數(shù)量是 UIProcess 進程決定的。在 UIProcess 進程中,WebProcessPool 初始化 WebBackForwardCache 時會設(shè)置最大緩存數(shù)量,并且在創(chuàng)建 WebProcessProxy 時通過 IPC 通知到對應(yīng)的 WebContent 進程去設(shè)置 BackForwardCache 的m_maxSize。 

          WebProcessPool 的 WebBackForwardCache 對應(yīng)了 WebContent 進程池里每一個的 BackForwardCache 單例,是一個一對多的模式,WebBackForwardCache 在修剪緩存元素析構(gòu)時會自動觸發(fā) IPC 通知到 WebContent 進程去清理對應(yīng)緩存:

          WebBackForwardCacheEntry::~WebBackForwardCacheEntry() {
          if (m_backForwardItemID && !m_suspendedPage) {
          auto& process = this->process();
          process.sendWithAsyncReply(Messages::WebProcess::ClearCachedPage(m_backForwardItemID), [] { });
          }
          }

          所以緩存最大數(shù)量取決于 WebProcessPool 的數(shù)量,一個 WebProcessPool 就最多兩個歷史棧緩存,不管它的進程池有多少個 WebContent。

          狀態(tài)同步

          在歷史棧緩存狀態(tài)發(fā)生變化時,WebContent 進程會調(diào)用notifyChanged()通過 IPC 通知到 UIProcess 進程的對應(yīng) WebBackForwardCache 去同步狀態(tài):

          notifyChanged() 最終調(diào)用到:
          static void WK2NotifyHistoryItemChanged(HistoryItem& item) {
          WebProcess::singleton().parentProcessConnection()->send(Messages::WebProcessProxy::UpdateBackForwardItem(toBackForwardListItemState(item)), 0);
          }


          重定向、跨域場景分析


          請求數(shù)據(jù)前決議階段

          WKWebView 在切換頁面時,真正發(fā)起網(wǎng)絡(luò)請求或使用緩存之前,會進行一些決議,大家熟知的 WKNavigationDelegate 的-webView:decidePolicyForNavigationAction:decisionHandler:就是在這個流程之中:

          void WebPageProxy::decidePolicyForNavigationAction(...) {
          ...
          auto listener = ... {
          ...
          receivedNavigationPolicyDecision(policyAction, navigation.get(), processSwapRequestedByClient, frame, WTFMove(policies), WTFMove(sender));
          ...
          }
          ...
          //這個 m_navigationClient 和上層設(shè)置的 WKNavigationDelegate 代理關(guān)聯(lián),即會調(diào)用到 `-webView:decidePolicyForNavigationAction:decisionHandler:
          //上層調(diào)用 decisionHandler(WKNavigationActionPolicyAllow) 后,會調(diào)用上面的 listener 關(guān)聯(lián)的閉包,執(zhí)行后續(xù)邏輯
          m_navigationClient->decidePolicyForNavigationAction(*this, WTFMove(navigationAction), WTFMove(listener), process->transformHandlesToObjects(userData.object()).get());
          ...
          }

          重點關(guān)注的是后續(xù)的這個方法:

          void WebPageProxy::receivedNavigationPolicyDecision(...) {
          ...
          //注:這里改寫了源碼
          Ref<WebProcessProxy>&& processForNavigation = process().processPool().processForNavigation(...);
          ...
          bool shouldProcessSwap = processForNavigation.ptr() != sourceProcess.ptr();
          if (shouldProcessSwap) {
          ...
          continueNavigationInNewProcess(...);
          }
          ...
          }

          這里做了一個關(guān)鍵操作是獲取 WebProcessProxy,然后判斷是否和來源的sourceProcess相同,如果不同則會用另外的 WebProcessProxy 去處理這個 Navigation。
          當(dāng)發(fā)生了 WebProcessProxy 切換,continueNavigationInNewProcess里面會創(chuàng)建一個 ProvisionalPageProxy 并關(guān)聯(lián)到 WebPageProxy 的 m_provisionalPage 實例變量,標(biāo)記這里有一次切換 WebProcessProxy 的操作。

          processForNavigation內(nèi)部會決議是否復(fù)用 WebProgressProxy,關(guān)鍵代碼如下:

          void WebProcessPool::processForNavigationInternal(...) {
          ...
          if (!sourceURL.isValid() || !targetURL.isValid() || sourceURL.isEmpty() || sourceURL.protocolIsAbout() || targetRegistrableDomain.matches(sourceURL))
          //域名相同,返回原始的 WebProgressProxy
          return completionHandler(WTFMove(sourceProcess), nullptr, "Navigation is same-site"_s);
          ...
          //域名不同,創(chuàng)建新的 WebProgressProxy 返回
          String reason = "Navigation is cross-site"_s;
          return completionHandler(createNewProcess(), nullptr, reason);
          }

          targetRegistrableDomain 是targetURL的一級+二級域名,也就是說目標(biāo)和來源的 URL 允許三級子域名不同時去復(fù)用 Process,比如m.sogou.comwww.sogou.com。此時的時機是發(fā)起網(wǎng)絡(luò)請求之前,對該targetURL是否會重定向不得而知,所以這里只和是否跨域有關(guān)。

          UIProcess 進程中的 WebProgressProxy 對 WebContent 進程的映射,不考慮 WebContent 的復(fù)用機制,基本可以認(rèn)為一個 WebProgressProxy 對應(yīng)一個進程。如果前后兩個頁面是兩個不同的 WebContent 進程,且沒有重定向操作,調(diào)用goBack/goForward時也能平滑的切換,并且分別復(fù)用到各自 WebContent 進程的歷史棧緩存。

          頁面數(shù)據(jù)返回階段

          前面提到,如果此次切換頁面會切換 WebProgressProxy,WebPageProxy 內(nèi)部就會創(chuàng)建一個 ProvisionalPageProxy 變量。在切換頁面拉取到網(wǎng)絡(luò)數(shù)據(jù)或者讀取到緩存數(shù)據(jù)時,會進行提交:

          void WebPageProxy::commitProvisionalPage(...) {
          ...
          //嘗試緩存當(dāng)前頁面信息
          bool didSuspendPreviousPage = navigation ? suspendCurrentPageIfPossible(...) : false;
          //清理當(dāng)前頁面信息,m_process 就是當(dāng)前的 WebProcessProxy
          m_process->removeWebPage(...);
          //頁面信息切換到新的 m_provisionalPage
          //比如把 WebPageProxy 標(biāo)識當(dāng)前 WebProcessProxy 的 m_process 變量設(shè)置為 provisionalPage->process()
          swapToProvisionalPage(std::exchange(m_provisionalPage, nullptr));
          ...
          }

          suspendCurrentPageIfPossible會嘗試去緩存當(dāng)前頁面的信息:

          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;
          }
          ...
          //創(chuàng)建 SuspendedPageProxy 變量,此時 m_suspendedPageCount 的值會加一
          auto suspendedPage = makeUnique<SuspendedPageProxy>(*this, m_process.copyRef(), *mainFrameID, shouldDelayClosingUntilFirstLayerFlush);
          m_lastSuspendedPage = makeWeakPtr(*suspendedPage);
          ...
          //添加進歷史棧緩存
          backForwardCache().addEntry(*fromItem, WTFMove(suspendedPage));
          ...
          }

          可以看到源碼中的注釋,在發(fā)生了client-side redirect時,即客戶端重定向,會立即返回,并不會走到后面的添加歷史棧緩存邏輯。而如果是服務(wù)器重定向,在 Networking 進程就會處理,這里其實并未感知到,所以就和常規(guī)的頁面切換一樣會把頁面加入歷史棧緩存。

          看看更多的處理代碼,發(fā)現(xiàn)若沒有走到這個方法后面的邏輯讓m_suspendedPageCount計數(shù)加一,commitProvisionalPage函數(shù)里面m_process->removeWebPage(...)會調(diào)用到如下邏輯:

          void WebProcessProxy::shutDown() {
          ...
          //m_processPool 是裝有 WebProcessProxy 集合的 WebProcessPool
          m_processPool->disconnectProcess(this);
          ...
          }
          void WebProcessPool::disconnectProcess(WebProcessProxy* process) {
          ...
          //這里就會清理掉 m_backForwardCache 里面和當(dāng)前 process 關(guān)聯(lián)的歷史棧緩存了
          //m_backForwardCache 是 WebBackForwardCache 類型,一個 WebProcessPool 唯一一個
          m_backForwardCache->removeEntriesForProcess(*process);
          ...
          }

          它會清理當(dāng)前 WebProcessProxy 的所有歷史棧緩存,而不會影響到其它 WebProcessPool 或 WebProcessProxy。

          如何理解client-side redirect

          判斷代碼很簡單:

          fromItem && fromItem == m_backForwardList->currentItem()

          走到這段邏輯的前提是切換頁面時切換了 WebProgressProxy,那目標(biāo) URL 就得跨域,比如從www.a.comwww.b.com,到這里表現(xiàn)如下:

          fromItem : www.a.com
          currentItem : www.b.com

          那何時才能讓兩者相等?
          推測可能是fromItem被強制更改,考慮到 JS window.location對象的replace()函數(shù)有較大嫌疑,測試在www.a.com頁面執(zhí)行window.location.replace('www.b.com'),果不其然復(fù)現(xiàn)了兩者相等的場景。

          這么一看 WebKit 的處理似乎是合理的,因為replace()前的頁面已經(jīng)回不去了,但不知為何直接簡單粗暴的干掉replace()前的頁面歸屬的 WebProgressProxy 關(guān)聯(lián)的所有歷史棧緩存,可能 WebKit 這部分邏輯有優(yōu)化空間,后續(xù)有空再關(guān)注下。 


          結(jié)論


          現(xiàn)在可以回答文章開頭的疑惑了。

          • WebKit 的常規(guī)歷史棧緩存策略是怎樣的?

          限制最大緩存數(shù)量為兩個的 LRU 淘汰算法。

          • WebKit 在跨域、重定向等場景下,歷史棧緩存策略有怎樣的變化?

          WKWebView 切換頁面時,發(fā)生cross-site + client-side redirect 時會清理當(dāng)前 WebProgressProxy 關(guān)聯(lián)的所有歷史棧緩存,后續(xù)切換到這些歷史棧時都需要重新請求網(wǎng)絡(luò)。

          這種場景用戶切歷史棧時重新拉取網(wǎng)絡(luò),一般會卡住好幾秒,所以理論上應(yīng)該避免這種現(xiàn)象發(fā)生,盡量利用 WebKit 的緩存機制提高用戶體驗。給 Web 開發(fā)同學(xué)的建議就是,在跨域場景盡量避免使用window.location.replace()去重定向頁面,可以使用服務(wù)器重定向,或者前置頁面旁路上報等方案替代。

          另外注意的是,觸發(fā)這種場景后,會讓歷史棧訪問量增加,所以在服務(wù)訪問量相關(guān)指標(biāo)數(shù)據(jù)分析層面這是一個值得關(guān)注的重要變量。


          瀏覽 25
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色婷婷精品国产免费 | 亚洲欧洲日本不卡视频在线观看 | 午夜AV福利电影 | 日本无码操逼视频 | 欧美精品成人一区二区三区四区 |