WebView 和記憶體

WebView 是一項功能強大的元件,可讓您在 Android 應用程式中顯示網頁內容。不過,由於這基本上是功能齊全的瀏覽器引擎 (Chromium),因此記憶體用量相當大,且多重程序架構複雜。

技術背景:多程序架構

在搭載 Android 的新式裝置上,WebView 會使用多程序模型來提升安全性和穩定性。應用程式使用 WebView 時,記憶體會分配給不同程序:

  1. 瀏覽器程序 (應用程式程序):這是應用程式的主要程序。其中包含 Java WebView 物件,以及 Chromium 引擎的「瀏覽器」部分。這個程序會管理 UI、網路要求和 GPU 算繪 (直接與 Android HWUI 算繪管道整合)。與 Chrome 不同,WebView 沒有獨立的 GPU 程序。
  2. 轉譯器程序:這個程序負責剖析 HTML、執行 JavaScript 和版面配置。為確保安全,這項功能與系統的其餘部分隔離。目前,應用程式的所有 WebView 都只會取得一個轉譯器程序 (少數特殊情況除外),這與 Chrome 不同,Chrome 通常會為不同網站使用不同的轉譯器程序。

WebView 架構

對記憶體的重要性

使用 dumpsys meminfo <your_package> 時,您只會看到瀏覽器程序 (您的應用程式程序) 所用的記憶體。Renderer 程序使用的記憶體會分開計算。

在瀏覽器程序中,WebView 記憶體的分配方式如下:

  • Java 堆積:包含 WebView Java 包裝函式和相關物件。
  • 原生堆積:包含 Chromium 瀏覽器引擎的內部資料結構、快取和狀態。請注意,由於使用 PartitionAlloc,部分 WebView 原生分配項目可能不會計入 dumpsys meminfo 的「原生堆積」中,而是顯示在「其他」或「不明」下方。
  • 共用記憶體:用於共用圖形緩衝區和其他資料。這類內容可能無法由 dumpsys meminfo 清楚分類。

疑難排解工具

Chrome DevTools

如要分析 WebView 內部的記憶體 (轉譯器程序),最強大的工具就是 Chrome 開發人員工具。

  1. 在應用程式中啟用 WebView 偵錯功能:

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. 使用 USB 連接裝置。

  3. 在主體機器上開啟 Chrome,然後前往 chrome://inspect/#devices

  4. 找到應用程式,然後按一下「檢查」

  5. 在開發人員工具視窗中,前往「記憶體」分頁,即可擷取堆積快照或記錄 JavaScript 堆積的配置時間軸。

dumpsys meminfo

使用 adb shell dumpsys meminfo --all <package> 查看記憶體用量明細。在輸出內容中尋找 WebView 類別和物件計數。

剖析轉譯器

由於 Renderer 是在獨立程序中執行,因此您無法只剖析應用程式,必須找出 Renderer 程序的 PID。

如果有多個 WebView 處於啟用狀態,請按照下列步驟找出正確的算繪器 PID:

  1. 使用 dumpsys activity

    adb shell dumpsys activity processes <your_package_name>
    

    尋找「mConnections」部分。畫面會顯示ConnectionRecord,指出應用程式已連結至SandboxedProcessService。該程序的 PID 就是您的算繪器。範例:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. 檢查程序名稱:Renderer 程序通常命名為 com.google.android.webview:sandboxed_processX 或類似名稱。如果只有一個應用程式使用 WebView,則可能只會有一個。

取得 PID 後,您可以使用 heapprofd 進行剖析。

WebView 記憶體最佳做法

明確銷毀

應用程式應呼叫 WebView.destroy(),指出何時完成執行個體。

雖然 WebView 會盡量確保執行個體可進行垃圾收集,並自動釋出所有資源,但很難保證 100% 的情況都能做到。即使自動垃圾收集功能正常運作,也可能會大幅延遲,導致應用程式保留資源的時間遠超出預期。

如果應用程式在適當時間 (例如 Activity.onDestroy() 中) 呼叫 WebView.destroy(),保留對 WebView 物件本身的參照,就不會洩漏任何重要原生資源。刪除 WebView 物件後,不一定需要將 Activity 欄位中的參照設為空值,因為系統會在垃圾收集 Activity 時清除這些參照。

練習:動手操作 WebView 記憶體

練習 1:觀察多程序足跡

  1. 啟動 MemoryLab,並對應用程式的記憶體進行基準測量:

    adb shell dumpsys meminfo com.android.memorylab
    

    範例基準 (rango): TOTAL PSS: 18915 KB

  2. 輕觸「啟動 WebView (一般)」

  3. 在 WebView 中,多次輕觸「Allocate JS Memory (1000 DIVs)」

  4. 再次檢查應用程式的記憶體:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. 觀察應用程式程序中的記憶體是否大幅增加 (與基準相比)!這是因為 DOM 元素位於「Renderer Process」

  6. 找出渲染程序:

    adb shell ps -A | grep webview | grep sandboxed
    

    輸出內容範例:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. 檢查渲染器程序的記憶體 (使用 PID):

    adb shell dumpsys meminfo 14227
    
  8. 觀察顯示程序的高 TOTAL PSS。在我們的範例執行中,幾次配置後,記憶體用量就跳到 ~55MB。請注意,JavaScript 分配 (由 V8 引擎處理) 通常會計入 dumpsys meminfo 的「Private Other」或「Unknown」 (mmap) 區段,而不是 Dalvik 堆積。

練習 2:Java 端 WebView 洩漏

常見的錯誤是在靜態欄位或會造成記憶體流失的長期存留物件中,保留 WebView 例項。因為 WebView 物件是「錨點」,會保留原生資源和整個渲染程序,因此洩漏這個物件的代價非常高昂。

WebView 洩漏影響

  1. MemoryLab 中,輕觸「Launch WebView (Java Leak)」
  2. 頁面載入後,活動會自動關閉 (模擬重複導覽和洩漏累積)。
  3. 輕觸按鈕 4 次。
  4. 查看應用程式中的 WebView 執行個體數量:

    adb shell dumpsys meminfo com.android.memorylab
    

    在底部找到「物件」部分。您會看到 WebViews 的計數增加到 4。

    rango 上的輸出內容範例 (4 個洩漏的執行個體):

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. 擷取記憶體快照資料,並使用 AHAT 找出記憶體流失問題。如果路徑中沒有 ahat,可以從 Android 樹狀結構建構:

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. 在 AHAT 網頁介面 (localhost:8888) 中,按一下頂端選單中的「allocations」(配置) 連結 (或「sites」(網站)),即可查看整體記憶體用量。

    AHAT 分配

  7. 搜尋 android.webkit.WebView 類別。按一下執行個體計數,即可查看所有即時執行個體。清單中應該會顯示多個執行個體。

    AHAT WebView 執行個體

  8. 按一下其中一個外洩的 WebView 執行個體。向下捲動至「Sample Path from GC Root」(從 GC 根目錄取得的範例路徑) 部分。您會看到該工作由 com.android.memorylab.WebViewActivity 中的 sLeakedWebViews 清單保留。

    AHAT Path to GC Root


← 原生 | ↑ 向上 | 應用程式代碼 →