Wywołania interfejsu Android API zwykle wiążą się ze znacznym opóźnieniem i obliczeniami na wywołanie. Pamięć podręczna po stronie klienta jest zatem ważnym czynnikiem przy projektowaniu interfejsów API, które są przydatne, prawidłowe i wydajne.
Motywacja
Interfejsy API udostępniane deweloperom aplikacji w pakiecie Android SDK są często implementowane jako kod klienta w Android Framework, który wykonuje wywołanie Binder IPC do usługi systemowej w procesie platformy. Zadaniem tej usługi jest wykonanie obliczeń i zwrócenie wyniku do klienta. Na czas oczekiwania na tę operację mają zwykle wpływ 3 czynniki:
- Narzucone obciążenie IPC: podstawowe wywołanie IPC jest zwykle 10 000 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 nadeszły wcześniej.
 - Obliczenia po stronie serwera: obsługa żądania na serwerze może wymagać znacznego nakładu pracy.
 
Wszystkie 3 czynniki opóźnienia możesz wyeliminować, wdrażając pamięć podręczną po stronie klienta, pod warunkiem że:
- Prawidłowo: 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.
 - Skuteczność: żądania klientów są często obsługiwane z pamięci podręcznej, np. pamięć podręczna ma wysoki wskaźnik trafień.
 - Wydajność: 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 na urządzeniu klienta
Jeśli klienci często wysyłają to samo żądanie wiele razy, a zwracana wartość nie zmienia się z czasem, w bibliotece klienta należy zaimplementować pamięć podręczną z kluczem opartym na parametrach żądania.
W implementacji możesz użyć 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 sekcji android.app.admin.DevicePolicyManager.
IpcDataCache jest dostępny dla całego kodu systemowego, w tym modułów głównych.
Istnieje też PropertyInvalidatedCache, który jest niemal identyczny, ale jest widoczny tylko dla platformy. W miarę możliwości używaj IpcDataCache.
Unieważnianie pamięci podręcznych w przypadku zmian po stronie serwera
Jeśli wartość zwracana z serwera może się zmieniać z czasem, wdróż wywołanie zwrotne do obserwowania zmian i zarejestruj je, aby móc odpowiednio unieważniać pamięć podręczną po stronie klienta.
Unieważnianie pamięci podręcznych między przypadkami testów jednostkowych
W zestawie testów jednostkowych możesz testować kod klienta w odniesieniu do obiektu testowego zamiast do prawdziwego serwera. Jeśli tak, pamiętaj, aby wyczyścić pamięć podręczną po stronie klienta między przypadkami testowymi. Ma to na celu zachowanie wzajemnej hermetyczności przypadków testowych i uniemożliwienie jednemu przypadkowi testowemu zakłócania 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 pamięci podręcznej, 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 pamięci podręcznej używanej w kodzie klienta.
Analizowanie trafień i nietrafień w pamięci podręcznej
IpcDataCache i PropertyInvalidatedCache mogą drukować 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
  ...
Fieldsem
Wyświetlenia:
- Definicja: liczba przypadków, w których żądane dane zostały znalezione w pamięci podręcznej.
 - Znaczenie: wskazuje wydajne i szybkie pobieranie danych, co ogranicza niepotrzebne pobieranie danych.
 - Wyższe wartości są zwykle lepsze.
 
Wyczyść:
- Definicja: liczba przypadków wyczyszczenia pamięci podręcznej z powodu unieważnienia.
 - Przyczyny czyszczenia:
- Unieważnienie: nieaktualne dane z serwera.
 - Zarządzanie miejscem: zwalnianie miejsca na nowe dane, gdy pamięć podręczna jest pełna.
 
 - Wysokie wartości mogą wskazywać na często zmieniające się dane i potencjalną nieefektywność.
 
Nie pasuje do wyrażeń:
- Definicja: liczba przypadków, w których pamięć podręczna nie dostarczyła żądanych danych.
 - Przyczyny:
- Nieefektywne buforowanie: za mała pamięć podręczna lub brak przechowywania odpowiednich danych.
 - Dane, które często się zmieniają.
 - Pierwsze prośby.
 
 - Wysokie wartości sugerują potencjalne problemy z buforowaniem.
 
Pominięcia:
- Definicja: przypadki, w których pamięć podręczna nie została użyta, mimo że mogła.
 - Przyczyny pominięcia:
- Corking: dotyczy aktualizacji Menedżera pakietów Androida. Celowe wyłączanie buforowania z powodu dużej liczby wywołań podczas uruchamiania.
 - Nieustawione: pamięć podręczna istnieje, ale nie została zainicjowana. Wartość nonce nie została ustawiona, co oznacza, że pamięć podręczna nigdy nie została unieważniona.
 - Pominięcie: świadoma decyzja o pominięciu pamięci podręcznej.
 
 - Wysokie wartości wskazują na potencjalne problemy z wydajnością korzystania z pamięci podręcznej.
 
Unieważnia:
- Definicja: proces oznaczania danych w pamięci podręcznej jako nieaktualnych lub przestarzałych.
 - Znaczenie: sygnał, że system działa na podstawie najbardziej aktualnych danych, co zapobiega błędom i niespójnościom.
 - Zwykle jest 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 wykorzystanie zasobów pamięci podręcznej i potencjalny wpływ na wydajność systemu.
 - Wyższe wartości zwykle oznaczają, że pamięć podręczna wykorzystuje 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 zachować równowagę między skutecznością pamięci podręcznej a 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ść.
 
Najwyższy poziom wody:
- Definicja: maksymalny rozmiar osiągnięty przez pamięć podręczną od momentu jej utworzenia.
 - Znaczenie: dostarcza informacji o maksymalnym wykorzystaniu pamięci podręcznej i potencjalnym obciążeniu pamięci.
 - Monitorowanie najwyższego poziomu może pomóc w identyfikowaniu potencjalnych wąskich gardeł lub obszarów wymagających optymalizacji.
 
Przekroczenia:
- 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 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 ponowna ocena strategii buforowania.
 
Te same statystyki znajdziesz też w raporcie o błędach.
Dostosowywanie rozmiaru pamięci podręcznej
Pamięci podręczne mają maksymalny rozmiar. Gdy maksymalny rozmiar pamięci podręcznej zostanie przekroczony, wpisy są usuwane w kolejności LRU.
- Zbyt mała liczba wpisów w pamięci podręcznej może negatywnie wpłynąć na współczynnik trafień w pamięci podręcznej.
 - Zbyt wiele wpisów w pamięci podręcznej zwiększa jej wykorzystanie pamięci.
 
Znajdź odpowiednią równowagę w swoim przypadku użycia.
Eliminowanie zbędnych wywołań klienta
Klienci mogą wielokrotnie wysyłać to samo zapytanie do serwera 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 wykorzystanie wyników 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ż buforowanie po stronie klienta ostatnich odpowiedzi serwera
Aplikacje klienckie mogą wysyłać zapytania do interfejsu API szybciej, niż serwer interfejsu API może generować znaczące nowe odpowiedzi. W takim przypadku skutecznym rozwiązaniem jest zapamiętanie ostatniej odpowiedzi serwera po stronie klienta wraz z sygnaturą czasową i zwrócenie zapamiętanego wyniku bez wysyłania zapytania do serwera, jeśli zapamiętany wynik jest wystarczająco aktualny. Autor klienta interfejsu API może określić czas trwania zapamiętywania.
Na przykład aplikacja może wyświetlać użytkownikowi statystyki ruchu w sieci, wysyłając zapytania 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ć zapytania do serwera o statystyki co najwyżej raz na sekundę, a jeśli zapytanie zostanie wysłane w ciągu sekundy od poprzedniego zapytania, zwrócić ostatnią widzianą wartość.
Jest to dozwolone, ponieważ dokumentacja interfejsu API nie zawiera żadnej umowy dotyczącej 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
Zamiast zapytań do serwera rozważ generowanie kodu po stronie klienta
Jeśli wyniki zapytania są znane serwerowi w momencie kompilacji, zastanów się, czy są one również znane klientowi w momencie kompilacji, i rozważ, czy interfejs API można wdrożyć w całości po stronie klienta.
Rozważmy ten kod aplikacji, który sprawdza, czy urządzenie jest zegarkiem (czyli czy działa na nim 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 momencie kompilacji, a dokładniej 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 w kliencie
Na koniec klient interfejsu API może zarejestrować w serwerze interfejsu API wywołania zwrotne, aby otrzymywać powiadomienia o zdarzeniach.
Aplikacje zwykle rejestrują wiele wywołań zwrotnych dla tych samych informacji podstawowych. Zamiast powiadamiać 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"];
  }
}