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 刪除時能順利關閉並釋出資源,請按照下列步驟操作:
- 從上層容器 (
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 分析器中標示為「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++ 配置和自訂記憶體對應 (例如 ChromiumPartitionAlloc或嵌入式 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在每次導覽時遞增,且未返回基準,表示應用程式正在流失 JavaWebView執行個體或主機Activity(例如缺少ViewGroup.removeView()或保留的事件監聽器參照)。由於外洩的Activity會將整個檢視畫面樹狀結構和解碼的圖片資源固定在記憶體中,重複造訪會快速耗盡 Java 堆積,並導致OutOfMemoryError異常終止。原生或 DOM 洩漏:如果
WebViews和Activities保持不變,但程序 RSS 總量和「Private Other」持續攀升,則洩漏源自未釋出的原生資源、DOM 元素或 JavaScript 引擎繫結。由於這些配置位於原生記憶體中,且會略過 ART 垃圾收集器,因此標準 Java 洩漏偵測工具無法偵測到這些配置,且會持續累積,直到作業系統終止應用程式為止。
使用 CLI 分析獨立轉譯器程序
使用應用程式套件名稱執行 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 效能進行偵錯及分析,請參閱下列資源: