管理及診斷 WebView 記憶體

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

本文說明 WebView 多程序記憶體模型,並介紹如何妥善管理生命週期以防止洩漏,以及提供實用的工作流程來診斷記憶體問題。

瞭解 WebView 記憶體架構

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

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

    • 主機 (瀏覽器) 程序:主要應用程式程序,用於執行 Activity 和 Java/Kotlin 程式碼。
    • 獨立的 Renderer 程序:獨立的沙箱程序 (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 執行個體

如要確保 ActivityFragment 遭到破壞時能正常關閉並釋出資源,請採取下列做法:

  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 Profiler 中標示為「總計」)。

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

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

  • 瀏覽器 PMF 與 Renderer PMF:應用程式主程序使用的記憶體,與獨立 Renderer 程序使用的記憶體。大量網頁內容 主要會導致渲染程序出現尖峰。

  • 即時物件計數 (WebViewsActivitiesViews):記憶體中保留的有效 UI、Context 和 WebView 執行個體數量。追蹤這些項目可判斷記憶體成長是否是由於保留的 Java 參照或僅限原生的配置所致。

  • 私人其他和原生堆積:dumpsys meminfo 中,原生 C/C++ 配置和自訂記憶體對應 (例如 Chromium PartitionAlloc 或嵌入式 JavaScript 執行階段堆積) 會顯示在「原生堆積」和「私人其他」下方,而不是「Java 堆積」。

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

實用的診斷工作流程

由於 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 診斷,主要著重於 ActivitiesWebViews

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

  • 例項洩漏:如果 WebViewsActivities 在每次導覽時遞增,且不會返回基準線,表示應用程式洩漏 Java WebView 例項或主機 Activity (例如,因為缺少 ViewGroup.removeView() 或保留的監聽器參照)。因為洩漏的 Activity 會將整個檢視區塊樹狀結構和解碼的圖片資源固定在記憶體中,因此重複造訪會快速耗盡 Java 堆積,並導致 OutOfMemoryError 崩潰。

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

使用 CLI 分析獨立的 Renderer 程序

使用應用程式的套件名稱執行 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.getMemoryInfoActivityManager.getProcessMemoryInfo) 只會測量呼叫程序。在多程序模式下,這些 API 無法擷取獨立轉譯器程序耗用的記憶體。如要準確評估總記憶體,請使用 dumpsys meminfo、Perfetto 或 Android Studio Profiler 等系統工具。

在混合式應用程式中分類記憶體用量偏高的問題

診斷重複WebView互動 (例如開啟網頁連結或瀏覽網頁動態消息) 期間發生的不明記憶體成長時,請使用下列分類工作流程,判斷記憶體洩漏是源自 Java 層還是原生引擎:

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

    • 觀察:如果 ActivitiesWebViews 計數保持穩定 (例如 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 效能,請參閱下列資源: