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 執行個體
如要確保 Activity 或 Fragment 遭到破壞時能正常關閉並釋出資源,請採取下列做法:
- 從父項容器 (
ViewGroup) 移除WebView。 - 停止載入中的內容,並清除瀏覽記錄。
- 呼叫
destroy()。 - 清除對
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 程序使用的記憶體。大量網頁內容 主要會導致渲染程序出現尖峰。
即時物件計數 (
WebViews、Activities、Views):記憶體中保留的有效 UI、Context 和WebView執行個體數量。追蹤這些項目可判斷記憶體成長是否是由於保留的 Java 參照或僅限原生的配置所致。私人其他和原生堆積:在
dumpsys meminfo中,原生 C/C++ 配置和自訂記憶體對應 (例如 ChromiumPartitionAlloc或嵌入式 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 診斷,主要著重於 Activities 和 WebViews。
重複執行目標使用者互動 (例如開啟及關閉網頁畫面),並比較計數:
例項洩漏:如果
WebViews或Activities在每次導覽時遞增,且不會返回基準線,表示應用程式洩漏 JavaWebView例項或主機Activity(例如,因為缺少ViewGroup.removeView()或保留的監聽器參照)。因為洩漏的Activity會將整個檢視區塊樹狀結構和解碼的圖片資源固定在記憶體中,因此重複造訪會快速耗盡 Java 堆積,並導致OutOfMemoryError崩潰。原生或 DOM 洩漏:如果
WebViews和Activities維持不變,但程序 RSS 總計和「私人其他」持續攀升,則洩漏源自未發布的原生資源、DOM 元素或 JavaScript 引擎繫結。由於這些配置位於原生記憶體中,並略過 ART 垃圾收集器,因此標準 Java 洩漏偵測工具無法偵測到這些配置,且這些配置會持續累積,直到作業系統終止應用程式為止。
使用 CLI 分析獨立的 Renderer 程序
使用應用程式的套件名稱執行 dumpsys meminfo 時,只會輸出主要主機程序的記憶體。如要檢查網頁的轉譯位置,也就是隔離的轉譯器程序,請按照下列步驟操作:
找出獨立轉譯器服務的程序 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:...}使用 PID 檢查轉譯器程序的記憶體細目:
adb shell dumpsys meminfo <var>RENDERER_PID</var>檢查主機應用程式程序,評估瀏覽器端的資源用量:
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 層還是原生引擎:
找出洩漏類型 (Java 與原生): 在重複的使用者轉場 (例如開啟及關閉網頁文章,或滑動瀏覽動態消息) 前後,執行
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"。- 觀察:如果
Activities和WebViews計數保持穩定 (例如 1 到 2 個有效例項),表示應用程式不會洩漏Activity背景資訊或 JavaWebView例項。
- 觀察:如果
評估互動期間的記憶體差異 (時間序列追蹤): 在多個使用者互動期間擷取
dumpsys meminfo快照,計算每次轉換的分配率:- 觀察結果:Java 堆積保持在上限且運作正常 (使用期間會尖峰,垃圾收集後會下降),但「Private Other」和「Native Heap」會穩定上升,每次轉移都會增加數 MB。這證明記憶體洩漏完全發生在 ART 執行階段以外的原生記憶體中。標準 Java 堆積傾印 (
.hprof) 不會顯示任何問題。
- 觀察結果:Java 堆積保持在上限且運作正常 (使用期間會尖峰,垃圾收集後會下降),但「Private Other」和「Native Heap」會穩定上升,每次轉移都會增加數 MB。這證明記憶體洩漏完全發生在 ART 執行階段以外的原生記憶體中。標準 Java 堆積傾印 (
檢查匿名記憶體對應:使用 ADB 檢查程序記憶體對應 (請參閱「檢查記憶體對應和配置」):
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- 觀察:記憶體成長集中在
[anon:partition_alloc]或嵌入式指令碼引擎堆積中,且 JNI 全域參照緩慢增加。這表示雖然 Java 檢視區塊已取代,但基礎原生網頁物件或 JavaScript 繫結並未發布。
- 觀察:記憶體成長集中在
補救措施:
- 請確保每個回收或捨棄的
WebView都會明確停止作用中的指令碼 (stopLoading())、清除記錄,並呼叫destroy()。 - 拆除與已關閉檢視區塊相關聯的自訂 JavaScript 橋接器回呼或 JNI 全域參照。
- 確認
Private Other和程序 RSS 在導覽轉換後穩定。
- 請確保每個回收或捨棄的
其他資源
如要進一步瞭解如何偵錯及剖析記憶體和 WebView 效能,請參閱下列資源: