Kode aplikasi adalah memori

Kode yang Anda tulis sendiri merupakan bentuk penggunaan memori. Setiap class, metode, dan konstanta string di aplikasi Anda harus dimuat ke dalam RAM saat dieksekusi. Makin besar basis kode aplikasi Anda, makin banyak memori yang akan digunakan hanya untuk ada.

Memori yang didukung file dan paging sesuai permintaan

Android memuat kode yang dapat dieksekusi dari .apk Anda (seperti file .oat atau .so) menggunakan mmap. Artinya, kode tersebut didukung file.

Yang penting, Android menggunakan paging sesuai permintaan. Saat aplikasi Anda dimulai, kernel tidak memuat seluruh APK ke dalam RAM secara langsung. Sebagai gantinya, hanya memetakan file ke ruang alamat virtual proses. Saat aplikasi Anda dieksekusi, dan CPU beralih ke fungsi baru, hal ini akan memicu "kesalahan halaman". Kernel menjeda thread, membaca halaman kode 4 KB tertentu dari penyimpanan ke RAM fisik, dan melanjutkan eksekusi.

Diagram yang menggambarkan paging sesuai permintaan, yang menunjukkan halaman virtual dipetakan ke halaman RAM fisik hanya saat diakses

Artinya, kode yang Anda paketkan tetapi tidak pernah dieksekusi tidak menggunakan memori fisik untuk halaman kode itu sendiri. Namun, library yang tidak digunakan tetap meningkatkan ukuran APK secara keseluruhan dan dapat meningkatkan penggunaan memori secara signifikan oleh metadata internal sistem (seperti indeks DEX dan deskriptor class), yang harus dibaca untuk mengetahui bahwa kode tersebut ada. Selain itu, banyak library berisi penginisialisasi statis atau dipengaruhi oleh framework injeksi dependensi selama startup aplikasi, sehingga library tersebut tetap di-paging ke RAM.

Pengusiran halaman dan pelambatan

Karena memori yang didukung file selalu dapat dibaca ulang dari penyimpanan, kernel menganggap halaman ini "bersih". Saat sistem mengalami tekanan memori, kernel akan mengeluarkan (menghapus) halaman kode bersih ini dari RAM untuk memberi ruang bagi hal lain.

Jika aplikasi Anda kemudian perlu menjalankan kode tersebut lagi, CPU akan mengalami kesalahan, dan kernel harus membaca ulang halaman dari penyimpanan. Makin banyak kode yang dimiliki aplikasi Anda, makin rentan kode tersebut dikeluarkan. Saat pengguna kembali ke aplikasi Anda yang terlalu besar setelah menggunakan aplikasi lain, mereka akan mengalami jank dan pelambatan acak karena CPU terus terhenti menunggu kode dipanggil kembali dari penyimpanan.

Biaya Kesalahan Halaman: Meskipun sangat bervariasi berdasarkan kecepatan penyimpanan perangkat (UFS vs. eMMC) dan status kernel, kesalahan halaman besar (membaca 4 KB dari penyimpanan) dapat berbiaya antara 0,5 md hingga 5 md. Jika jalur startup Anda menyentuh 500 halaman kode yang tidak dioptimalkan, Anda dapat dengan mudah menambahkan latensi I/O murni beberapa ratus milidetik ke waktu peluncuran aplikasi Anda.

Mempelajari ukuran kode dengan Compiler Explorer

Untuk membangun intuisi tentang cara kode Java atau Kotlin Anda diterjemahkan ke kode mesin native (dan dengan demikian byte memori), Anda dapat menggunakan Compiler Explorer.

Dukungan Android dibuat langsung ke dalam Godbolt. Dengan ini, Anda dapat melihat cara berbagai bagian toolchain Android (D8, R8, dan dex2oat) mentransformasi kode sumber Anda.

Cara menggunakan Compiler Explorer dengan Android

  1. Buka godbolt.org.
  2. Pilih Android Java atau Android Kotlin dari dropdown bahasa (kiri atas).
  3. Di dropdown compiler (kanan atas panel kode), Anda dapat memilih berbagai alat:
    • d8: Menampilkan bytecode Dalvik (.dex). Ini adalah representasi terdekat dengan kode asli Anda dan lebih mudah dibaca.
    • r8: Menunjukkan cara pengoptimal R8 menyusutkan dan mengoptimalkan bytecode Anda.
    • dex2oat: Menampilkan kode mesin ARM64 akhir yang benar-benar dieksekusi di perangkat. Di sinilah Anda dapat melihat dampak memori yang sebenarnya (4 byte per petunjuk). dex2oat dapat menargetkan ISA yang berbeda, tetapi ARM64 adalah yang paling umum untuk ponsel.
  4. Penyorotan Input<>Output: Mengarahkan kursor ke baris kode akan menandai petunjuk bytecode atau kode mesin yang sesuai, sehingga memudahkan untuk melacak dampak pernyataan tertentu.
  5. Pipeline Pengoptimalan: Dalam tampilan pembongkaran, Anda dapat mengklik Tambahkan yang baru... -> Opt Pipeline. Hal ini memungkinkan Anda melihat langkah-langkah internal yang dilakukan compiler. Anda dapat memeriksa cara Representasi Internal (IR) diubah pada setiap tahap (misalnya, antara langkah "Inliner (sebelum)" dan "Inliner (setelah)") sebelum diturunkan ke kode mesin ARM64 akhir.

Screenshot UI Compiler Explorer yang menampilkan program contoh dan
disassembly output dex2oat serta pipeline pengoptimalannya dengan langkah inlining
yang ditampilkan

Mengapa hal ini penting untuk memori

Setiap instruksi yang Anda lihat dalam output dex2oat yang menargetkan ARM64 ISA akan menggunakan 4 byte dalam file yang dapat dieksekusi (.odex atau .oat) aplikasi Anda.

Coba masukkan kode yang menggunakan fitur bahasa yang berbeda dan pelajari output compiler:

  • Akses Array vs. Iterator Daftar:
    • Loop array sederhana pada int[] dapat dikompilasi menjadi ~10 petunjuk (~40 byte).
    • Loop foreach pada List secara implisit menggunakan Iterator. Hal ini dapat menghasilkan 30-40 instruksi (~160 byte) karena panggilan metode tambahan (hasNext(), next()) dan alokasi objek iterator itu sendiri.
    • Pengoptimalan R8: Dalam kondisi yang tepat (misalnya, saat List terbukti merupakan ArrayList), pengoptimal R8 dapat mengubah loop foreach kembali menjadi loop berindeks sederhana, sehingga menghilangkan overhead iterator dan mengurangi ukuran kode serta churn memori runtime.
  • Panggilan Metode Virtual: Melibatkan pemuatan class objek, menemukan metode di vtable, lalu melakukan percabangan. Proses ini biasanya memerlukan 4-5 petunjuk (~20 byte).
  • Panggilan Langsung/Statis: Sering kali diterjemahkan ke dalam satu instruksi bl (Cabang dengan Link) (4 byte).
  • Lambda Kotlin: Dapat menghasilkan seluruh class anonim dan metode jembatan tambahan, yang menambahkan ratusan byte overhead kode dan metadata untuk blok fungsional sederhana.

Dengan menggunakan Compiler Explorer, Anda dapat melihat bagaimana fitur bahasa yang canggih (seperti lambda Kotlin, API stream, atau penggunaan generik yang berat) memengaruhi ukuran aplikasi yang dikompilasi akhir, dan bagaimana pengoptimal seperti R8 dapat mengatasi biaya abstraksi bahasa dalam beberapa kasus. Alat ini dapat membantu Anda membuat pertimbangan yang tepat dalam merancang dan menerapkan aplikasi.

Secara umum, semakin kompleks kode aplikasi Anda, semakin tinggi penggunaan memori. Sebaliknya, kode yang lebih sederhana - atau kode yang disederhanakan oleh R8 - menghasilkan representasi yang lebih kecil sebagai instruksi CPU dan byte dalam penyimpanan dan RAM.

Mengukur dampak kode dengan meminfo dan showmap

Anda dapat menggunakan alat memori Android standar untuk melihat seberapa banyak memori yang digunakan oleh kode aplikasi Anda.

dumpsys meminfo

Saat Anda menjalankan adb shell dumpsys meminfo <package>, kategori Code di bagian Ringkasan Aplikasi memberikan tampilan tingkat tinggi tentang memori terkait kode:

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     3244
         Native Heap:     5412
                Code:    24512  # <--- Sum of .so, .dex, .oat, .art, etc.

showmap

Untuk tampilan yang lebih terperinci, gunakan showmap. Hal ini mengungkapkan region di luar file tertentu yang dipetakan ke memori.

adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"

Anda akan melihat entri untuk kode yang dikompilasi aplikasi Anda:

   size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
  12288     8192     8192     8192        0        0        0        0        0 /data/app/.../base.odex

Kode tidak terpakai dan R8

Karena setiap metode yang dijalankan menggunakan memori, aplikasi yang "membengkak" dengan inisialisasi yang tidak perlu atau library yang tidak digunakan dapat sangat memengaruhi performa startup dan penggunaan memori dasar.

Itulah sebabnya alat seperti R8 (ProGuard) sangat penting. R8 menganalisis bytecode aplikasi Anda dan menghapus class atau metode yang tidak pernah dipanggil ("penghapusan kode tidak terpakai").

Latihan interaktif: biaya bloat

Untuk menunjukkan dampak ukuran kode, pertimbangkan eksperimen yang membandingkan dua build aplikasi yang berisi 300 class yang dihasilkan (masing-masing dengan 500 metode):

  • CodeBloat (Tidak Dioptimalkan): Build standar yang tidak dioptimalkan dan berisi semua class yang dihasilkan dan string unik.
  • CodeBloatOptimized: Kode sumber yang sama, tetapi dikompilasi dengan penyingkatan R8 diaktifkan.

1. Kompilasi ahead-of-time (AOT)

Untuk memaksimalkan dampak memori yang didukung file, kami akan menggunakan alat cmd package compile untuk mengompilasi aplikasi ke dalam file .oat dengan kompilasi ahead-of-time (AOT).

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized

Perhatikan bahwa ini adalah contoh sintetis. Biasanya, aplikasi akan menggunakan mode kompilasi speed-profile (lihat lebih lanjut di bawah).

2. Luncurkan dan bandingkan

Untuk melihat mulai benar-benar dingin saat sistem harus membaca kode dari penyimpanan, kita akan menghapus cache halaman kernel sebelum meluncurkan setiap aplikasi. Hal ini memerlukan akses root.

Luncurkan aplikasi yang tidak dioptimalkan:

adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat

Sekarang lakukan hal yang sama untuk aplikasi yang dioptimalkan:

adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
Hasil

Jika melihat baris Code di bagian App Summary, Anda akan melihat perbedaan yang sangat besar:

  • Tidak dioptimalkan Code: ~30.000 KB (30 MB)
  • Dioptimalkan Code: ~2.000 KB (2 MB)

Karena R8 menentukan bahwa 500 metode di dalam class tersebut tidak pernah melakukan sesuatu yang berguna (metode doSomething() hanya memanggil method0(), dan hasilnya diabaikan), R8 menghapus hampir semua kode yang dibuat secara artifisial dari APK akhir.

3. Melihat dampak di Perfetto

Dampak pembengkakan kode terlihat jelas selama fase pemuatan awal aplikasi. Secara khusus, cari slice bindApplication di thread utama, dan slice bertingkat yang dimulai dengan madvising, yang menunjukkan sistem sedang bersiap untuk memuat file dari APK dan kode yang dikompilasinya (.odex).

Saat cold start interaktif, sistem akan memuat kode mmap() dan madvise() serta data lain dari file ini yang diperlukan agar aplikasi dapat dimuat dan dijalankan. Nilai setelah "size=" dalam irisan madvising menunjukkan jumlah data yang perlu dimuat. Pengambilan data kode aplikasi ini dilakukan untuk mempercepat startup aplikasi.

Dari perbandingan tersebut, kita dapat melihat bahwa jumlah kode aplikasi yang perlu dimuat dari penyimpanan ke RAM jauh lebih besar dalam kasus aplikasi yang membengkak, sehingga durasi yang lebih lama berkontribusi pada peluncuran aplikasi yang lebih lambat. Selain itu, rekaman aktivitas saat aplikasi yang terlalu besar dimulai menunjukkan irisan untuk memuat file DEX sekunder (classes2.dex, classes3.dex) yang terpaksa "tumpah" ke dalam aplikasi yang terlalu besar karena tidak muat dalam satu file DEX.

Sebagai perbandingan (cold start di Pixel 10a)
Metrik Tidak dioptimalkan (CodeBloat) Dioptimalkan (CodeBloatOptimized)
base.odex madvise size ~7,9 MB (2,0 md) ~16 KB (0,003 md)
base.apk madvise size ~2,4 MB (2,4 md) ~4 KB (0,001 md)
classes2.dex madvise size ~7,3 MB (8,6 md) T/A
classes3.dex madvise size ~7,3 MB (8,0 md) T/A
Total durasi madvising ~21 md ~0,004 md
Performa pemuatan aplikasi yang tidak dioptimalkan

Screenshot UI Perfetto yang menampilkan proses com.android.codebloat dengan
slice madvising untuk file DEX primer dan sekunder

Performa pemuatan aplikasi yang dioptimalkan

Screenshot UI Perfetto yang menampilkan proses com.android.codebloat.optimized dengan satu slice madvising kecil

Dampak pembengkakan kode bervariasi berdasarkan ukuran aplikasi, karakteristik perangkat pengguna, dan beban sistem.

PerfettoSQL untuk analisis pemuatan

Anda dapat menggunakan kueri berikut untuk mengekstrak metrik ini dari rekaman aktivitas Anda.

1. Durasi peluncuran aplikasi

Ini menunjukkan waktu sejak Aktivitas aplikasi diluncurkan hingga Aktivitas telah menggambar frame pertama.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';

Lihat: Memahami berbagai status peluncuran aplikasi

Durasi startup aplikasi sensitif terhadap banyak faktor selain yang dibahas dalam panduan ini.

2. Mengekstrak ukuran dan durasi madvising

Kueri ini memperbesar bagian madvising yang kita lihat di atas.

INCLUDE PERFETTO MODULE slices.with_context;

SELECT
  name,
  dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
  AND name LIKE 'madvising %';
3. Pengelompokan status thread utama (total durasi per status)

Kueri ini menunjukkan berapa banyak waktu yang dihabiskan thread utama aplikasi dalam berbagai status.

SELECT
  p.name AS process_name,
  state,
  sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
GROUP BY p.name, state;

Anda dapat menyempurnakan kueri untuk hanya melihat status thread utama selama durasi peluncuran aplikasi.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT
  p.name AS process_name,
  ts.state,
  -- Calculate only the duration that falls within the startup window
  SUM(
    MAX(0,
      MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
    )
  ) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
  -- Only select thread states that overlap with the startup interval
  AND ts.ts + ts.dur > s.ts
  AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;

Hal ini dapat memaparkan beberapa masalah menarik, misalnya:

  • Waktu yang dihabiskan untuk Runnable (R) tinggi, tetapi tidak Berjalan: Hal ini menunjukkan bahwa startup aplikasi tertunda oleh pertentangan CPU, yaitu thread utama aplikasi tidak dapat berjalan karena thread lain (mungkin dari aplikasi lain) sedang menggunakan CPU.
  • Waktu yang dihabiskan dalam Tidur yang Dapat Diinterupsi (D) tinggi: Hal ini biasanya menunjukkan I/O lambat atau tekanan memori yang menghentikan peluncuran aplikasi.
  • Waktu yang dihabiskan untuk Tidur (S) tinggi: Artinya, thread utama menunggu thread lain melakukan pekerjaan. Terkadang hal ini menunjukkan pertentangan penguncian di jalur startup aplikasi (yaitu, thread utama diblokir pada resource eksklusif yang digunakan oleh thread lain di aplikasi).
4. Memori yang didukung file maksimum (file RSS)

Metrik ini berkorelasi baik dengan jumlah kode dan data yang dimuat aplikasi saat startup. Aplikasi yang lebih "membengkak" akan mencapai angka yang lebih tinggi di sini, sehingga menyebabkan tekanan memori pada sistem. Tekanan tersebut pada gilirannya dapat menunda startup aplikasi, karena sistem berjuang untuk memenuhi permintaan alokasi, atau mengalihkan waktu CPU dari berfokus pada memulai aplikasi dan ke arah merebut kembali memori dari proses lain untuk memenuhi kebutuhan langsung aplikasi yang dimulai.

SELECT
  p.name AS process_name,
  max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.name = 'mem.rss.file'
GROUP BY p.name;

Mode kompilasi dan memori ART

Android Runtime (ART) dapat mengompilasi kode aplikasi Anda dalam salah satu dari beberapa mode yang berbeda, yang juga dikenal sebagai filter compiler. Filter compiler yang dipilih berdampak langsung pada jejak memori aplikasi Anda.

  • verify: ART hanya melakukan verifikasi bytecode. Tidak ada kompilasi AOT yang dilakukan. Kode dieksekusi melalui Interpreter atau dikompilasi saat runtime oleh compiler JIT.
    • Dampak Memori: Ukuran di disk terkecil. Penggunaan memori kode native didorong ke JIT Cache (memori kotor anonim).
  • speed: ART melakukan kompilasi AOT penuh dari semua metode.
    • Dampak Memori: Ukuran .odex terbesar. Memaksimalkan penggunaan memori yang didukung file (bersih).
  • speed-profile: ART hanya mengompilasi metode yang telah ditandai sebagai "hot" dalam profil JIT.
    • Dampak Memori: Pendekatan seimbang. Hanya kode paling penting yang dikompilasi AOT.

Filter yang paling umum adalah speed-profile, yang digunakan saat menginstal aplikasi pengguna. Hal ini dikonfigurasi di properti sistem pm.dexopt.install dan pm.dexopt.bg-dexopt, dan biasanya ditetapkan di build/make/target/product/runtime_libart.mk.

Beberapa aplikasi sistem akan menggunakan kompilasi speed, dan juga akan dikompilasi pada waktu build image sistem. verify biasanya hanya digunakan dalam kasus penggunaan pengembangan.

Kasus penggunaan Filter compiler umum
Pengembangan verify
Image sistem speed
Aplikasi pengguna speed-profile

Latihan interaktif: mode kompilasi dan memori

Kita dapat menggunakan aplikasi CodeBloat untuk melihat pengaruh filter ini terhadap memori. Untuk mereproduksi pengukuran ini:

  1. Memaksa kompilasi ulang aplikasi ke mode target.
  2. Hentikan secara paksa dan mulai ulang aplikasi.
  3. Tunggu hingga thread latar belakang selesai menyentuh class (lihat logcat atau tunggu 5 detik).
  4. Jalankan adb shell dumpsys meminfo com.android.codebloat.

Mode: verify (tanpa AOT)

adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

Dalam mode verify, Ringkasan aplikasi menampilkan: * PSS Kode: ~8.000 KB * Dalvik Lainnya (JIT): ~25.000 KB

Karena tidak ada kode yang dikompilasi AOT, runtime harus mengompilasi metode aktif JIT ke dalam Cache JIT, yang muncul sebagai memori anonim kotor (Dalvik Other).

Mode: speed (AOT penuh)

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

Dalam mode speed, hasilnya berubah secara drastis: * PSS Kode: ~24.000 KB * Dalvik Lainnya (JIT): ~5.000 KB

Kode aplikasi kini dipetakan dari file .odex sebagai memori yang didukung file bersih. Hal ini mengurangi tekanan pada cache JIT dan membuat memori memenuhi syarat untuk dikeluarkan saat ada tekanan, bukan "macet" sebagai RAM kotor.

Mode: speed-profile (AOT selektif)

Aplikasi modern dapat memaketkan profil dasar pengukuran baseline.prof. ART menggunakan ini untuk mengompilasi secara selektif hanya kode yang diperlukan untuk pengaktifan yang cepat dan hemat memori.

Dalam latihan ini, kita akan membuat profil dasar pengukuran untuk mencantumkan class startup aplikasi. Namun, pada kenyataannya, compiler juga dapat menerima profil dari sumber eksternal seperti dari toko aplikasi ("profil cloud"), yang dapat menyediakan profil JIT yang diperoleh dari banyak sumber untuk aplikasi, terlepas dari apakah developer juga memaketkan profil dasar yang mereka buat.

Membuat dan menggunakan profil di perangkat

Untuk melihat dampak speed-profile, Anda dapat membuat profil Anda sendiri di perangkat:

  1. Reset dan Mulai:

    adb shell am force-stop com.android.codebloat
    
  2. Berinteraksi: Mulai aplikasi dan biarkan aplikasi menjalankan urutan startup-nya.

  3. Dump Profile:

    adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)
    

    (Tindakan ini akan memaksa aplikasi untuk menulis profilnya saat ini ke disk).

  4. Install Profile:

    adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \
    /data/misc/profiles/ref/com.android.codebloat/primary.prof
    
  5. Kompilasi:

    adb shell cmd package compile -m speed-profile -f com.android.codebloat
    

Saat meluncurkan lagi, Anda akan melihat keseimbangan: PSS Kode akan lebih rendah daripada speed (misalnya, ~16.000 KB) karena hanya metode peluncuran "hot" yang dikompilasi, sehingga sisanya ditangani oleh interpreter atau JIT hanya jika benar-benar digunakan.

Lihat:

Mempelajari kode yang dikompilasi secara mendalam

Jika Anda ingin melihat dengan tepat petunjuk yang dihasilkan ART, lihat art/DISASSEMBLY_GUIDE.md.

Panduan ini memberikan petunjuk mendetail tentang cara menggunakan:

  • oatdump: Untuk melihat petunjuk ARM64 di dalam file .odex yang ada.
  • dex2oat: Untuk menyimulasikan kompilasi dengan tanda debug verbose.

Latihan: penyisipan kode

Salah satu alasan kode yang dikompilasi dapat bertambah secara tidak terduga adalah penyisipan metode. Compiler dapat memutuskan untuk menyalin isi metode kecil yang sering dipanggil langsung ke pemanggilnya.

Di aplikasi CodeBloat, metode doSomething() di setiap class yang dihasilkan hanya memanggil method0(). Saat dikompilasi dalam mode speed, compiler Pengoptimalan ART kemungkinan akan menyisipkan method0() ke dalam doSomething().

Latihan: Verifikasi ini menggunakan oatdump di perangkat Anda:

# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk

adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]

# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
                  --class-filter=com.android.codebloat.GeneratedClass0

Cari metode doSomething di output. Jika di-inline, Anda akan melihat petunjuk untuk memuat konstanta string panjang secara langsung dalam doSomething, bukan petunjuk bl yang menargetkan method0.

Memvisualisasikan pengoptimalan (CFG)

Untuk melihat secara tepat kapan compiler memutuskan untuk menyisipkan metode, Anda dapat membuat Grafik Alur Kontrol (CFG). Bagian ini menunjukkan status kode di setiap tahap pipeline pengoptimalan, dengan setiap transformasi melalui Representasi Menengah (IR) compiler hingga kode diturunkan ke ISA target (misalnya, ARM64).

  1. Jalankan dex2oat dengan tanda dump: Gunakan tanda --verbose-methods untuk membatasi output ke metode tertentu; jika tidak, file .cfg untuk aplikasi besar dapat bertambah hingga beberapa gigabita.

    # Substitution of actual paths required:
    adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \
                        --oat-file=/data/local/tmp/dump.odex \
                        --compiler-filter=speed \
                        --dump-cfg=/data/local/tmp/codebloat.cfg \
                        --verbose-methods=doSomething
    
  2. Tarik dan Lihat: Tarik file .cfg ke workstation Anda dan buka dengan IR Hydra.

  3. Menemukan Inliner: Di IR Hydra, muat artefak kompilasi dan telusuri doSomething. Bandingkan representasi sebelum dan setelah penerusan Inliner. Anda akan melihat grafik meluas saat petunjuk dari method0 digabungkan ke pemanggil.

Atau, gunakan alat Opt Pipeline di Compiler Explorer (seperti yang dijelaskan di bagian atas) dan masukkan kode serupa untuk melihat transformasi serupa yang dilakukan pada pass Inliner.

Latihan: kolom volatile dan penghalang memori

Di aplikasi MemoryLab, kolom mGarbageSink ditandai sebagai volatile. Hal ini memastikan bahwa compiler tidak mengoptimalkan alokasi sampah kita.

public volatile byte[] mGarbageSink;

Dalam disassembly ARM64, Anda akan melihat bahwa setiap penyimpanan ke kolom ini disertai dengan Penghalang Memori (dmb ish) atau penggunaan instruksi Load-Acquire/Store-Release (ldar/stlr). Hal ini memastikan visibilitas thread, tetapi menambahkan beberapa instruksi tambahan ke setiap akses, sehingga ukuran kode sedikit meningkat dibandingkan dengan kolom biasa.

Latihan: Temukan akses kolom dan penghalang memori terkait dalam disassembly.

Latihan: pemeriksaan penangguhan implisit

Jika Anda membongkar loop, seperti yang ada di generateAllocationChurn, Anda akan melihat petunjuk aneh di akhir isi loop:

ldr x21, [x21]

Ini adalah Pemeriksaan Penangguhan Implisit. ART menggunakannya untuk memungkinkan Pengumpul Sampah menjeda thread dengan aman. Register x21 biasanya mengarah ke dirinya sendiri. Saat GC perlu menangguhkan thread, GC akan "meracuni" lokasi memori tersebut. Lain kali saat thread menjalankan ldr tersebut, thread akan memicu kesalahan, yang ditangkap oleh runtime dan digunakan untuk mentransisikan thread ke status ditangguhkan.

Pola ini diulang di setiap loop dan di awal setiap metode, sehingga berkontribusi pada ukuran kode total aplikasi Anda.

Latihan: Temukan semua pemeriksaan penangguhan implisit dalam disassembly metode, dan coba korelasikan dengan kode sumber asli.


← WebView | ↑ Atas | Threads →