Android-API-Aufrufe sind in der Regel mit erheblicher Latenz und Rechenleistung pro Aufruf verbunden. Clientseitiges Caching ist daher ein wichtiger Aspekt bei der Entwicklung von APIs, die hilfreich, korrekt und leistungsstark sind.
Ziel
APIs, die App-Entwicklern im Android SDK zur Verfügung gestellt werden, werden häufig als Clientcode im Android-Framework implementiert. Dieser Code führt einen Binder-IPC-Aufruf an einen Systemdienst in einem Plattformprozess aus, dessen Aufgabe es ist, eine Berechnung durchzuführen und ein Ergebnis an den Client zurückzugeben. Die Latenz dieses Vorgangs wird in der Regel von drei Faktoren bestimmt:
- IPC-Overhead: Ein einfacher IPC-Aufruf hat in der Regel eine 10.000-mal höhere Latenz als ein einfacher In-Process-Methodenaufruf.
 - Konflikte auf Serverseite: Die Arbeit, die im Systemdienst als Reaktion auf die Anfrage des Clients ausgeführt wird, kann nicht sofort beginnen, z. B. wenn ein Serverthread mit der Verarbeitung anderer Anfragen beschäftigt ist, die früher eingegangen sind.
 - Serverseitige Verarbeitung: Die Verarbeitung der Anfrage auf dem Server kann einen erheblichen Aufwand erfordern.
 
Sie können alle drei Latenzfaktoren eliminieren, indem Sie einen Cache auf der Clientseite implementieren, sofern der Cache folgende Anforderungen erfüllt:
- Richtig: Der clientseitige Cache gibt niemals Ergebnisse zurück, die sich von den Ergebnissen des Servers unterscheiden.
 - Effektiv: Clientanfragen werden häufig aus dem Cache bedient, z. B. hat der Cache eine hohe Trefferquote.
 - Effizient: Der clientseitige Cache nutzt clientseitige Ressourcen effizient, z. B. durch die kompakte Darstellung von Cache-Daten und dadurch, dass nicht zu viele Cache-Ergebnisse oder veraltete Daten im Arbeitsspeicher des Clients gespeichert werden.
 
Serverergebnisse im Client zwischenspeichern
Wenn Clients häufig genau dieselbe Anfrage mehrmals stellen und sich der zurückgegebene Wert im Laufe der Zeit nicht ändert, sollten Sie einen Cache in der Clientbibliothek implementieren, der anhand der Anfrageparameter indexiert wird.
Erwägen Sie, IpcDataCache in Ihrer Implementierung zu verwenden:
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);
    }
}
Ein vollständiges Beispiel finden Sie unter android.app.admin.DevicePolicyManager.
IpcDataCache ist für den gesamten Systemcode verfügbar, einschließlich Mainline-Module.
Es gibt auch PropertyInvalidatedCache, das nahezu identisch ist, aber nur für das Framework sichtbar ist. Verwenden Sie nach Möglichkeit IpcDataCache.
Caches bei serverseitigen Änderungen ungültig machen
Wenn sich der vom Server zurückgegebene Wert im Laufe der Zeit ändern kann, implementieren Sie einen Callback zum Beobachten von Änderungen und registrieren Sie einen Callback, damit Sie den clientseitigen Cache entsprechend ungültig machen können.
Caches zwischen Unittestfällen entwerten
In einer Einheitentestsuite testen Sie den Clientcode möglicherweise mit einem Test-Double anstelle des echten Servers. Wenn das der Fall ist, sollten Sie zwischen den Testläufen alle clientseitigen Caches leeren. So wird sichergestellt, dass die Testläufe hermetisch voneinander getrennt sind und sich nicht gegenseitig beeinträchtigen.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
Beim Schreiben von CTS-Tests für einen API-Client, der intern Caching verwendet, ist der Cache ein Implementierungsdetail, das dem API-Autor nicht zur Verfügung steht. Daher sollten CTS-Tests kein besonderes Wissen über das im Clientcode verwendete Caching erfordern.
Cache-Treffer und ‑Fehler analysieren
IpcDataCache und PropertyInvalidatedCache können Live-Statistiken ausgeben:
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
  ...
Felder
Treffer:
- Definition: Die Anzahl der Male, die ein angefordertes Datenelement erfolgreich im Cache gefunden wurde.
 - Bedeutung: Weist auf einen effizienten und schnellen Datenabruf hin, wodurch unnötiger Datenabruf reduziert wird.
 - Höhere Werte sind in der Regel besser.
 
Löschen:
- Definition: Die Anzahl der Cache-Löschungen aufgrund einer Invalidierung.
 - Gründe für die Freigabe:
- Ungültigkeit: Veraltete Daten vom Server.
 - Speicherplatzverwaltung: Platz für neue Daten schaffen, wenn der Cache voll ist.
 
 - Hohe Werte können auf häufig wechselnde Daten und potenzielle Ineffizienz hinweisen.
 
Fehler:
- Definition: Die Anzahl der Fälle, in denen der Cache die angeforderten Daten nicht bereitstellen konnte.
 - Ursachen:
- Ineffizientes Caching: Der Cache ist zu klein oder es werden nicht die richtigen Daten gespeichert.
 - Die Daten ändern sich häufig.
 - Erstanfragen
 
 - Hohe Werte deuten auf potenzielle Caching-Probleme hin.
 
Überspringen:
- Definition: Fälle, in denen der Cache überhaupt nicht verwendet wurde, obwohl dies möglich gewesen wäre.
 - Gründe für das Überspringen:
- Corking: Speziell für Android Package Manager-Updates wird das Caching aufgrund einer hohen Anzahl von Aufrufen während des Bootvorgangs absichtlich deaktiviert.
 - Nicht festgelegt: Der Cache ist vorhanden, wurde aber nicht initialisiert. Die Nonce wurde nicht festgelegt, was bedeutet, dass der Cache noch nie entwertet wurde.
 - Umgehen: Eine bewusste Entscheidung, den Cache zu überspringen.
 
 - Hohe Werte deuten auf potenzielle Ineffizienzen bei der Cache-Nutzung hin.
 
Ungültig:
- Definition: Der Prozess, bei dem Daten im Cache als veraltet gekennzeichnet werden.
 - Bedeutung: Das System arbeitet mit den aktuellsten Daten, wodurch Fehler und Inkonsistenzen vermieden werden.
 - Wird in der Regel vom Server ausgelöst, der die Daten enthält.
 
Aktuelle Größe:
- Definition: Die aktuelle Anzahl der Elemente im Cache.
 - Bedeutung: Gibt die Ressourcennutzung des Cache und die potenziellen Auswirkungen auf die Systemleistung an.
 - Höhere Werte bedeuten in der Regel, dass mehr Arbeitsspeicher vom Cache verwendet wird.
 
Maximale Größe:
- Definition: Die maximale Größe des für den Cache zugewiesenen Speicherplatzes.
 - Bedeutung: Bestimmt die Kapazität des Cache und seine Fähigkeit, Daten zu speichern.
 - Wenn Sie eine angemessene maximale Größe festlegen, können Sie die Cache-Effektivität und die Speichernutzung in Einklang bringen. Wenn die maximale Größe erreicht ist, wird ein neues Element hinzugefügt, indem das am wenigsten zuletzt verwendete Element entfernt wird. Dies kann auf Ineffizienz hindeuten.
 
Höchststand:
- Definition: Die maximale Größe, die der Cache seit seiner Erstellung erreicht hat.
 - Bedeutung: Bietet Einblicke in die maximale Cache-Nutzung und den potenziellen Speicherdruck.
 - Wenn Sie den Höchststand im Blick behalten, können Sie potenzielle Engpässe oder Bereiche für die Optimierung ermitteln.
 
Überläufe:
- Definition: Die Anzahl der Male, die der Cache seine maximale Größe überschritten hat und Daten entfernt werden mussten, um Platz für neue Einträge zu schaffen.
 - Bedeutung: Gibt den Cache-Druck und potenzielle Leistungseinbußen aufgrund des Entfernens von Daten an.
 - Hohe Überlaufzahlen deuten darauf hin, dass die Cachegröße angepasst oder die Caching-Strategie neu bewertet werden muss.
 
Dieselben Statistiken finden Sie auch in einem Fehlerbericht.
Cache-Größe anpassen
Caches haben eine maximale Größe. Wenn die maximale Cachegröße überschritten wird, werden Einträge in LRU-Reihenfolge entfernt.
- Wenn zu wenige Einträge im Cache gespeichert werden, kann sich das negativ auf die Cache-Trefferquote auswirken.
 - Wenn zu viele Einträge im Cache gespeichert werden, steigt die Arbeitsspeichernutzung des Cache.
 
Finden Sie die richtige Balance für Ihren Anwendungsfall.
Redundante Client-Aufrufe vermeiden
Clients können dieselbe Anfrage innerhalb kurzer Zeit mehrmals an den Server senden:
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();
  }
}
Erwägen Sie, die Ergebnisse aus vorherigen Aufrufen wiederzuverwenden:
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();
  }
}
Clientseitige Memoization der letzten Serverantworten in Betracht ziehen
Client-Apps fragen die API möglicherweise schneller ab, als der Server der API sinnvolle neue Antworten generieren kann. In diesem Fall ist es sinnvoll, die letzte Serverantwort auf Clientseite zusammen mit einem Zeitstempel zu speichern und das gespeicherte Ergebnis zurückzugeben, ohne den Server abzufragen, wenn das gespeicherte Ergebnis aktuell genug ist. Der Autor des API-Clients kann die Dauer der Memoization festlegen.
Eine App kann dem Nutzer beispielsweise Statistiken zum Netzwerkverkehr anzeigen, indem sie in jedem gerenderten Frame die Statistiken abfragt:
@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()));
}
Die App kann Frames mit 60 Hz rendern. Der Clientcode in TrafficStats kann den Server jedoch hypothetisch höchstens einmal pro Sekunde nach Statistiken fragen und bei einer Anfrage innerhalb einer Sekunde nach einer vorherigen Anfrage den zuletzt gesehenen Wert zurückgeben.
Das ist zulässig, da die API-Dokumentation keine Vereinbarung bezüglich der Aktualität der zurückgegebenen Ergebnisse enthält.
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
Clientseitige Codegenerierung anstelle von Serveranfragen verwenden
Wenn die Abfrageergebnisse dem Server zur Build-Zeit bekannt sind, sollten Sie prüfen, ob sie auch dem Client zur Build-Zeit bekannt sind, und ob die API vollständig auf der Clientseite implementiert werden könnte.
Sehen Sie sich den folgenden App-Code an, mit dem geprüft wird, ob es sich bei dem Gerät um eine Smartwatch handelt (d. h. ob auf dem Gerät Wear OS ausgeführt wird):
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Diese Eigenschaft des Geräts ist zur Build-Zeit bekannt, insbesondere zu dem Zeitpunkt, zu dem das Framework für das Boot-Image dieses Geräts erstellt wurde. Der clientseitige Code für hasSystemFeature könnte sofort ein bekanntes Ergebnis zurückgeben, anstatt den Remote-Systemdienst PackageManager abzufragen.
Serverseitige Callbacks im Client deduplizieren
Schließlich kann der API-Client Callbacks beim API-Server registrieren, um über Ereignisse benachrichtigt zu werden.
Es ist normal, dass Apps mehrere Rückrufe für dieselben zugrunde liegenden Informationen registrieren. Anstatt den Client über IPC einmal pro registriertem Callback vom Server benachrichtigen zu lassen, sollte die Clientbibliothek einen registrierten Callback über IPC mit dem Server haben und dann jeden registrierten Callback in der App benachrichtigen.
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"];
  }
}