Mengelola dan mendiagnosis memori WebView

WebView menjalankan kode native di beberapa proses untuk merender konten web di aplikasi Android Anda. Jika tidak dikelola, instance WebView dapat menyebabkan kebocoran memori, error karena kehabisan memori (OOM), dan penurunan performa aplikasi.

Dokumen ini menjelaskan model memori multi-proses WebView, menjelaskan cara mengelola siklus prosesnya dengan benar untuk mencegah kebocoran, dan memberikan alur kerja praktis untuk mendiagnosis masalah memori.

Memahami arsitektur memori WebView

Untuk mengelola memori WebView secara efektif, pahami cara Android mengalokasikan resource untuk konten web:

  • Eksekusi multi-proses: Di Android 8.0 (level API 26) dan yang lebih tinggi, WebView memisahkan konten web dari fungsi inti aplikasi Anda di beberapa proses (pada perangkat dengan RAM rendah, mungkin kembali ke satu proses):

    • Proses Host (Browser): Proses aplikasi utama tempat kode Activity dan Java atau Kotlin Anda berjalan.
    • Proses Perender Terisolasi: Proses sandbox terpisah (SandboxedProcessService) yang mem-parsing HTML dan CSS, mengeksekusi JavaScript, dan merender halaman web.
  • Jejak memori native: Sebagian besar memori WebView, termasuk grafis yang dirender, hierarki DOM, dan memori runtime JavaScript, dialokasikan dalam memori native, bukan di heap Java. Heap dump Java (.hprof) hanya menampilkan objek wrapper Java ringan dan tidak merekam memori sebenarnya yang digunakan oleh konten web.

  • Dampak memori native pada sistem: Tidak seperti alokasi heap Java, yang dibatasi oleh batas maxHeap aplikasi dan gagal dengan cepat dengan OutOfMemoryError, memori native dapat bertambah secara diam-diam hingga gigabyte. Saat memori native yang belum dirilis mengisi RAM fisik dan ruang swap (zRAM), Low Memory Killer (LMK) Android mulai menghentikan proses latar belakang untuk merebut kembali memori. Hal ini menurunkan kualitas multitasking perangkat secara keseluruhan sebelum akhirnya menghentikan aplikasi latar depan.

Mengelola siklus proses WebView

Pengelolaan siklus proses yang tepat sangat penting untuk mencegah kebocoran memori. Kesalahan umum adalah mengasumsikan bahwa menghapus WebView dari tata letak atau membiarkan Activity selesai secara otomatis akan mengosongkan memorinya.

Untuk memastikan pembersihan lengkap referensi konteks Java dan resource render native, Anda harus mengatur urutan penonaktifan secara eksplisit dalam siklus proses komponen host (seperti onDestroy()), menghentikan eksekusi halaman aktif, melepaskan tampilan dari penampungnya, dan melepaskan binding native.

Membersihkan instance WebView

Untuk memastikan penonaktifan yang bersih dan melepaskan resource saat Activity atau Fragment Anda dihancurkan, lakukan hal berikut:

  1. Hapus WebView dari penampung induknya (ViewGroup).
  2. Hentikan pemuatan aktif dan hapus histori navigasi.
  3. Panggil destroy().
  4. Hapus referensi ke null.

Contoh berikut menunjukkan cara membersihkan WebView dengan benar:

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();
}

Memahami memori pasca-penghancuran

Saat Anda memanggil destroy(), sistem akan melepaskan konteks Activity, membersihkan hierarki tampilan, dan menghentikan pekerjaan latar belakang web. Namun, Anda mungkin mengamati bahwa memori fisik proses (Ukuran Set Residen) tidak langsung turun ke dasar pengukuran pra-WebView.

Hal ini normal terjadi. Cache runtime native, library bersama, dan halaman memori yang dialokasikan tetap berada dalam proses hingga sistem operasi mereklamasi atau proses berakhir. Tujuan utama destroy() adalah untuk mencegah kebocoran memori Activity kumulatif saat pengguna masuk dan keluar dari layar yang didukung web.

Metrik pen-debug-an utama

Saat menganalisis konsumsi memori WebView, fokus pada metrik berikut:

  • Resident Set Size (RSS): Total RAM fisik yang dipetakan ke dalam proses, termasuk kode dan library bersama (diberi label Total di Android Studio Profiler).

  • RSS Anonim (RssAnon): Memori yang dialokasikan langsung oleh proses yang tidak didukung oleh file di disk (seperti alokasi heap native dan runtime JavaScript). Ini menunjukkan biaya memori utama konten web Anda (diberi label Dialokasikan di Android Studio Profiler).

  • Jejak Memori Pribadi (PMF): Jumlah RSS Anonim dan swap (zRAM). PMF mencerminkan beban memori yang sebenarnya tidak dapat dikeluarkan yang ditimbulkan aplikasi Anda pada sistem.

  • PMF Browser versus PMF Perender: Memori yang digunakan oleh proses utama aplikasi Anda dibandingkan dengan memori yang digunakan oleh proses perender terisolasi. Konten web berat menyebabkan lonjakan terutama dalam proses perender.

  • Jumlah Objek Aktif (WebViews, Activities, Views): Jumlah instance UI, Konteks, dan WebView aktif yang disimpan dalam memori. Pelacakan ini mengidentifikasi apakah pertumbuhan memori disebabkan oleh referensi Java yang dipertahankan atau alokasi khusus native.

  • Private Other dan Native Heap: Di dumpsys meminfo, alokasi C/C++ native dan pemetaan memori kustom (seperti PartitionAlloc Chromium atau heap runtime JavaScript tersemat) muncul di bagian Native Heap dan Private Other, bukan Java Heap.

Untuk mengetahui informasi selengkapnya tentang penghitung memori proses dan kategorinya, lihat Glosarium memori proses.

Alur kerja diagnostik praktis

Karena WebView beroperasi di beberapa proses dan mengalokasikan memori native, gunakan alat dan teknik berikut untuk memeriksa jejaknya:

Alat pembuatan profil dan diagnostik

Untuk memeriksa alokasi memori dan mendiagnosis kebocoran, gunakan alat berikut:

  • Android Studio Memory Profiler: Gunakan Memory Profiler untuk memvisualisasikan alokasi native, melacak kategori memori dari waktu ke waktu, dan mendeteksi kebocoran Activity di seluruh transisi layar.

  • Pelacakan memori dengan Perfetto: Gunakan Perfetto untuk merekam penghitung memori tingkat sistem (seperti RSS dan RSS Anonim) untuk mengamati pertumbuhan memori secara keseluruhan. Perhatikan bahwa alokasi mesin native WebView tidak menghasilkan callstack di alat pembuatan profil heap Perfetto. Gunakan Chrome DevTools untuk memeriksa snapshot heap JavaScript dan alokasi DOM di dalam konten web.

Memeriksa jumlah objek live

Untuk menentukan apakah pertumbuhan memori disebabkan oleh objek framework Java yang dipertahankan (seperti komponen UI) atau alokasi native, periksa bagian Objects dumpsys meminfo:

adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"

Output menampilkan jumlah objek aktif:

 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

Bagian ini menampilkan jumlah objek framework aktif, handle IPC, dan alokasi Parcel. Untuk diagnostik WebView, fokuslah terutama pada Activities dan WebViews.

Lakukan interaksi pengguna target (seperti membuka dan menutup layar web) berulang kali dan bandingkan jumlahnya:

  • Kebocoran instance: Jika WebViews atau Activities bertambah pada setiap navigasi dan tidak kembali ke dasar, aplikasi Anda membocorkan instance Java WebView atau host Activity (misalnya, karena referensi listener yang hilang atau dipertahankan ViewGroup.removeView()). Karena pin Activity yang bocor menyematkan seluruh hierarki tampilan dan resource gambar yang didekode dalam memori, kunjungan berulang akan dengan cepat menghabiskan heap Java dan menyebabkan OutOfMemoryError error.

  • Kebocoran Native atau DOM: Jika WebViews dan Activities tetap konstan sementara RSS proses total dan Lainnya Pribadi terus meningkat, kebocoran berasal dari resource native yang belum dirilis, elemen DOM, atau binding mesin JavaScript. Karena alokasi ini berada di memori native dan melewati pengumpul sampah ART, alokasi ini tetap tidak terlihat oleh alat deteksi kebocoran Java standar dan terus terakumulasi hingga sistem operasi menghentikan aplikasi.

Membuat profil proses perender yang diisolasi menggunakan CLI

Menjalankan dumpsys meminfo dengan nama paket aplikasi Anda hanya akan menghasilkan output memori untuk proses host utama. Untuk memeriksa proses perender yang diisolasi tempat halaman web dirender:

  1. Temukan ID proses (PID) layanan perender terisolasi:

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    Output menampilkan catatan proses yang diisolasi dan PID-nya RENDERER_PID (misalnya, 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Periksa perincian memori proses perender menggunakan PID-nya:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Periksa proses aplikasi host untuk mengevaluasi jejak sisi browser:

    adb shell dumpsys meminfo <var>PACKAGE_NAME</var>

Memeriksa peta dan alokasi memori

Untuk melihat subsistem atau pengalokasi native mana yang menempati memori anonim, periksa peta memori proses:

adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"

Tabel berikut mencantumkan tag memori anonim umum dan relevansinya dengan pertumbuhan memori:

Tag Memori Subsistem Relevansi dengan konten aplikasi dan web Penyebab umum peningkatan memori?
[anon:partition_alloc] Chromium PartitionAlloc Alokasi untuk hierarki DOM, buffer rendering, heap JavaScript V8, dan eksekusi WebAssembly di WebView. Ya (Tinggi): Memuat halaman web yang berat, DOM yang kaya media, atau gagal memanggil destroy() pada instance WebView yang dibuang secara langsung akan meningkatkan tag ini.
[anon:scudo...] atau [anon:libc_malloc] Pengalokasi heap native Android (Scudo / jemalloc) Alokasi native C/C++ umum yang digunakan oleh library NDK, jembatan JNI, dan pipeline grafis native. Ya (Sedang hingga Tinggi): Pertumbuhan terjadi saat wrapper JNI native atau dependensi C++ pihak ketiga mempertahankan alokasi yang belum dirilis di seluruh navigasi.
[anon:...] (misalnya, [anon:quickjs_heap...]) Skrip kustom atau runtime native Mesin JavaScript tersemat, runtime WebAssembly kustom, atau kumpulan buffer native kustom. Ya (Bergantung pada konteks): Umum terjadi di aplikasi hybrid yang menjalankan mesin scripting bersama tampilan native dan gagal membersihkan binding runtime.

Batasan API memori dalam aplikasi

API memori dalam aplikasi (seperti Debug.getMemoryInfo atau ActivityManager.getProcessMemoryInfo) hanya mengukur proses panggilan. Dalam mode multiproses, API ini tidak dapat merekam memori yang digunakan oleh proses perender terisolasi. Untuk penilaian total memori yang akurat, andalkan alat sistem seperti dumpsys meminfo, Perfetto, atau Profiler Android Studio.

Menangani penggunaan memori tinggi dalam aplikasi hybrid

Saat mendiagnosis pertumbuhan memori yang tidak dapat dijelaskan selama interaksi WebView yang berulang (seperti membuka link web atau menjelajahi feed yang didukung web), gunakan alur kerja triase berikut untuk mengisolasi apakah kebocoran berasal dari lapisan Java atau mesin native:

  1. Isolasi jenis kebocoran (Java versus native): Jalankan dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" sebelum dan setelah transisi pengguna berulang (seperti membuka dan menutup artikel web atau menggeser feed).

    • Pengamatan: Jika jumlah Activities dan WebViews tetap stabil (misalnya, 1–2 instance aktif), aplikasi tidak membocorkan konteks Activity atau instance WebView Java.
  2. Mengukur delta memori di seluruh interaksi (Pelacakan deret waktu): Ambil snapshot dumpsys meminfo di beberapa interaksi pengguna untuk menghitung rasio alokasi per transisi:

    • Pengamatan: Heap Java tetap dibatasi dan dalam kondisi baik (melonjak selama penggunaan dan menurun setelah pembersihan sampah memori), tetapi Private Other dan Native Heap terus meningkat beberapa megabyte per transisi. Hal ini membuktikan bahwa kebocoran sepenuhnya terjadi di memori native di luar runtime ART. Dump heap Java standar (.hprof) tidak akan menunjukkan masalah apa pun.
  3. Periksa Peta Memori Anonim: Periksa peta memori proses menggunakan ADB (lihat Memeriksa peta dan alokasi memori):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Pengamatan: Pertumbuhan memori terkonsentrasi di heap mesin skrip yang disematkan atau [anon:partition_alloc], disertai dengan peningkatan lambat dalam referensi global JNI. Hal ini menunjukkan bahwa meskipun tampilan Java diganti, objek halaman native yang mendasarinya atau pengikatan JavaScript tidak dilepaskan.
  4. Perbaikan:

    • Pastikan setiap WebView yang didaur ulang atau dibuang secara eksplisit menghentikan skrip aktif (stopLoading()), menghapus histori, dan memanggil destroy().
    • Hapus callback jembatan JavaScript kustom atau referensi global JNI yang terkait dengan tampilan yang ditutup.
    • Pastikan Private Other dan pemrosesan RSS stabil setelah transisi navigasi.

Referensi lainnya

Untuk mempelajari lebih lanjut proses debug dan pembuatan profil memori dan performa WebView, lihat referensi berikut: