Petunjuk

Memprioritaskan Efisiensi Memori: Langkah-Langkah Penting untuk Android 17

Waktu baca: 10 menit

Meskipun performa aplikasi sering kali disamakan dengan UI yang lancar dan waktu mulai yang cepat, memori berfungsi sebagai fondasi senyap yang mendasari metrik yang terlihat ini. Bukan rahasia lagi bahwa kita melihat perubahan di mana memori perangkat menjadi lebih penting dari sebelumnya. Kami tidak hanya membuat kemajuan dalam pengoptimalan memori Android dengan Android 17, tetapi juga menyediakan dukungan API dan alat untuk membantu Anda memenuhi persyaratan memori yang lebih ketat pada tahun ini.

Untuk memastikan stabilitas perangkat, mulai Android 17, sistem akan mulai menerapkan batasan memori aplikasi berdasarkan total RAM perangkat. Jika aplikasi melampaui batas tersebut, Android akan menghentikan proses tanpa stack trace terkait.

Selain penghentian paksa ini, penggunaan memori yang tidak dioptimalkan pasti akan menurunkan kualitas pengalaman pengguna. Saat aplikasi mendekati batas memori heap, aplikasi akan memicu pembersihan sampah memori yang sering, sehingga menyebabkan UI tersendat. Selain itu, saat perangkat kehabisan memori yang tersedia, sistem akan berupaya merebut kembali halaman, sehingga menyebabkan beban CPU, latensi UI, dan pengurasan baterai. Jika kekurangan memori terlalu parah, hal ini dapat menyebabkan peristiwa Low Memory Killer (LMK) yang menghentikan proses latar belakang secara tiba-tiba dan memaksa aplikasi mengalami cold start yang lambat serta kehilangan status pengguna.

Untuk membuat aplikasi berperforma tinggi dan menghindari penghentian paksa ini, sebaiknya terapkan strategi pengoptimalan memori berikut:

  1. Memaksimalkan pengoptimalan bytecode dengan R8
  2. Mengoptimalkan pemuatan gambar
  3. Mendeteksi dan memperbaiki kebocoran memori dengan Android Studio
  4. Memangkas memori saat aplikasi keluar dari status terlihat
  5. Observabilitas memori lanjutan dengan ProfilingManager

Versi ringkas dari postingan blog ini juga tersedia dalam format video, tonton sekarang.

Memahami batas memori aplikasi Android 17

Batasan memori aplikasi diperkenalkan di Android 17 untuk mencegah "satu aplikasi yang bermasalah" merusak pengalaman multitasking dan stabilitas seluruh perangkat pengguna.

Berikut perincian alasan yang mendorong perubahan arsitektur ini:

  • Mencegah penghentian beruntun: Saat aplikasi menjadi terlalu besar atau mengalami kebocoran memori saat mempertahankan status istimewa (misalnya, menjalankan Layanan Latar Depan), aplikasi awalnya terlindung dari Low Memory Killer (LMK) sistem. Seiring berkembangnya aplikasi tunggal ini tanpa terkendali dan menimbun RAM, LMK terpaksa mengompensasi dengan menghentikan puluhan aplikasi yang di-cache dan tugas latar belakang yang lebih kecil dan berperilaku baik untuk merebut kembali ruang bagi aplikasi yang memakan banyak memori.
     
  • Mempertahankan multitasking dan status pengguna: Jika sistem terpaksa menghapus aplikasi yang di-cache untuk mengakomodasi satu proses yang mengalami kebocoran, pengalaman multitasking akan sangat terganggu. Pengguna yang kembali ke aplikasi yang di-cache sebelumnya akan mengalami cold start yang lambat, bukan melanjutkan aplikasi dengan cepat. Inefisiensi ini menghasilkan lebih banyak tekanan CPU dan mempercepat pengurasan baterai. Hal ini juga dapat merusak konteks pengguna di aplikasi yang baru saja digunakan, seperti posisi scroll, tumpukan navigasi, dan progres dalam game.

Untuk menentukan apakah sesi aplikasi Anda terpengaruh oleh batasan ini di lapangan, Anda dapat memanggil getDescription() dalam ApplicationExitInfo. Jika sistem menerapkan batas, alasan keluar dilaporkan sebagai REASON_OTHER dan string deskripsi akan berisi "MemoryLimiter:AnonSwap". Anda juga dapat memanfaatkan pembuatan profil berbasis pemicu menggunakan TRIGGER_TYPE_ANOMALY untuk otomatis mengambil heap dump saat batas memori tercapai. Selain itu, Android secara aktif berupaya menampilkan lebih banyak metrik memori di lapangan kepada developer dalam Konsol Google Play.

Kami juga telah memperluas dokumentasi batas memori untuk menyertakan perintah pen-debug-an lokal, sehingga Anda dapat menyimulasikan batasan memori di lingkungan lokal dan memvalidasi perilaku aplikasi Anda dalam penegakan batas memori apa pun. 

Memaksimalkan pengoptimalan bytecode dengan R8

Cara yang sangat efektif untuk mengurangi jejak memori aplikasi Anda adalah dengan mengaktifkan pengoptimal R8. Dengan menyusutkan class, metode, dan kolom menjadi nama yang lebih pendek serta menghapus kode dan resource yang tidak digunakan, R8 secara signifikan mengurangi jejak memori aplikasi Anda dengan meminimalkan jumlah kode residen yang diperlukan selama eksekusi. 

R8 meminimalkan kode residen, sehingga mengurangi jejak memori dan menurunkan risiko penghentian LMK. Hal ini menghasilkan warm start yang lebih sering daripada cold start yang lambat. Selain itu, bytecode yang disederhanakan mengurangi overhead CPU thread utama, sehingga secara langsung mengurangi rasio ANR untuk pengalaman pengguna yang lebih lancar. Misalnya, bank digital Monzo mengaktifkan pengoptimalan R8 penuh dan mengalami penurunan rasio ANR sebesar 35%, peningkatan rasio cold start sebesar 30%, dan penurunan ukuran aplikasi secara keseluruhan sebesar 9%.

pic1-IO26_113_TSV-monzo-casestudy.jpg
Bank digital Monzo mengaktifkan pengoptimalan R8 penuh dan meningkatkan metrik performa hingga 35%.

Untuk mengonfigurasi R8 dengan benar di file build.gradle Anda:

  • Tetapkan isShrinkResources = true dan isMinifyEnabled = true.
  • Gunakan proguard-android-optimize.txt, bukan proguard-android.txt lama, yang sebenarnya mencegah pengoptimalan dan tidak lagi didukung di plugin Android Gradle 9.
  • Hapus android.enableR8.fullMode = false dari gradle.properties Anda.

Jika Anda menggunakan refleksi di basis kode, tambahkan Aturan keep untuk mencegah R8 mengoptimalkan bagian kode tersebut. Pastikan untuk membatasi cakupan aturan penyimpanan secara sempit untuk mendapatkan pengoptimalan maksimum. 

Untuk mendapatkan pengoptimalan maksimum, pastikan untuk mengikuti praktik terbaik berikut dalam file aturan penyimpanan Anda.

  • Hapus opsi global seperti -dontoptimize-dontshrink, dan -dontobfuscate yang mencegah R8 mengoptimalkan seluruh codebase
  • Hapus aturan penyimpanan yang mencegah pengoptimalan komponen Android seperti Aktivitas, Layanan, Tampilan, atau Penerima siaran.
  • Perbaiki aturan penyimpanan luas di seluruh paket untuk menargetkan hanya class atau metode tertentu. 

Untuk melihat praktik terbaik lainnya, lihat dokumentasi aturan penyimpanan kami.

Praktik Terbaik Developer Library R8

Jika Anda adalah developer library, tempatkan aturan yang dibutuhkan konsumen Anda secara ketat ke dalam consumer-rules file, dan simpan aturan perlindungan internal library Anda dalam file proguard-rules.pro. Untuk mengetahui informasi selengkapnya tentang cara mengoptimalkan library, lihat Pengoptimalan untuk penulis library.

Penganalisis Konfigurasi R8

Untuk mengaudit pengoptimalan R8, gunakan Configuration Analyzer. Penganalisis konfigurasi menampilkan status pengoptimalan saat ini dengan skor Obfuscation, Pengoptimalan, dan Penyingkatan. Dengan penganalisis konfigurasi, Anda juga dapat memahami jumlah class, metode, atau kolom yang dicegah agar tidak dioptimalkan oleh setiap aturan keep. Pertajam aturan penyimpanan luas di seluruh paket ini untuk mengoptimalkan hasil maksimal. 

Dengan menggunakan penganalisis konfigurasi, Anda juga dapat mengidentifikasi aturan penyimpanan yang mencakup aturan penyimpanan lain, aturan penyimpanan yang berlebihan, dan aturan penyimpanan yang tidak digunakan.

pic2-r8-config-analyzer.png
Configuration Analyzer menampilkan status pengoptimalan saat ini dengan skor Obfuscation, Pengoptimalan, dan Penyingkatan.

Keahlian Agen R8 

Anda juga dapat memanfaatkan Keterampilan Agen R8 dengan agen Android Studio atau alat AI lainnya untuk mengatasi kesalahan konfigurasi dan menyempurnakan aturan Anda sehingga meningkatkan performa aplikasi. (Insight dari keterampilan berbasis AI akan memerlukan verifikasi teknis)

Mengoptimalkan pemuatan gambar

Bitmap biasanya merupakan objek umum terbesar yang berada di memori aplikasi Anda. Tahap ini merupakan tahap akhir dari proses pemuatan gambar saat file yang dikompresi, seperti JPEG atau PNG, didekode menjadi data piksel mentah untuk ditampilkan. Artinya, gambar terkompresi berukuran 100 KB yang kecil dapat membengkak menjadi beberapa megabyte RAM karena konsumsi memori ditentukan oleh dimensi piksel dan kedalaman warna gambar. Karena operasi bitmap sering kali berada di jalur penting untuk menggambar frame, gambar yang tidak dioptimalkan menyebabkan pembengkakan memori dan jank UI yang parah.

Google merekomendasikan penggunaan library pemuatan gambar Coil untuk project yang mengutamakan Kotlin, terutama saat mengembangkan dengan Jetpack Compose dan Glide untuk aplikasi berbasis Java.

Terapkan lima praktik terbaik ini

  1. Turunkan sampel gambar: Jika Anda memuat bitmap secara manual, hindari memuat gambar besar ke dalam tampilan thumbnail kecil; gunakan inSampleSize untuk memuat versi yang lebih kecil. Glide dan Coil secara default mengurangi kualitas gambar dan Anda dapat mengonfigurasi strategi pengurangan kualitas ini menggunakan DownsampleStrategy dan ImageLoader.
  2. Memangkas: Hindari menyematkan padding langsung ke file gambar untuk tujuan letterboxing (misalnya, membuat batas transparan untuk memperluas dimensi gambar). Daripada menyertakan batas ini, gunakan InsetDrawable atau terapkan padding langsung dalam View atau Composable yang berisi bitmap.
  3. Konfigurasi: Seimbangkan memori dan kualitas dengan memilih format piksel yang tepat. Gunakan RGB_565 saat transparansi tidak diperlukan, yang menggunakan setengah memori dari format ARGB_8888 default. Di Glide, Anda dapat mengonfigurasi ini menggunakan DecodeFormat dan di Coil, Anda dapat menggunakan properti bitmapConfig.
  4. Prioritaskan vektor drawable: Untuk aset geometris dasar, manfaatkan ShapeDrawable sebagai alternatif ringan untuk mendekode bitmap raster. Dengan menentukan aset ini satu kali melalui XML, Anda memastikan aset tersebut diskalakan dengan lancar di semua kepadatan tampilan sekaligus secara efektif menghilangkan pembengkakan memori yang didorong oleh resource.
  5. Penggunaan Ulang: Jika aplikasi Anda mengelola Bitmap secara manual, untuk meminimalkan churn memori, saat bitmap tidak lagi diperlukan, aplikasi harus memanggil bitmap.recycle() dan segera menghapus referensi Bitmap. Jika Anda menggunakan library pemuatan gambar seperti Glide atau Coil, kembalikan bitmap ke kumpulan terkelola library. Dengan menyediakan buffer yang ada untuk kebutuhan memori di masa mendatang, pool secara efektif menghindari overhead alokasi baru.

Lihat dokumentasi kami tentang Mengoptimalkan performa untuk gambar untuk mempelajari lebih lanjut.

Alat Android Studio

Anda juga dapat menghilangkan bitmap yang berlebihan menggunakan Android Studio Narwhal 4. Berikut cara menemukannya dalam lima langkah sederhana:

  1. Buka tab Profiler di Android Studio
  2. Klik Heap Dump (atau "Analyze Memory Usage") dan tekan tombol rekam untuk mengambil snapshot status memori aplikasi Anda saat ini.
  3. Pindai hasil analisis untuk menemukan segitiga peringatan kuning ⚠️, yang digunakan Android Studio untuk menandai bitmap duplikat yang disimpan beberapa kali. Atau, buka header profiler, pilih "Filter menurut:", lalu pilih setelan "Bitmap Duplikat".
  4. Klik entri yang ditandai untuk membuka panel Pratinjau Bitmap, sehingga Anda dapat melihat dengan tepat gambar mana yang berulang kali melanggar.
  5. Gunakan konfirmasi visual tersebut untuk melacak logika pemuatan yang berlebihan dalam kode Anda dan menerapkan strategi penyiapan cache yang lebih baik.
pic3-IO26_113_TSV -dup-bitmaps-cropped.jpg
Cari segitiga peringatan kuning ⚠️ dalam heap dump saat menggunakan Android Studio Profiler.

Mendeteksi dan memperbaiki kebocoran memori dengan Android Studio

Kebocoran memori di Android terjadi saat kode Anda mempertahankan referensi objek lama setelah siklus prosesnya berakhir. Hal ini mencegah Pengumpul Sampah (GC) merebut kembali memori tersebut, yang pada akhirnya menyebabkan performa lambat atau OutOfMemoryError (OOM).

Android Studio Panda 3 memiliki tugas profiler LeakCanary khusus, sehingga developer dapat menganalisis kebocoran memori real-time dan memetakan rekaman aktivitas langsung dalam IDE.

Tugas profiler LeakCanary di Android Studio secara aktif memindahkan analisis kebocoran memori dari perangkat Anda ke mesin pengembangan, sehingga menghasilkan peningkatan performa yang signifikan selama fase analisis kebocoran dibandingkan dengan analisis kebocoran di perangkat.

pic4-android-studio-leaks.png
 Analisis kebocoran memori LeakCanary yang dikontekstualisasikan dengan Buka deklarasi untuk proses debug

Selain itu, analisis kebocoran kini dikontekstualisasikan dalam IDE dan terintegrasi sepenuhnya dengan kode sumber Anda, sehingga menyediakan fitur seperti buka deklarasi dan koneksi kode bermanfaat lainnya yang secara drastis mengurangi gesekan dan waktu yang diperlukan untuk menyelidiki dan memperbaiki kebocoran memori.  

Contoh kebocoran memori umum 

Kebocoran memori terjadi saat objek tetap berada dalam memori di luar masa pakainya yang dimaksudkan. Hal ini biasanya terjadi karena:

  • Mempertahankan referensi ke Fragmen, Aktivitas, atau Tampilan yang tidak lagi digunakan.
  • Salah mengelola referensi Konteks.
  • Gagal membatalkan pendaftaran pengamat, pemroses, dan penerima dengan benar.
  • Membuat referensi statis ke objek yang terikat ke komponen dengan siklus proses yang lebih pendek.

Berikut beberapa contoh skenarionya:

SkenarioContoh berbasis Compose Contoh berbasis penayangan
Membocorkan Konteks

Contoh:
Meneruskan LocalContext.current ke ViewModel

Perbaikan:
Pertahankan logika yang bergantung pada Konteks dalam lapisan UI. Untuk lapisan non-UI, lakukan refaktorisasi untuk menggunakan injeksi dependensi atau amati status UI menggunakan flow Kotlin.

Contoh: 
Menyimpan Activity dalam objek pendamping atau variabel statis.

Perbaikan:
Jangan menyimpan referensi statis ke komponen UI. Lakukan refaktor untuk menggunakan injeksi dependensi atau amati status UI menggunakan flow Kotlin.

Kebocoran Pemroses

Contoh:
Menggunakan DisposableEffect untuk memulai pemroses, tetapi membiarkan onDispose kosong.

Perbaikan:
Lakukan pembatalan pendaftaran dan logika pembersihan di dalam blok onDispose.

Contoh:
Mendaftar untuk update SensorManager dan lupa membatalkan pendaftaran.

Perbaikan:
Panggil unregisterListener() secara manual dalam siklus proses onStop() atau onDestroy().

Membocorkan Tampilan

Contoh:
Memegang referensi ke View lama di dalam AndroidView tanpa strategi rilis.


Perbaikan:
Gunakan blok release dari composable AndroidView untuk membersihkan View lama.

Contoh:
Mempertahankan referensi ke objek binding tampilan setelah Fragment dihancurkan.

 

Perbaikan:
Tetapkan variabel binding ke null di dalam metode siklus proses onDestroyView().

Memangkas memori saat aplikasi keluar dari status terlihat

Android dapat memperoleh kembali memori dari aplikasi Anda atau menghentikan aplikasi sepenuhnya jika diperlukan untuk mengosongkan memori untuk tugas penting, seperti yang dijelaskan dalam Ringkasan pengelolaan memori. Android biasanya akan merebut kembali memori dari aplikasi Anda saat aplikasi tidak terlihat oleh pengguna, seperti dengan menghapus beberapa halaman kode dan data aplikasi Anda dalam memori atau mengompresi alokasi heap Anda. Saat pengguna melanjutkan aplikasi Anda dan aplikasi Anda mencoba mengakses beberapa memori yang telah diklaim kembali, OS akan menukar memori tersebut kembali sesuai permintaan. Perilaku pertukaran ini bisa lambat, dan menyebabkan jank atau stutter yang tidak terduga di aplikasi Anda.

Jika Anda membiarkan OS memutuskan memori mana yang akan diambil kembali dari aplikasi Anda, Anda mungkin mendapati bahwa OS mengambil kembali memori yang akan Anda butuhkan segera setelah melanjutkan aplikasi Anda. Sebagai gantinya, aplikasi Anda dapat secara sukarela membuang alokasi memori yang dapat diregenerasi nanti, sesuai permintaan dan dengan biaya rendah. Untuk melakukannya, Anda dapat menerapkan antarmuka ComponentCallbacks2. Anda dapat menerapkan onTrimMemory di ActivityFragmentService, atau bahkan class Application kustom. Menggunakannya di class Application sangat efektif untuk pengelolaan cache global.

Metode callback onTrimMemory() yang disediakan memberi tahu aplikasi Anda tentang peristiwa siklus proses atau terkait memori yang memberikan peluang baik bagi aplikasi Anda untuk secara sukarela mengurangi penggunaan memorinya.

Dalam hal pengelolaan siklus proses memori, penerapan Anda harus berfokus secara eksklusif pada TRIM_MEMORY_UI_HIDDEN dan TRIM_MEMORY_BACKGROUND. Sejak Android 14, sistem telah berhenti mengirimkan notifikasi untuk konstanta lama lainnya, yang secara resmi tidak digunakan lagi di Android 15.

TRIM_MEMORY_UI_HIDDEN: Sinyal ini menunjukkan bahwa UI aplikasi Anda telah keluar dari tampilan pengguna. Hal ini memberikan peluang untuk melepaskan alokasi memori yang besar dan terikat secara ketat ke antarmuka—seperti Bitmap, buffer pemutaran video, atau resource animasi yang kompleks.

TRIM_MEMORY_BACKGROUND: Pada level ini, proses Anda berada di latar belakang dan sekarang menjadi kandidat untuk dihentikan guna memenuhi kebutuhan memori global sistem. Untuk memperpanjang durasi proses Anda tetap dalam status yang di-cache, dan mengurangi jumlah cold start aplikasi, Anda harus melepaskan semua resource yang dapat dengan mudah direkonstruksi setelah pengguna melanjutkan sesi mereka.

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Catatan: Integrasi onTrimMemory mungkin bergantung pada dukungan SDK. Misalnya, game tertentu mengandalkan game engine-nya untuk mengaktifkan kemampuan ini. Lihat dokumen pengoptimalan memori game.

Observabilitas memori lanjutan dengan ProfilingManager

Untuk menangkap dan mendiagnosis masalah memori di lapangan yang tidak dapat direproduksi secara lokal, Anda harus memanfaatkan ProfilingManager API. Diperkenalkan di Android 15, API pengamatan tingkat lanjut ini memungkinkan Anda mengumpulkan profil Perfetto pengguna sebenarnya secara terprogram. 

Untuk tim yang tidak memiliki infrastruktur khusus untuk mengelola dan menghosting artefak performa, Crashlytics sedang mempelajari solusi khusus untuk menyederhanakan alur kerja ini. Mereka mengundang developer untuk memberikan masukan.

Android 17 memperkenalkan pemicu baru berbasis peristiwa, terutama TRIGGER_TYPE_OOM dan TRIGGER_TYPE_ANOMALY:

  • Pemicu OOM secara otomatis mengumpulkan heap dump Java tepat saat error OutOfMemoryError terjadi, sehingga memberikan status alokasi yang akurat. Profil OOM yang dikumpulkan akan diberikan saat aplikasi dimulai dan mendaftarkan callback registerForAllProfilingResults pada waktu berikutnya.
  • Pemicu anomali mendeteksi masalah performa berat, seperti spam binder yang berlebihan atau pelanggaran batas memori. Anomali memori memberikan dump heap tepat sebelum sistem menghentikan aplikasi.
    val profilingManager = 
applicationContext.getSystemService(ProfilingManager::class.java)
    val triggers = ArrayList<ProfilingTrigger>()  


    triggers.add(ProfilingTrigger.Builder(
                 ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
    val mainExecutor: Executor = Executors.newSingleThreadExecutor()
    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) {
            // upload profile result to server for further analysis          
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } 

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

Setelah mengumpulkan heap dump, Anda dapat mendownload profil dari server, atau secara lokal melalui penarikan adb dan menarik lalu melepas file ke UI Perfetto. Untuk menyederhanakan alur kerja penelusuran memori, gunakan Heap Dump Explorer, yang merupakan tampilan default baru untuk heap dump di UI Perfetto. Alat ini menyediakan antarmuka intuitif untuk memeriksa dump heap Java, sehingga Anda dapat memvisualisasikan hierarki alokasi objek, menghitung ukuran memori yang dipertahankan, dan mengidentifikasi jalur terpendek dari root pembersihan sampah memori. Dengan memanfaatkan Heap Dump Explorer, Anda dapat dengan cepat menunjukkan kebocoran memori, objek yang dipertahankan yang terlalu besar seperti alokasi bitmap yang berlebihan, dan menganalisis alokasi objek heap di satu tempat.

pic5-perfettoheapdump-analyzer.png
Gunakan flamegraph tersemat Heap Dump Explorer untuk memeriksa dan menavigasi objek dengan alokasi heap tertinggi secara visual.

Kesimpulan

Mengoptimalkan bytecode dengan R8, menerapkan praktik terbaik pemuatan gambar, dan mengatasi kebocoran memori adalah langkah penting untuk memberikan pengalaman pengguna berkualitas tinggi sekaligus mengelola resource secara efektif dalam kondisi tertekan. Menerapkan langkah-langkah proaktif ini membantu menjaga stabilitas dan performa aplikasi, mencegah penghentian yang tidak terduga sekaligus melindungi konteks pengguna. Untuk meningkatkan keahlian performa Anda, pelajari panduan memori kami yang telah direvisi.

Ditulis oleh:
Lanjutkan membaca