Panggilan API Android biasanya melibatkan latensi dan komputasi yang signifikan per pemanggilan. Oleh karena itu, penyimpanan data ke dalam cache sisi klien merupakan pertimbangan penting dalam merancang API yang bermanfaat, benar, dan berperforma tinggi.
Motivasi
API yang diekspos ke developer aplikasi di Android SDK sering kali diimplementasikan sebagai kode klien di Android Framework yang membuat panggilan Binder IPC ke layanan sistem dalam proses platform, yang tugasnya adalah melakukan beberapa komputasi dan mengembalikan hasil ke klien. Latensi operasi ini biasanya dipengaruhi oleh tiga faktor:
- Overhead IPC: panggilan IPC dasar biasanya 10.000x latensi panggilan metode dalam proses dasar.
- Persaingan sisi server: pekerjaan yang dilakukan di layanan sistem sebagai respons terhadap permintaan klien mungkin tidak segera dimulai, misalnya jika thread server sedang menangani permintaan lain yang tiba lebih awal.
- Komputasi sisi server: tugas itu sendiri untuk menangani permintaan di server mungkin memerlukan banyak tugas.
Anda dapat menghilangkan ketiga faktor latensi ini dengan menerapkan cache di sisi klien, asalkan cache tersebut:
- Benar: cache sisi klien tidak pernah menampilkan hasil yang akan berbeda dengan yang akan ditampilkan server.
- Efektif: permintaan klien sering kali ditayangkan dari cache, misalnya cache memiliki rasio hit yang tinggi.
- Efisien: cache sisi klien menggunakan resource sisi klien secara efisien, seperti dengan merepresentasikan data yang di-cache secara ringkas dan dengan tidak menyimpan terlalu banyak hasil yang di-cache atau data yang sudah tidak berlaku di memori klien.
Pertimbangkan untuk menyimpan hasil server dalam cache di klien
Jika klien sering membuat permintaan yang sama persis beberapa kali, dan nilai yang ditampilkan tidak berubah dari waktu ke waktu, Anda harus menerapkan cache di library klien yang dikunci oleh parameter permintaan.
Sebaiknya gunakan IpcDataCache dalam penerapan Anda:
public class BirthdayManager {
private final IpcDataCache.QueryHandler<User, Birthday> mBirthdayQuery =
new IpcDataCache.QueryHandler<User, Birthday>() {
@Override
public Birthday apply(User user) {
return mService.getBirthday(user);
}
};
private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
private static final String BDAY_API = "getUserBirthday";
private final IpcDataCache<User, Birthday> mCache
new IpcDataCache<User, Birthday>(
BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery);
/** @hide **/
@VisibleForTesting
public static void clearCache() {
IpcDataCache.invalidateCache(MODULE_SYSTEM, BDAY_API);
}
public Birthday getBirthday(User user) {
return mCache.query(user);
}
}
Untuk contoh lengkap, lihat android.app.admin.DevicePolicyManager.
IpcDataCache tersedia untuk semua kode sistem, termasuk modul utama.
Ada juga PropertyInvalidatedCache yang hampir identik, tetapi hanya
terlihat oleh framework. Pilih IpcDataCache jika memungkinkan.
Membatalkan validasi cache pada perubahan sisi server
Jika nilai yang ditampilkan dari server dapat berubah seiring waktu, terapkan callback untuk mengamati perubahan, dan daftarkan callback agar Anda dapat membatalkan validasi cache sisi klien dengan tepat.
Membatalkan validasi cache di antara kasus pengujian unit
Dalam rangkaian pengujian unit, Anda dapat menguji kode klien terhadap test double, bukan server sebenarnya. Jika ya, pastikan untuk menghapus cache sisi klien di antara kasus pengujian. Hal ini dilakukan untuk menjaga kasus pengujian tetap hermetik satu sama lain, dan mencegah satu kasus pengujian mengganggu kasus pengujian lainnya.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
Saat menulis tes CTS yang melatih klien API yang menggunakan caching secara internal, cache adalah detail implementasi yang tidak diekspos ke penulis API, sehingga tes CTS tidak memerlukan pengetahuan khusus tentang caching yang digunakan dalam kode klien.
Mempelajari hit dan miss cache
IpcDataCache dan PropertyInvalidatedCache dapat mencetak statistik langsung:
adb shell dumpsys cacheinfo
...
Cache Name: cache_key.is_compat_change_enabled
Property: cache_key.is_compat_change_enabled
Hits: 1301458, Misses: 21387, Skips: 0, Clears: 39
Skip-corked: 0, Skip-unset: 0, Skip-bypass: 0, Skip-other: 0
Nonce: 0x856e911694198091, Invalidates: 72, CorkedInvalidates: 0
Current Size: 1254, Max Size: 2048, HW Mark: 2049, Overflows: 310
Enabled: true
...
Kolom
Hits:
- Definisi: Jumlah permintaan data yang berhasil ditemukan dalam cache.
- Signifikansi: Menunjukkan pengambilan data yang efisien dan cepat, sehingga mengurangi pengambilan data yang tidak perlu.
- Jumlah yang lebih tinggi umumnya lebih baik.
Menghapus:
- Definisi: Jumlah frekuensi cache dihapus karena pembatalan validasi.
- Alasan Penghapusan:
- Pembatalan: Data yang sudah tidak berlaku dari server.
- Pengelolaan Ruang: Menyediakan ruang untuk data baru saat cache penuh.
- Jumlah yang tinggi dapat menunjukkan data yang sering berubah dan potensi inefisiensi.
Tidak Cocok:
- Definisi: Jumlah kegagalan cache dalam menyediakan data yang diminta.
- Penyebab:
- Penyimpanan dalam cache yang tidak efisien: Cache terlalu kecil atau tidak menyimpan data yang tepat.
- Data yang sering berubah.
- Permintaan pertama.
- Jumlah yang tinggi menunjukkan potensi masalah caching.
Lewatan:
- Definisi: Instance saat cache tidak digunakan sama sekali, meskipun cache dapat digunakan.
- Alasan melewati:
- Corking: Khusus untuk update Pengelola Paket Android, sengaja menonaktifkan caching karena volume panggilan yang tinggi selama booting.
- Tidak disetel: Cache ada, tetapi tidak diinisialisasi. Nonce tidak disetel, yang berarti cache tidak pernah dibatalkan.
- Lewati: Keputusan yang disengaja untuk melewati cache.
- Jumlah yang tinggi menunjukkan potensi inefisiensi dalam penggunaan cache.
Membatalkan:
- Definisi: Proses menandai data yang di-cache sebagai tidak berlaku atau sudah tidak relevan.
- Signifikansi: Memberikan sinyal bahwa sistem bekerja dengan data terbaru, sehingga mencegah error dan inkonsistensi.
- Biasanya dipicu oleh server yang memiliki data.
Ukuran Saat Ini:
- Definisi: Jumlah elemen saat ini dalam cache.
- Signifikansi: Menunjukkan pemakaian resource cache dan potensi dampaknya terhadap performa sistem.
- Nilai yang lebih tinggi umumnya berarti lebih banyak memori yang digunakan oleh cache.
Ukuran Maksimum:
- Definisi: Jumlah ruang maksimum yang dialokasikan untuk cache.
- Signifikansi: Menentukan kapasitas cache dan kemampuannya untuk menyimpan data.
- Menetapkan ukuran maks yang sesuai membantu menyeimbangkan efikasi cache dengan penggunaan memori. Setelah ukuran maksimum tercapai, elemen baru ditambahkan dengan mengeluarkan elemen yang paling jarang digunakan, yang dapat menunjukkan inefisiensi.
Tanda Air Tinggi:
- Definisi: Ukuran maksimum yang dicapai oleh cache sejak pembuatannya.
- Signifikansi: Memberikan insight tentang penggunaan cache puncak dan potensi tekanan memori.
- Memantau tanda batas atas dapat membantu mengidentifikasi potensi hambatan atau area untuk pengoptimalan.
Overflow:
- Definisi: Jumlah berapa kali cache melampaui ukuran maksimumnya dan harus mengeluarkan data untuk menyediakan ruang bagi entri baru.
- Signifikansi: Menunjukkan tekanan cache dan potensi penurunan performa karena penghapusan data.
- Jumlah luapan yang tinggi menunjukkan bahwa ukuran cache mungkin perlu disesuaikan atau strategi caching dievaluasi ulang.
Statistik yang sama juga dapat ditemukan dalam laporan bug.
Menyesuaikan ukuran cache
Cache memiliki ukuran maksimum. Jika ukuran cache maksimum terlampaui, entri akan dikeluarkan dalam urutan LRU.
- Menyimpan terlalu sedikit entri ke dalam cache dapat berdampak negatif pada rasio hit cache.
- Menyimpan terlalu banyak entri ke dalam cache akan meningkatkan penggunaan memori cache.
Temukan keseimbangan yang tepat untuk kasus penggunaan Anda.
Menghilangkan panggilan klien yang berlebihan
Klien dapat membuat kueri yang sama ke server beberapa kali dalam rentang waktu singkat:
public void executeAll(List<Operation> operations) throws SecurityException {
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException("Missing permission " + permission);
}
}
op.execute();
}
}
Pertimbangkan untuk menggunakan kembali hasil dari panggilan sebelumnya:
public void executeAll(List<Operation> operations) throws SecurityException {
Set<Permission> permissionsChecked = new HashSet<>();
for (Operation op : operations) {
for (Permission permission : op.requiredPermissions()) {
if (!permissionsChecked.add(permission)) {
if (!permissionChecker.checkPermission(permission, ...)) {
throw new SecurityException(
"Missing permission " + permission);
}
}
}
op.execute();
}
}
Pertimbangkan memoisasi sisi klien untuk respons server terbaru
Aplikasi klien dapat membuat kueri API dengan kecepatan yang lebih tinggi daripada yang dapat dilakukan server API untuk menghasilkan respons baru yang bermakna. Dalam hal ini, pendekatan yang efektif adalah memoriisasi respons server yang terakhir terlihat di sisi klien beserta stempel waktunya, dan mengembalikan hasil yang dimemoriisasi tanpa mengkueri server jika hasil yang dimemoriisasi cukup baru. Penulis klien API dapat menentukan durasi memoization.
Misalnya, aplikasi dapat menampilkan statistik traffic jaringan kepada pengguna dengan mengirim kueri untuk statistik di setiap frame yang digambar:
@UiThread
private void setStats() {
mobileRxBytesTextView.setText(
Long.toString(TrafficStats.getMobileRxBytes()));
mobileRxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileRxPackages()));
mobileTxBytesTextView.setText(
Long.toString(TrafficStats.getMobileTxBytes()));
mobileTxPacketsTextView.setText(
Long.toString(TrafficStats.getMobileTxPackages()));
}
Aplikasi dapat menggambar frame pada 60 Hz. Namun, secara hipotetis, kode klien di
TrafficStats dapat memilih untuk meminta statistik dari server paling banyak sekali per detik,
dan jika dikueri dalam waktu satu detik dari kueri sebelumnya, akan menampilkan nilai yang terakhir terlihat.
Hal ini diizinkan karena dokumentasi API tidak memberikan kontrak apa pun terkait keaktualan hasil yang ditampilkan.
participant App code as app
participant Client library as clib
participant Server as server
app->clib: request @ T=100ms
clib->server: request
server->clib: response 1
clib->app: response 1
app->clib: request @ T=200ms
clib->app: response 1
app->clib: request @ T=300ms
clib->app: response 1
app->clib: request @ T=2000ms
clib->server: request
server->clib: response 2
clib->app: response 2
Pertimbangkan codegen sisi klien, bukan kueri server
Jika hasil kueri dapat diketahui oleh server pada waktu build, pertimbangkan apakah hasil tersebut juga dapat diketahui oleh klien pada waktu build, dan pertimbangkan apakah API dapat diimplementasikan sepenuhnya di sisi klien.
Pertimbangkan kode aplikasi berikut yang memeriksa apakah perangkat adalah smartwatch (yaitu, perangkat menjalankan Wear OS):
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Properti perangkat ini diketahui pada waktu build, khususnya pada saat
Framework dibangun untuk image booting perangkat ini. Kode sisi klien
untuk hasSystemFeature dapat langsung menampilkan hasil yang diketahui, bukan
mengirim kueri ke layanan sistem PackageManager jarak jauh.
Menghapus duplikat callback server di klien
Terakhir, klien API dapat mendaftarkan callback dengan server API untuk menerima notifikasi peristiwa.
Aplikasi biasanya mendaftarkan beberapa callback untuk informasi mendasar yang sama. Daripada membuat server memberi tahu klien sekali per callback terdaftar menggunakan IPC, library klien harus memiliki satu callback terdaftar menggunakan IPC dengan server, lalu memberi tahu setiap callback terdaftar di aplikasi.
digraph d_front_back {
rankdir=RL;
node [style=filled, shape="rectangle", fontcolor="white" fontname="Roboto"]
server->clib
clib->c1;
clib->c2;
clib->c3;
subgraph cluster_client {
graph [style="dashed", label="Client app process"];
c1 [label="my.app.FirstCallback" color="#4285F4"];
c2 [label="my.app.SecondCallback" color="#4285F4"];
c3 [label="my.app.ThirdCallback" color="#4285F4"];
clib [label="android.app.FooManager" color="#F4B400"];
}
subgraph cluster_server {
graph [style="dashed", label="Server process"];
server [label="com.android.server.FooManagerService" color="#0F9D58"];
}
}