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