1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        WebKit 歷史棧緩存策略探索

        共 9787字,需瀏覽 20分鐘

         ·

        2021-07-21 15:47

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

        https://juejin.cn/post/6986283172522622983


        背景


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

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

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


        涉及 WebKit 基礎概念


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

        WebContent 進程

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

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

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

        UIProcess 進程

        應用程序對應的進程。

        初始化 WKWebView 時,需關注的內存結構:

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

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

        WebPageProxy / WebProcessProxy 分別對應了 WebContent 進程的 WebPage / WebProcess。

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

        Networking 進程

        負責網絡相關處理,創(chuàng)建多個 WKWebView 也僅只有一個進程,本文不關注該進程。


        歷史棧緩存策略簡述


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

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

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

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

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

        緩存淘汰策略

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

        WebContent 進程 在切換頁面時,會將當前頁面通過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;
        ...
        }
        ...

        基本可以認為 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ù)量,并且在創(chuàng)建 WebProcessProxy 時通過 IPC 通知到對應的 WebContent 進程去設置 BackForwardCache 的m_maxSize。 

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

        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 進程會調用notifyChanged()通過 IPC 通知到 UIProcess 進程的對應 WebBackForwardCache 去同步狀態(tài):

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


        重定向、跨域場景分析


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

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

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

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

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

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

        processForNavigation內部會決議是否復用 WebProgressProxy,關鍵代碼如下:

        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的一級+二級域名,也就是說目標和來源的 URL 允許三級子域名不同時去復用 Process,比如m.sogou.comwww.sogou.com。此時的時機是發(fā)起網絡請求之前,對該targetURL是否會重定向不得而知,所以這里只和是否跨域有關。

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

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

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

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

        suspendCurrentPageIfPossible會嘗試去緩存當前頁面的信息:

        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時,即客戶端重定向,會立即返回,并不會走到后面的添加歷史棧緩存邏輯。而如果是服務器重定向,在 Networking 進程就會處理,這里其實并未感知到,所以就和常規(guī)的頁面切換一樣會把頁面加入歷史棧緩存。

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

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

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

        如何理解client-side redirect?

        判斷代碼很簡單:

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

        走到這段邏輯的前提是切換頁面時切換了 WebProgressProxy,那目標 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'),果不其然復現(xiàn)了兩者相等的場景。

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


        結論


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

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

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

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

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

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

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


        瀏覽 25
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            印度三级a做爰全过程 | 无遮无挡试看120秒动态图 | 丝袜足交诱惑 | 午夜天堂在线观看 | 男人的天堂2024 | 青青草在线免费视频 | 亚洲干综合 | 极品国产在线 | 国产成人午夜福利 | 女生勿进必湿 |