WebView chạy mã gốc trên nhiều quy trình để hiển thị nội dung web trong ứng dụng Android của bạn. Việc không quản lý các thực thể WebView có thể dẫn đến tình trạng rò rỉ bộ nhớ, sự cố hết bộ nhớ (OOM) và hiệu suất của ứng dụng giảm sút.
Tài liệu này giải thích mô hình bộ nhớ nhiều quy trình WebView, mô tả cách quản lý đúng vòng đời của mô hình này để ngăn chặn rò rỉ và cung cấp các quy trình thực tế để chẩn đoán các vấn đề về bộ nhớ.
Tìm hiểu cấu trúc bộ nhớ WebView
Để quản lý bộ nhớ WebView một cách hiệu quả, hãy tìm hiểu cách Android phân bổ tài nguyên cho nội dung web:
Thực thi nhiều quy trình: Trên Android 8.0 (cấp độ API 26) trở lên,
WebViewtách nội dung trên web khỏi các chức năng cốt lõi của ứng dụng trên nhiều quy trình (trên các thiết bị có RAM thấp, có thể quay lại một quy trình duy nhất):- Quy trình Máy chủ lưu trữ (Trình duyệt): Quy trình ứng dụng chính nơi chạy
Activityvà mã Java hoặc Kotlin của bạn. - Quy trình Isolated Renderer: Một quy trình riêng biệt được cách ly (
SandboxedProcessService) phân tích cú pháp HTML và CSS, thực thi JavaScript và hiển thị các trang web.
- Quy trình Máy chủ lưu trữ (Trình duyệt): Quy trình ứng dụng chính nơi chạy
Mức sử dụng bộ nhớ gốc: Hầu hết bộ nhớ
WebView, bao gồm cả đồ hoạ được kết xuất, cây DOM và bộ nhớ thời gian chạy JavaScript, đều được phân bổ trong bộ nhớ gốc, chứ không phải trên vùng nhớ khối xếp Java. Tệp báo lỗi vùng nhớ khối xếp Java (.hprof) chỉ cho thấy một đối tượng trình bao bọc Java đơn giản và không ghi lại bộ nhớ thực mà nội dung trên web sử dụng.Tác động của bộ nhớ gốc đến hệ thống: Không giống như các lượt phân bổ vùng nhớ khối xếp Java (bị giới hạn bởi hạn mức
maxHeapcủa ứng dụng và nhanh chóng gặp lỗi vớiOutOfMemoryError), bộ nhớ gốc có thể âm thầm tăng lên đến hàng gigabyte. Khi bộ nhớ gốc chưa được giải phóng lấp đầy RAM vật lý và không gian hoán đổi (zRAM), Low Memory Killer (LMK) của Android sẽ bắt đầu chấm dứt các quy trình trong nền để thu hồi bộ nhớ. Điều này làm giảm khả năng đa nhiệm tổng thể của thiết bị trước khi cuối cùng buộc dừng ứng dụng trên nền trước.
Quản lý vòng đời của WebView
Việc quản lý vòng đời đúng cách là rất quan trọng để ngăn chặn tình trạng rò rỉ bộ nhớ. Một sai lầm thường gặp là cho rằng việc xoá một WebView khỏi bố cục hoặc cho phép một Activity tự động hoàn tất sẽ giải phóng bộ nhớ của nó.
Để đảm bảo dọn dẹp hoàn toàn cả các tham chiếu ngữ cảnh Java và tài nguyên kết xuất gốc, bạn phải điều phối rõ ràng một trình tự tháo dỡ trong vòng đời của thành phần lưu trữ (chẳng hạn như onDestroy()), dừng thực thi trang đang hoạt động, tách khung hiển thị khỏi vùng chứa và giải phóng các liên kết gốc.
Dọn dẹp các phiên bản WebView
Để đảm bảo tắt đúng cách và giải phóng tài nguyên khi Activity hoặc Fragment bị huỷ, hãy làm như sau:
- Xoá
WebViewkhỏi vùng chứa mẹ (ViewGroup). - Ngừng tải chủ động và xoá nhật ký điều hướng.
- Gọi
destroy(). - Xoá tham chiếu đến
null.
Ví dụ sau đây minh hoạ cách dọn dẹp đúng cách một 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();
}
Tìm hiểu về bộ nhớ sau khi bị phá huỷ
Khi bạn gọi destroy(), hệ thống sẽ giải phóng ngữ cảnh Activity, dọn dẹp hệ phân cấp khung hiển thị và dừng hoạt động ở chế độ nền của web. Tuy nhiên, bạn có thể nhận thấy rằng bộ nhớ thực của quy trình (Dung lượng bộ nhớ RAM bị chiếm dụng) không giảm ngay xuống mức cơ sở trước WebView.
Đây là hành vi bình thường. Các trang bộ nhớ được phân bổ, thư viện dùng chung và bộ nhớ đệm thời gian chạy gốc vẫn nằm trong quy trình cho đến khi hệ điều hành thu hồi chúng hoặc quy trình kết thúc. Mục tiêu chính của destroy() là ngăn chặn tình trạng rò rỉ bộ nhớ Activity tích luỹ khi người dùng di chuyển vào và ra khỏi các màn hình dựa trên web.
Các chỉ số gỡ lỗi chính
Khi phân tích mức tiêu thụ bộ nhớ WebView, hãy tập trung vào các chỉ số sau:
Kích thước cài đặt thường trú (RSS): Tổng RAM vật lý được ánh xạ vào quy trình, bao gồm cả mã và thư viện dùng chung (được gắn nhãn là Tổng trong Trình phân tích tài nguyên Android Studio).
RSS ẩn danh (RssAnon): Bộ nhớ do quy trình phân bổ trực tiếp và không được tệp trên ổ đĩa hỗ trợ (chẳng hạn như mức phân bổ thời gian chạy JavaScript và vùng nhớ khối xếp gốc). Đây là chi phí bộ nhớ chính của nội dung trên web (được gắn nhãn là Đã phân bổ trong Trình phân tích tài nguyên của Android Studio).
Mức sử dụng bộ nhớ riêng tư (PMF): Tổng của RSS ẩn danh và bộ nhớ hoán đổi (zRAM). PMF phản ánh gánh nặng bộ nhớ thực tế mà ứng dụng của bạn áp đặt lên hệ thống và không thể loại bỏ.
PMF của trình duyệt so với PMF của trình kết xuất: Bộ nhớ do quy trình chính của ứng dụng sử dụng so với bộ nhớ do quy trình trình kết xuất riêng biệt sử dụng. Nội dung web có dung lượng lớn chủ yếu gây ra các đợt tăng đột biến trong quy trình kết xuất.
Số lượng đối tượng đang hoạt động (
WebViews,Activities,Views): Số lượng phiên bản giao diện người dùng, Ngữ cảnh vàWebViewđang hoạt động được lưu giữ trong bộ nhớ. Việc theo dõi những thông tin này sẽ xác định xem mức tăng bộ nhớ có phải do các tham chiếu Java được giữ lại hay chỉ do các lượt phân bổ gốc gây ra hay không.Vùng nhớ khối xếp riêng tư khác và vùng nhớ khối xếp gốc: Trong
dumpsys meminfo, các lượt phân bổ C/C++ gốc và các ánh xạ bộ nhớ tuỳ chỉnh (chẳng hạn nhưPartitionAllocChromium hoặc vùng nhớ khối xếp thời gian chạy JavaScript nhúng) sẽ xuất hiện trong Vùng nhớ khối xếp gốc và Vùng nhớ khối xếp riêng tư khác thay vì Vùng nhớ khối xếp Java.
Để biết thêm thông tin về các bộ đếm bộ nhớ quy trình và danh mục của chúng, hãy xem Bảng chú giải bộ nhớ quy trình.
Quy trình chẩn đoán thực tế
Vì WebView hoạt động trên nhiều quy trình và phân bổ bộ nhớ gốc, hãy sử dụng các công cụ và kỹ thuật sau để kiểm tra mức sử dụng bộ nhớ của nó:
Công cụ chẩn đoán và phân tích tài nguyên
Để kiểm tra mức phân bổ bộ nhớ và chẩn đoán rò rỉ, hãy sử dụng các công cụ sau:
Trình phân tích bộ nhớ của Android Studio: Sử dụng Trình phân tích bộ nhớ để trực quan hoá các hoạt động phân bổ gốc, theo dõi các danh mục bộ nhớ theo thời gian và phát hiện các lỗi rò rỉ
Activitytrong quá trình chuyển đổi màn hình.Theo dõi bộ nhớ bằng Perfetto: Sử dụng Perfetto để ghi lại các bộ đếm bộ nhớ ở cấp hệ thống (chẳng hạn như RSS và RSS ẩn danh) nhằm quan sát mức tăng bộ nhớ tổng thể. Xin lưu ý rằng việc phân bổ công cụ gốc
WebViewkhông tạo ra ngăn xếp lệnh gọi trong công cụ lập hồ sơ heap của Perfetto. Sử dụng Chrome DevTools để kiểm tra snapshot của vùng nhớ heap JavaScript và các hoạt động phân bổ DOM trong nội dung trên web.
Kiểm tra số lượng đối tượng trực tiếp
Để xác định xem mức tăng bộ nhớ có phải do các đối tượng khung Java được giữ lại (chẳng hạn như các thành phần giao diện người dùng) hay các hoạt động phân bổ gốc gây ra hay không, hãy kiểm tra phần Objects của dumpsys meminfo:
adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"Kết quả đầu ra hiển thị số lượng đối tượng trực tiếp:
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
Phần này hiển thị số lượng đối tượng khung hoạt động, các thao tác IPC và việc phân bổ Parcel. Đối với thông tin chẩn đoán WebView, hãy tập trung chủ yếu vào Activities và WebViews.
Thực hiện nhiều lần hoạt động tương tác của người dùng mục tiêu (chẳng hạn như mở và đóng màn hình web) rồi so sánh số lượt:
Rò rỉ thực thể: Nếu
WebViewshoặcActivitiestăng trên mỗi thao tác điều hướng và không trở về đường cơ sở, thì ứng dụng của bạn đang rò rỉ thực thểWebViewJava hoặcActivitymáy chủ lưu trữ (ví dụ: do thiếuViewGroup.removeView()hoặc giữ lại các tham chiếu đến trình nghe). Vì mộtActivitybị rò rỉ sẽ ghim toàn bộ cây khung hiển thị và các tài nguyên hình ảnh đã giải mã vào bộ nhớ, nên các lượt truy cập lặp lại sẽ nhanh chóng làm cạn kiệt heap Java và gây ra sự cốOutOfMemoryError.Rò rỉ gốc hoặc DOM: Nếu
WebViewsvàActivitieskhông đổi trong khi tổng RSS quy trình và Private Other tiếp tục tăng, thì tình trạng rò rỉ bắt nguồn từ các tài nguyên gốc chưa phát hành, các phần tử DOM hoặc các liên kết công cụ JavaScript. Vì những hoạt động phân bổ này nằm trong bộ nhớ gốc và bỏ qua trình thu gom rác ART, nên chúng vẫn không xuất hiện trong các công cụ phát hiện rò rỉ Java tiêu chuẩn và tiếp tục tích luỹ cho đến khi hệ điều hành kết thúc ứng dụng.
Hồ sơ tiến trình kết xuất riêng biệt bằng giao diện dòng lệnh
Việc chạy dumpsys meminfo bằng tên gói của ứng dụng chỉ xuất bộ nhớ cho quy trình máy chủ lưu trữ chính. Cách kiểm tra quy trình kết xuất riêng biệt nơi các trang web được kết xuất:
Tìm mã nhận dạng tiến trình (PID) của dịch vụ trình kết xuất riêng biệt:
adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"Kết quả đầu ra hiển thị bản ghi quy trình riêng biệt và PID RENDERER_PID (ví dụ:
22155):Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}Kiểm tra mức sử dụng bộ nhớ của quy trình kết xuất bằng PID:
adb shell dumpsys meminfo <var>RENDERER_PID</var>Kiểm tra quy trình ứng dụng lưu trữ để đánh giá dấu vết phía trình duyệt:
adb shell dumpsys meminfo <var>PACKAGE_NAME</var>
Kiểm tra việc phân bổ và bản đồ bộ nhớ
Để xem những hệ thống con hoặc trình phân bổ gốc nào chiếm bộ nhớ ẩn danh, hãy kiểm tra các bản đồ bộ nhớ của quy trình:
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"Bảng sau đây liệt kê các thẻ bộ nhớ ẩn danh phổ biến và mức độ liên quan của chúng đến việc tăng bộ nhớ:
| Thẻ nhớ | OOB | Mức độ liên quan đến nội dung trên ứng dụng và web | Nguyên nhân phổ biến khiến bộ nhớ tăng lên? |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | Việc phân bổ cho cây DOM, vùng đệm kết xuất, vùng nhớ heap JavaScript V8 và việc thực thi WebAssembly trong WebView. |
Có (Cao): Việc tải các trang web có dung lượng lớn, DOM đa phương tiện hoặc không gọi destroy() trên các phiên bản WebView bị loại bỏ sẽ trực tiếp làm tăng thẻ này. |
[anon:scudo...] hoặc [anon:libc_malloc] |
Trình phân bổ vùng nhớ heap gốc của Android (Scudo / jemalloc) | Các hoạt động phân bổ gốc C/C++ chung do các thư viện NDK, cầu nối JNI và quy trình đồ hoạ gốc sử dụng. | Có (Trung bình đến cao): Mức tăng xảy ra khi trình bao bọc JNI gốc hoặc các phần phụ thuộc C++ của bên thứ ba giữ lại các mục phân bổ chưa phát hành trên các thao tác điều hướng. |
[anon:...] (ví dụ: [anon:quickjs_heap...]) |
Tập lệnh tuỳ chỉnh hoặc thời gian chạy gốc | Công cụ JavaScript được nhúng, thời gian chạy WebAssembly tuỳ chỉnh hoặc nhóm vùng đệm gốc tuỳ chỉnh. | Có (Tuỳ theo bối cảnh): Thường gặp trong các ứng dụng kết hợp thực thi công cụ kịch bản cùng với các khung hiển thị gốc và không dọn dẹp các liên kết thời gian chạy. |
Giới hạn của các API bộ nhớ trong ứng dụng
Các API bộ nhớ trong ứng dụng (chẳng hạn như Debug.getMemoryInfo hoặc ActivityManager.getProcessMemoryInfo) chỉ đo lường quy trình gọi.
Ở chế độ nhiều quy trình, các API này không thể ghi lại bộ nhớ mà quy trình kết xuất riêng biệt sử dụng. Để đánh giá chính xác tổng bộ nhớ, hãy dựa vào các công cụ hệ thống như dumpsys meminfo, Perfetto hoặc Trình phân tích tài nguyên trong Android Studio.
Phân loại bộ nhớ cao trong ứng dụng kết hợp
Khi chẩn đoán tình trạng tăng bộ nhớ không rõ nguyên nhân trong các hoạt động tương tác WebView
định kỳ (chẳng hạn như mở đường liên kết trên web hoặc điều hướng các nguồn cấp dữ liệu dựa trên web), hãy sử dụng quy trình phân loại sau để xác định xem rò rỉ có bắt nguồn từ lớp Java hay công cụ gốc:
Cô lập loại rò rỉ (Java so với rò rỉ gốc): Chạy
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"trước và sau các lượt chuyển đổi lặp lại của người dùng (chẳng hạn như mở và đóng bài viết trên web hoặc vuốt qua nguồn cấp dữ liệu).- Quan sát: Nếu số lượng
ActivitiesvàWebViewsvẫn ổn định (ví dụ: 1–2 phiên bản đang hoạt động), thì ứng dụng không bị rò rỉ các ngữ cảnhActivityhoặc các phiên bảnWebViewJava.
- Quan sát: Nếu số lượng
Đo lường mức chênh lệch bộ nhớ trong các hoạt động tương tác (Theo dõi chuỗi thời gian): Chụp ảnh nhanh
dumpsys meminfotrong nhiều hoạt động tương tác của người dùng để tính tốc độ phân bổ trên mỗi quá trình chuyển đổi:- Quan sát: Nhóm Java vẫn bị giới hạn và hoạt động bình thường (tăng đột biến trong quá trình sử dụng và giảm sau khi thu gom rác), nhưng Private Other và Native Heap tăng đều đặn vài megabyte cho mỗi lần chuyển đổi. Điều này chứng minh rằng lỗi rò rỉ hoàn toàn nằm trong bộ nhớ gốc bên ngoài thời gian chạy ART.
Các kết xuất heap Java tiêu chuẩn (
.hprof) sẽ không cho thấy vấn đề nào.
- Quan sát: Nhóm Java vẫn bị giới hạn và hoạt động bình thường (tăng đột biến trong quá trình sử dụng và giảm sau khi thu gom rác), nhưng Private Other và Native Heap tăng đều đặn vài megabyte cho mỗi lần chuyển đổi. Điều này chứng minh rằng lỗi rò rỉ hoàn toàn nằm trong bộ nhớ gốc bên ngoài thời gian chạy ART.
Các kết xuất heap Java tiêu chuẩn (
Kiểm tra các bản đồ bộ nhớ ẩn danh: Kiểm tra các bản đồ bộ nhớ của quy trình bằng ADB (xem phần Kiểm tra các bản đồ bộ nhớ và mức phân bổ):
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- Quan sát: Mức sử dụng bộ nhớ tăng lên tập trung ở
[anon:partition_alloc]hoặc các vùng nhớ của công cụ tập lệnh được nhúng, kèm theo mức tăng chậm trong các lượt tham chiếu JNI toàn cục. Điều này cho thấy rằng mặc dù các khung hiển thị Java đã được thay thế, nhưng các đối tượng trang gốc cơ bản hoặc các liên kết JavaScript chưa được phát hành.
- Quan sát: Mức sử dụng bộ nhớ tăng lên tập trung ở
Khắc phục:
- Đảm bảo rằng mọi
WebViewđược tái chế hoặc loại bỏ đều dừng rõ ràng các tập lệnh đang hoạt động (stopLoading()), xoá nhật ký và gọidestroy(). - Huỷ các lệnh gọi lại cầu JavaScript tuỳ chỉnh hoặc các tham chiếu JNI toàn cục được liên kết với các khung hiển thị bị loại bỏ.
- Xác nhận rằng
Private Othervà quy trình RSS ổn định sau các hiệu ứng chuyển đổi điều hướng.
- Đảm bảo rằng mọi
Tài nguyên khác
Để tìm hiểu thêm về cách gỡ lỗi và lập hồ sơ bộ nhớ cũng như hiệu suất WebView, hãy xem các tài nguyên sau: