Android API-Aufrufe erfordern in der Regel eine erhebliche Latenz und Verarbeitung pro Aufruf. Clientseitiges Caching ist daher ein wichtiger Aspekt beim Entwerfen 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, der einen Binder-IPC-Aufruf an einen Systemdienst in einem Plattformprozess ausführt, der eine Berechnung ausführen und ein Ergebnis an den Client zurückgeben soll. 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-fache Latenz wie ein einfacher In-Process-Methodenaufruf.
- Serverseitige Zugriffskonflikte: Die im Systemdienst als Reaktion auf die Anfrage des Clients ausgeführte Arbeit beginnt möglicherweise nicht sofort, z. B. wenn ein Server-Thread damit beschäftigt ist, andere Anfragen zu bearbeiten, die früher eingegangen sind.
- Serverseitige Berechnung: Die Verarbeitung der Anfrage auf dem Server kann viel Arbeit erfordern.
Sie können alle drei dieser Latenzfaktoren beseitigen, indem Sie einen Cache auf der Clientseite implementieren, vorausgesetzt, der Cache ist:
- Richtig: Der clientseitige Cache gibt nie Ergebnisse zurück, die von den Ergebnissen des Servers abweichen.
- Effektiv: Clientanfragen werden häufig aus dem Cache bereitgestellt, z. B. wenn der Cache eine hohe Trefferquote hat.
- Effizient: Der clientseitige Cache nutzt clientseitige Ressourcen effizient, z. B. indem zwischengespeicherte Daten kompakt dargestellt und nicht zu viele zwischengespeicherte Ergebnisse oder veraltete Daten im Arbeitsspeicher des Clients gespeichert werden.
Serverergebnisse im Client zwischenspeichern
Wenn Clients dieselbe Anfrage häufig mehrmals stellen und sich der zurückgegebene Wert im Laufe der Zeit nicht ändert, sollten Sie in der Clientbibliothek einen Cache implementieren, der anhand der Anfrageparameter schlüsselt.
Sie können IpcDataCache
in Ihrer Implementierung 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-Modulen.
Es gibt auch PropertyInvalidatedCache
, das fast identisch ist, aber nur für das Framework sichtbar ist. Verwenden Sie nach Möglichkeit IpcDataCache
.
Caches bei serverseitigen Änderungen entwerten
Wenn sich der vom Server zurückgegebene Wert im Laufe der Zeit ändern kann, implementieren Sie einen Callback, um Änderungen zu beobachten, und registrieren Sie einen Callback, damit Sie den clientseitigen Cache entsprechend ungültig machen können.
Caches zwischen Unit-Testfällen entwerten
In einer Unit-Test-Suite können Sie den Clientcode anhand eines Test-Doubles statt des echten Servers testen. Leeren Sie in diesem Fall alle clientseitigen Caches zwischen den Testfällen. So bleiben die Testfälle hermetisch und es wird verhindert, dass sich ein Testfall auf einen anderen auswirkt.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
Beim Schreiben von CTS-Tests, die einen API-Client testen, der intern Caching verwendet, ist der Cache ein Implementierungsdetail, das dem API-Entwickler nicht bekannt ist. Daher sollten CTS-Tests kein spezielles Wissen über das Caching im Clientcode erfordern.
Cache-Treffer und Cache-Fehler analysieren
Mit IpcDataCache
und PropertyInvalidatedCache
können Sie Live-Statistiken ausdrucken:
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: Gibt an, wie oft ein angefordertes Datenelement im Cache gefunden wurde.
- Bedeutung: Gibt an, dass Daten effizient und schnell abgerufen werden, wodurch unnötige Datenabrufe reduziert werden.
- Je höher die Anzahl, desto besser.
Löst Folgendes:
- Definition: Gibt an, wie oft der Cache aufgrund von Ungültigkeit geleert wurde.
- Gründe für die Klärung:
- Ungültig: Veraltete Daten vom Server.
- Speicherplatzverwaltung: Wenn der Cache voll ist, wird Platz für neue Daten freigegeben.
- Hohe Werte können auf häufig wechselnde Daten und potenzielle Ineffizienz hinweisen.
Fehlschläge:
- Definition: Die Anzahl der Male, dass der Cache die angeforderten Daten nicht bereitstellen konnte.
- Ursachen:
- Ineffizientes Caching: Der Cache ist zu klein oder die richtigen Daten werden nicht gespeichert.
- Die Daten ändern sich häufig.
- Erstanfragen
- Hohe Werte deuten auf potenzielle Caching-Probleme hin.
Überspringungen:
- Definition: Fälle, in denen der Cache nicht verwendet wurde, obwohl dies möglich gewesen wäre.
- Gründe für das Überspringen:
- Corking: Spezifisch für Updates des Android Package Managers, bei denen das Caching aufgrund einer hohen Anzahl von Aufrufen während des Starts absichtlich deaktiviert wird.
- Nicht festgelegt: Der Cache existiert, wurde aber nicht initialisiert. Der Nonce wurde nicht festgelegt, was bedeutet, dass der Cache nie entwertet wurde.
- Umgehen: Eine bewusste Entscheidung, den Cache zu umgehen.
- Hohe Werte deuten auf potenzielle Ineffizienzen bei der Cachenutzung hin.
Ungültig macht:
- Definition: Das Markieren von Daten im Cache als veraltet oder ungültig.
- Bedeutung: Gibt an, dass das System mit den aktuellsten Daten arbeitet, wodurch Fehler und Inkonsistenzen vermieden werden.
- Wird in der Regel vom Server ausgelöst, zu dem die Daten gehören.
Aktuelle Größe:
- Definition: Die aktuelle Anzahl der Elemente im Cache.
- Bedeutung: Gibt die Ressourcennutzung des Caches und die potenziellen Auswirkungen auf die Systemleistung an.
- Höhere Werte bedeuten in der Regel, dass der Cache mehr Arbeitsspeicher belegt.
Maximale Größe:
- Definition: Die maximale Menge an Speicherplatz, die dem Cache zugewiesen wird.
- Bedeutung: Bestimmt die Kapazität des Caches und seine Fähigkeit, Daten zu speichern.
- Wenn Sie eine geeignete maximale Größe festlegen, können Sie die Cache-Effizienz mit der Speichernutzung in Einklang bringen. Sobald die maximale Größe erreicht ist, wird ein neues Element hinzugefügt, indem das am längsten nicht verwendete Element entfernt wird. Dies kann auf Ineffizienz hinweisen.
Hochwassermarke:
- Definition: Die maximale Größe, die der Cache seit seiner Erstellung erreicht hat.
- Bedeutung: Bietet Einblicke in die maximale Cachenutzung und den potenziellen Speicherdruck.
- Wenn Sie den Höchststand beobachten, können Sie potenzielle Engpässe oder Bereiche mit Optimierungspotenzial erkennen.
Überlauf:
- Definition: Gibt an, wie oft die maximale Größe des Caches überschritten wurde und Daten entfernt werden mussten, um Platz für neue Einträge zu schaffen.
- Bedeutung: Gibt den Cache-Druck und die potenzielle Leistungseinbuße aufgrund der Datenentfernung an.
- Hohe Überlaufzahlen deuten darauf hin, dass die Cache-Größe möglicherweise angepasst oder die Caching-Strategie neu bewertet werden muss.
Dieselben Statistiken finden Sie auch in einem Fehlerbericht.
Größe des Caches optimieren
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 Caches.
Finden Sie das richtige Gleichgewicht für Ihren Anwendungsfall.
Redundante Clientaufrufe vermeiden
Clients können innerhalb kurzer Zeit dieselbe Abfrage 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();
}
}
Sie können die Ergebnisse früherer Aufrufe wiederverwenden:
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 Memorisierung der letzten Serverantworten in Betracht ziehen
Clientanwendungen können die API möglicherweise schneller abfragen, als der API-Server sinnvoll neue Antworten liefern kann. In diesem Fall ist es effektiv, die zuletzt gesehene Serverantwort zusammen mit einem Zeitstempel auf der Clientseite 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 Memorisierung festlegen.
Eine App kann beispielsweise Statistiken zum Netzwerkverkehr für den Nutzer anzeigen, indem sie in jedem gerenderten Frame nach den Statistiken fragt:
@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 zeichnen. Hypothetisch kann der Clientcode in TrafficStats
den Server jedoch höchstens einmal pro Sekunde um Statistiken bitten und bei einer Abfrage innerhalb einer Sekunde nach einer vorherigen Abfrage den zuletzt gesehenen Wert zurückgeben.
Das ist zulässig, da die API-Dokumentation keine Vereinbarung zur 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 Serverabfragen verwenden
Wenn die Abfrageergebnisse dem Server bei der Buildzeit bekannt sind, überlegen Sie, ob sie auch dem Client bei der Buildzeit bekannt sind, und ob die API vollständig clientseitig implementiert werden könnte.
Im folgenden App-Code wird geprüft, 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 zum Zeitpunkt der Erstellung bekannt, insbesondere zum 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-PackageManager
-Systemdienst abzufragen.
Server-Callbacks im Client deduplizieren
Schließlich kann der API-Client Callbacks beim API-Server registrieren, um über Ereignisse informiert zu werden.
Es ist üblich, dass Apps mehrere Rückrufe für dieselben zugrunde liegenden Informationen registrieren. Anstatt dass der Server den Client einmal pro registriertem Callback über IPC benachrichtigt, sollte die Clientbibliothek einen registrierten Callback mit 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"];
}
}