Wywołania interfejsu Android API zwykle wiążą się ze znacznym opóźnieniem i obciążeniem obliczeniowym na wywołanie. Dlatego podczas projektowania interfejsów API, które są przydatne, poprawne i wydajne, należy wziąć pod uwagę buforowanie po stronie klienta.
Motywacja
Interfejsy API udostępniane deweloperom aplikacji w pakiecie Android SDK są często implementowane jako kod klienta w platformie Android, który wywołuje Binder IPC do usługi systemowej w procesie platformy. Zadaniem tej usługi jest wykonanie pewnych obliczeń i zwrócenie wyniku do klienta. Opóźnienie tej operacji zależy zwykle od 3 czynników:
- Obciążenie IPC: podstawowe wywołanie IPC jest zwykle 10 tys. razy dłuższe niż podstawowe wywołanie metody w procesie.
- Konkurencja po stronie serwera: praca wykonywana w usłudze systemowej w odpowiedzi na żądanie klienta może nie rozpocząć się od razu, np. jeśli wątek serwera jest zajęty obsługą innych żądań, które dotarły wcześniej.
- Obliczenia po stronie serwera: obsługa żądania na serwerze może wymagać znacznego nakładu pracy.
Możesz wyeliminować wszystkie 3 te czynniki opóźnienia, implementując pamięć podręczną po stronie klienta, pod warunkiem że pamięć podręczna jest:
- Poprawna: pamięć podręczna po stronie klienta nigdy nie zwraca wyników, które różniłyby się od tych, które zwróciłby serwer.
- Skuteczna: żądania klientów są często obsługiwane z pamięci podręcznej, np. pamięć podręczna ma wysoki współczynnik trafień.
- Wydajna: pamięć podręczna po stronie klienta efektywnie wykorzystuje zasoby po stronie klienta, np. przez kompaktowe reprezentowanie danych w pamięci podręcznej i nieprzechowywanie zbyt wielu wyników w pamięci podręcznej ani nieaktualnych danych w pamięci klienta.
Rozważ buforowanie wyników serwera po stronie klienta
Jeśli klienci często wysyłają dokładnie to samo żądanie wiele razy, a zwracana wartość nie zmienia się z upływem czasu, zaimplementuj pamięć podręczną w bibliotece klienta, która będzie kluczowana przez parametry żądania.
W implementacji rozważ użycie IpcDataCache:
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);
}
}
Pełny przykład znajdziesz w android.app.admin.DevicePolicyManager.
Klasa IpcDataCache jest dostępna dla całego kodu systemowego, w tym dla modułów głównych.
Istnieje też klasa PropertyInvalidatedCache, która jest prawie identyczna, ale widoczna tylko dla platformy. Jeśli to możliwe, używaj klasy IpcDataCache.
Unieważnianie pamięci podręcznych w przypadku zmian po stronie serwera
Jeśli wartość zwracana przez serwer może się zmieniać z upływem czasu, zaimplementuj wywołanie zwrotne do obserwowania zmian i zarejestruj je, aby móc odpowiednio unieważnić pamięć podręczną po stronie klienta.
Unieważnianie pamięci podręcznych między przypadkami testów jednostkowych
W pakiecie testów jednostkowych możesz testować kod klienta na podstawie testu podwójnego, a nie rzeczywistego serwera. Jeśli tak jest, pamiętaj, aby wyczyścić wszystkie pamięci podręczne po stronie klienta między przypadkami testowymi. Dzięki temu przypadki testowe będą wzajemnie hermetyczne, a jeden przypadek testowy nie będzie zakłócał działania innego.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
Podczas pisania testów CTS, które sprawdzają klienta interfejsu API korzystającego wewnętrznie z buforowania, pamięć podręczna jest szczegółem implementacji, który nie jest udostępniany autorowi interfejsu API. Dlatego testy CTS nie powinny wymagać specjalnej wiedzy o buforowaniu używanym w kodzie klienta.
Analizowanie trafień i nietrafień w pamięci podręcznej
Klasy IpcDataCache i PropertyInvalidatedCache mogą wyświetlać statystyki na żywo:
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
...
Pola
Trafienia:
- Definicja: liczba przypadków, w których żądany fragment danych został znaleziony w pamięci podręcznej.
- Znaczenie: wskazuje na wydajne i szybkie pobieranie danych, co ogranicza niepotrzebne pobieranie danych.
- Większe liczby są na ogół lepsze.
Czyszczenia:
- Definicja: liczba przypadków, w których pamięć podręczna została wyczyszczona z powodu unieważnienia.
- Powody czyszczenia:
- Unieważnienie: nieaktualne dane z serwera.
- Zarządzanie miejscem: zwalnianie miejsca na nowe dane, gdy pamięć podręczna jest pełna.
- Wysokie liczby mogą wskazywać na często zmieniające się dane i potencjalną nieefektywność.
Nietrafienia:
- Definicja: liczba przypadków, w których pamięć podręczna nie dostarczyła żądanych danych.
- Przyczyny:
- Niewydajne buforowanie: pamięć podręczna jest zbyt mała lub nie przechowuje odpowiednich danych.
- Często zmieniające się dane.
- Żądania po raz pierwszy.
- Wysokie liczby sugerują potencjalne problemy z buforowaniem.
Pominięcia:
- Definicja: przypadki, w których pamięć podręczna nie była w ogóle używana, mimo że mogła być.
- Powody pominięcia:
- Corking: dotyczy aktualizacji systemu zarządzania pakietami Androida, celowe wyłączenie buforowania z powodu dużej liczby wywołań podczas uruchamiania.
- Nieużywane: pamięć podręczna istnieje, ale nie została zainicjowana. Wartość nonce została usunięta, co oznacza, że pamięć podręczna nigdy nie została unieważniona.
- Ominięcie: celowa decyzja o pominięciu pamięci podręcznej.
- Wysokie liczby wskazują na potencjalną nieefektywność w korzystaniu z pamięci podręcznej.
Unieważnienia:
- Definicja: proces oznaczania danych w pamięci podręcznej jako nieaktualnych.
- Znaczenie: sygnał, że system działa na podstawie najbardziej aktualnych danych, co zapobiega błędom i niespójnościom.
- Zwykle wywoływane przez serwer, który jest właścicielem danych.
Aktualny rozmiar:
- Definicja: aktualna liczba elementów w pamięci podręcznej.
- Znaczenie: wskazuje na wykorzystanie zasobów przez pamięć podręczną i potencjalny wpływ na wydajność systemu.
- Wyższe wartości zwykle oznaczają, że pamięć podręczna używa więcej pamięci.
Maksymalny rozmiar:
- Definicja: maksymalna ilość miejsca przydzielona na pamięć podręczną.
- Znaczenie: określa pojemność pamięci podręcznej i jej zdolność do przechowywania danych.
- Ustawienie odpowiedniego maksymalnego rozmiaru pomaga zrównoważyć skuteczność pamięci podręcznej z wykorzystaniem pamięci. Po osiągnięciu maksymalnego rozmiaru nowy element jest dodawany przez usunięcie najrzadziej używanego elementu, co może wskazywać na nieefektywność.
Wysoki poziom:
- Definicja: maksymalny rozmiar osiągnięty przez pamięć podręczną od momentu jej utworzenia.
- Znaczenie: pozwala uzyskać wgląd w szczytowe wykorzystanie pamięci podręcznej i potencjalne obciążenie pamięci.
- Monitorowanie wysokiego poziomu może pomóc w identyfikowaniu potencjalnych wąskich gardeł lub obszarów do optymalizacji.
Przepełnienia:
- Definicja: liczba przypadków, w których pamięć podręczna przekroczyła maksymalny rozmiar i musiała usunąć dane, aby zrobić miejsce na nowe wpisy.
- Znaczenie: wskazuje na obciążenie pamięci podręcznej i potencjalne pogorszenie wydajności z powodu usuwania danych.
- Wysoka liczba przepełnień sugeruje, że może być konieczne dostosowanie rozmiaru pamięci podręcznej lub ponowne ocenienie strategii buforowania.
Te same statystyki można też znaleźć w raporcie o błędach.
Dostosowywanie rozmiaru pamięci podręcznej
Pamięci podręczne mają maksymalny rozmiar. Po przekroczeniu maksymalnego rozmiaru pamięci podręcznej wpisy są usuwane w kolejności LRU.
- Buforowanie zbyt małej liczby wpisów może negatywnie wpłynąć na współczynnik trafień w pamięci podręcznej.
- Buforowanie zbyt dużej liczby wpisów zwiększa wykorzystanie pamięci przez pamięć podręczną.
Znajdź odpowiednią równowagę w swoim przypadku użycia.
Eliminowanie zbędnych wywołań klienta
Klienci mogą wysyłać to samo zapytanie do serwera wiele razy w krótkim czasie:
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();
}
}
Rozważ ponowne użycie wyników z poprzednich wywołań:
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();
}
}
Rozważ zapamiętywanie po stronie klienta ostatnich odpowiedzi serwera
Aplikacje klienckie mogą wysyłać zapytania do interfejsu API szybciej niż serwer interfejsu API może generować nowe odpowiedzi. W takim przypadku skutecznym rozwiązaniem jest zapamiętywanie ostatniej odpowiedzi serwera po stronie klienta wraz ze znacznikiem czasu i zwracanie zapamiętanego wyniku bez wysyłania zapytania do serwera, jeśli zapamiętany wynik jest wystarczająco aktualny. Czas trwania zapamiętywania może określić autor klienta interfejsu API.
Aplikacja może na przykład wyświetlać użytkownikowi statystyki ruchu w sieci, wysyłając zapytanie o statystyki w każdej rysowanej klatce:
@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()));
}
Aplikacja może rysować klatki z częstotliwością 60 Hz. Jednak hipotetycznie kod klienta w TrafficStats może wysyłać zapytanie do serwera o statystyki co najwyżej raz na sekundę, a jeśli zapytanie zostanie wysłane w ciągu sekundy od poprzedniego zapytania, zwracać ostatnią widzianą wartość.
Jest to dozwolone, ponieważ dokumentacja API nie zawiera żadnych informacji o aktualności zwracanych wyników.
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
Rozważ generowanie kodu po stronie klienta zamiast zapytań do serwera
Jeśli wyniki zapytania są znane serwerowi w czasie kompilacji, zastanów się, czy są one znane również klientowi w czasie kompilacji, i rozważ, czy interfejs API można zaimplementować w całości po stronie klienta.
Rozważ następujący kod aplikacji, który sprawdza, czy urządzenie jest zegarkiem (czyli czy urządzenie działa w systemie Wear OS):
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Ta właściwość urządzenia jest znana w czasie kompilacji, a konkretnie w momencie, gdy platforma została skompilowana na potrzeby obrazu rozruchowego tego urządzenia. Kod po stronie klienta dla hasSystemFeature może natychmiast zwrócić znany wynik, zamiast wysyłać zapytanie do zdalnej usługi systemowej PackageManager.
Usuwanie duplikatów wywołań zwrotnych serwera po stronie klienta
Na koniec klient interfejsu API może zarejestrować wywołania zwrotne na serwerze interfejsu API, aby otrzymywać powiadomienia o zdarzeniach.
Aplikacje zwykle rejestrują wiele wywołań zwrotnych dla tych samych podstawowych informacji. Zamiast wysyłać powiadomienie do klienta raz na zarejestrowane wywołanie zwrotne za pomocą IPC, biblioteka klienta powinna mieć jedno zarejestrowane wywołanie zwrotne za pomocą IPC z serwerem, a następnie powiadamiać każde zarejestrowane wywołanie zwrotne w aplikacji.
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"];
}
}