管理及診斷 WebView 記憶體

WebView 會跨多個程序執行原生程式碼,在 Android 應用程式中算繪網路內容。如果未管理 WebView 執行個體,可能會導致記憶體流失、記憶體不足 (OOM) 異常終止,以及應用程式效能降低。

本文將說明 WebView 多程序記憶體模型、如何妥善管理其生命週期以避免流失,並提供診斷記憶體問題的實用工作流程。

瞭解 WebView 記憶體架構

如要有效管理 WebView 記憶體,請瞭解 Android 如何為網路內容分配資源:

  • 多程序執行:在 Android 8.0 (API 級別 26) 以上版本中,WebView 會在多個程序中將網路內容與應用程式的核心功能分開 (在 RAM 較低的裝置上,可能會改用單一程序):

    • 主機 (瀏覽器) 程序:執行 Activity 和 Java 或 Kotlin 程式碼的主要應用程式程序。
    • 獨立轉譯器程序:獨立的沙箱程序 (SandboxedProcessService),用於剖析 HTML 和 CSS、執行 JavaScript,以及轉譯網頁。
  • 原生記憶體用量:大部分 WebView 記憶體 (包括算繪圖形、DOM 樹狀結構和 JavaScript 執行階段記憶體) 都是分配在原生記憶體中,而非 Java 堆積。Java 記憶體快照資料 (.hprof) 只會顯示輕量 Java 包裝函式物件,不會擷取網路內容實際使用的記憶體。

  • 原生記憶體對系統的影響:Java 堆積分配量受限於應用程式的 maxHeap 限制,且會因 OutOfMemoryError 快速失敗,但原生記憶體可能會默默增加到數 GB。當未釋出的原生記憶體填滿實體 RAM 和交換空間 (zRAM) 時,Android 的低記憶體終止程式 (LMK) 就會開始終止背景程序,以回收記憶體。這會降低裝置的整體多工處理效能,最終導致前景應用程式終止。

管理 WebView 生命週期

適當的生命週期管理對於防止記憶體流失至關重要。常見的錯誤是假設從版面配置中移除 WebView,或讓 Activity 完成,會自動釋放其記憶體。

為確保完整清除 Java 情境參照和原生算繪資源,您必須在主機元件的生命週期 (例如 onDestroy()) 中明確安排拆除序列,停止執行中的網頁、從容器中卸除檢視畫面,並釋出原生繫結。

清除 WebView 執行個體

如要確保在 Activity 或 Fragment 刪除時能順利關閉並釋出資源,請按照下列步驟操作:

  1. 從上層容器 (ViewGroup) 中移除 WebView。
  2. 停止載入並清除導覽記錄。
  3. 呼叫 destroy()。
  4. 清除 null 的參照。

以下範例說明如何正確清除 WebView:

Kotlin

override fun onDestroy() {
    myWebView?.let {
        // Remove the WebView from its parent ViewGroup.
        (it.parent as? ViewGroup)?.removeView(it)
        // Stop active loading and clear history.
        it.stopLoading()
        it.clearHistory()
        // Destroy the instance.
        it.destroy()
    }
    myWebView = null
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    if (myWebView != null) {
        // Remove the WebView from its parent ViewGroup.
        if (myWebView.getParent() instanceof ViewGroup) {
            ((ViewGroup) myWebView.getParent()).removeView(myWebView);
        }
        // Stop active loading and clear history.
        myWebView.stopLoading();
        myWebView.clearHistory();
        // Destroy the instance.
        myWebView.destroy();
    }
    myWebView = null;
    super.onDestroy();
}

瞭解銷毀後的記憶

呼叫 destroy() 時,系統會釋出 Activity 情境、清理檢視區塊階層,並停止網頁背景工作。不過,您可能會發現程序的實體記憶體 (常駐集大小) 並未立即降至 WebView 之前的基準。

這是正常現象。原生執行階段快取、共用程式庫和分配的記憶體頁面會保留在程序中,直到作業系統回收或程序終止。destroy() 的主要目標是防止使用者在網頁畫面中進出時,累積 Activity 記憶體流失。

重要偵錯指標

分析 WebView 記憶體消耗量時,請著重於下列指標:

  • 常駐集大小 (RSS):對應至程序的實體 RAM 總量,包括共用程式碼和程式庫 (在 Android Studio 分析器中標示為「Total」)。

  • 匿名 RSS (RssAnon):由程序直接配置的記憶體,並非由磁碟上的檔案支援 (例如原生堆積和 JavaScript 執行階段記憶體配置)。這代表網路內容的主要記憶體成本 (在 Android Studio 分析器中標示為「Allocated」)。

  • 私人記憶體用量 (PMF):匿名 RSS 和交換空間 (zRAM) 的總和。PMF 反映了應用程式對系統造成的實際不可收回記憶體負擔。

  • 瀏覽器 PMF 與轉譯器 PMF:應用程式主要程序使用的記憶體,與獨立轉譯器程序使用的記憶體。大型網路內容主要會導致轉譯器程序出現尖峰。

  • Live Object Counts (WebViews、Activities、Views):記憶體中保留的有效 UI、Context 和 WebView 執行個體數量。追蹤這些項目,可判斷記憶體成長是否由保留的 Java 參照或僅限原生的配置所造成。

  • Private Other 和 Native Heap:在 dumpsys meminfo 中,原生 C/C++ 配置和自訂記憶體對應 (例如 Chromium PartitionAlloc 或嵌入式 JavaScript 執行階段堆積) 會顯示在 Native Heap 和 Private Other 下方,而非 Java Heap。

如要進一步瞭解程序記憶體計數器及其類別,請參閱程序記憶體詞彙表。

實用的診斷工作流程

由於 WebView 會跨多個程序運作並分配原生記憶體,因此請使用下列工具和技術檢查其使用量:

剖析和診斷工具

如要檢查記憶體配置及診斷流失問題,請使用下列工具:

  • Android Studio 記憶體分析器:使用記憶體分析器,以視覺化方式呈現原生配置、追蹤記憶體類別隨時間的變化,以及偵測畫面轉換時的 Activity 洩漏情形。

  • 使用 Perfetto 追蹤記憶體:使用 Perfetto 記錄系統層級的記憶體計數器 (例如 RSS 和匿名 RSS),觀察整體記憶體成長情況。請注意,WebView 原生引擎分配不會在 Perfetto 的堆積快照工具中產生呼叫堆疊。使用 Chrome 開發人員工具,檢查網路內容中的 JavaScript 堆積快照和 DOM 分配。

檢查使用中的物件數量

如要判斷記憶體成長是否由保留的 Java 架構物件 (例如 UI 元件) 或原生分配造成,請檢查 dumpsys meminfo 的 Objects 區段:

adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"

輸出內容會顯示即時物件數量:

 Objects
               Views:     142         ViewRootImpl:        1
         AppContexts:       3           Activities:        1
              Assets:      12        AssetManagers:        0
       Local Binders:      32        Proxy Binders:       45
       Parcel memory:      15         Parcel count:       30
    Death Recipients:       2             WebViews:        1

這個部分會顯示使用中的架構物件、IPC 控制代碼和 Parcel 分配數量。進行 WebView 診斷時,請著重於 Activities 和 WebViews。

重複執行目標使用者互動 (例如開啟和關閉網頁畫面),並比較計數:

  • 執行個體流失:如果 WebViews 或 Activities 在每次導覽時遞增,且未返回基準,表示應用程式正在流失 Java WebView 執行個體或主機 Activity (例如缺少 ViewGroup.removeView() 或保留的事件監聽器參照)。由於外洩的 Activity 會將整個檢視畫面樹狀結構和解碼的圖片資源固定在記憶體中,重複造訪會快速耗盡 Java 堆積,並導致 OutOfMemoryError 異常終止。

  • 原生或 DOM 洩漏:如果 WebViews 和 Activities 保持不變,但程序 RSS 總量和「Private Other」持續攀升,則洩漏源自未釋出的原生資源、DOM 元素或 JavaScript 引擎繫結。由於這些配置位於原生記憶體中,且會略過 ART 垃圾收集器,因此標準 Java 洩漏偵測工具無法偵測到這些配置,且會持續累積,直到作業系統終止應用程式為止。

使用 CLI 分析獨立轉譯器程序

使用應用程式套件名稱執行 dumpsys meminfo,只會輸出主要主機程序的記憶體。如要檢查轉譯網頁的隔離轉譯器程序,請按照下列步驟操作:

  1. 找出隔離轉譯器服務的程序 ID (PID):

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    輸出內容會顯示隔離的程序記錄及其 PID RENDERER_PID (例如 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. 使用轉譯器程序的 PID 檢查記憶體細目:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. 檢查主機應用程式程序,評估瀏覽器端足跡:

    adb shell dumpsys meminfo <var>PACKAGE_NAME</var>

檢查記憶體對應和配置

如要查看哪些原生子系統或配置器占用匿名記憶體,請檢查程序記憶體對應:

adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"

下表列出常見的匿名記憶體標記,以及這些標記與記憶體成長的關聯:

記憶標記 子系統 與應用程式和網路內容的關聯性 記憶體增加的常見原因?
[anon:partition_alloc] Chromium PartitionAlloc WebView中 DOM 樹狀結構、轉譯緩衝區、V8 JavaScript 堆積和 WebAssembly 執行的分配。 是 (高):載入大型網頁、媒體豐富的 DOM,或無法在捨棄的 WebView 執行個體上呼叫 destroy(),會直接增加這個標記。
[anon:scudo...] 或 [anon:libc_malloc] Android 原生堆積分配器 (Scudo / jemalloc) NDK 程式庫、JNI 橋接和原生圖形管道使用的一般 C/C++ 原生分配。 是 (中到高):如果原生 JNI 包裝函式或第三方 C++ 依附元件在導覽過程中保留未釋放的分配,就會發生增長。
[anon:...] (例如 [anon:quickjs_heap...]) 自訂指令碼或原生執行階段 嵌入式 JavaScript 引擎、自訂 WebAssembly 執行階段,或自訂原生緩衝區集區。 是 (視情況而定):常見於混合式應用程式,這類應用程式會與原生檢視畫面一起執行指令碼引擎,且無法清除執行階段繫結。

應用程式內記憶體 API 的限制

應用程式內記憶體 API (例如 Debug.getMemoryInfo 或 ActivityManager.getProcessMemoryInfo) 只會評估呼叫程序。在多程序模式下,這些 API 無法擷取獨立轉譯器程序耗用的記憶體。如要準確評估總記憶體,請使用 dumpsys meminfo、Perfetto 或 Android Studio Profiler 等系統工具。

在混合式應用程式中分類高記憶體用量

在診斷重複 WebView 互動 (例如開啟網頁連結或瀏覽網頁式動態消息) 期間,如果記憶體用量不明原因增加,請使用下列分類工作流程,找出記憶體流失是源自 Java 層還是原生引擎:

  1. 找出洩漏類型 (Java 或原生):在使用者重複轉換前後 (例如開啟和關閉網路文章,或滑動瀏覽動態消息),執行 dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"。

    • 觀察:如果 Activities 和 WebViews 計數保持穩定 (例如 1 到 2 個有效執行個體),表示應用程式不會洩漏 Activity 結構定義或 Java WebView 執行個體。
  2. 評估互動之間的記憶體差異 (時間序列追蹤):擷取多個使用者互動的 dumpsys meminfo 快照,計算每次轉換的分配率:

    • 觀察結果:Java 堆積維持在上限內且狀態良好 (使用期間會激增,垃圾收集後會下降),但「Private Other」和「Native Heap」在每次轉換時都會穩定攀升數 MB。這證明流失問題完全發生在 ART 執行階段以外的原生記憶體中。標準 Java 記憶體快照資料 (.hprof) 不會顯示任何問題。
  3. 檢查匿名記憶體對應:使用 ADB 檢查程序記憶體對應 (請參閱「檢查記憶體對應和分配」):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • 觀察結果:記憶體成長集中在 [anon:partition_alloc] 或嵌入式指令碼引擎堆積,且 JNI 全域參照緩慢增加。這表示雖然 Java 檢視畫面已替換,但基礎原生網頁物件或 JavaScript 繫結並未釋出。
  4. 修復:

    • 請確保每個回收或捨棄的 WebView 都明確停止執行中的指令碼 (stopLoading())、清除歷史記錄,並呼叫 destroy()。
    • 拆除與已關閉檢視畫面相關聯的自訂 JavaScript 橋接回呼或 JNI 全域參照。
    • 確認 Private Other 和程序 RSS 在導覽轉換後穩定。

其他資源

如要進一步瞭解如何對記憶體和 WebView 效能進行偵錯及分析,請參閱下列資源: