Les appels d'API Android impliquent généralement une latence et un calcul importants par invocation. La mise en cache côté client est donc un élément important à prendre en compte lors de la conception d'API utiles, correctes et performantes.
Motivation
Les API exposées aux développeurs d'applications dans le SDK Android sont souvent implémentées en tant que code client dans le framework Android, qui effectue un appel Binder IPC à un service système dans un processus de plate-forme, dont le rôle est d'effectuer un calcul et de renvoyer un résultat au client. La latence de cette opération est généralement dominée par trois facteurs :
- Latence de l'IPC : un appel IPC de base est généralement 10 000 fois plus lent qu'un appel de méthode de base dans le processus.
 - Contention côté serveur : le travail effectué dans le service système en réponse à la requête du client peut ne pas démarrer immédiatement, par exemple si un thread de serveur est occupé à traiter d'autres requêtes arrivées plus tôt.
 - Calcul côté serveur : le travail lui-même pour traiter la demande sur le serveur peut nécessiter un travail important.
 
Vous pouvez éliminer ces trois facteurs de latence en implémentant un cache côté client, à condition que le cache soit :
- Correct : le cache côté client ne renvoie jamais de résultats différents de ceux que le serveur aurait renvoyés.
 - Efficace : les requêtes client sont souvent traitées à partir du cache (par exemple, le cache présente un taux de réussite élevé).
 - Efficace : le cache côté client utilise efficacement les ressources côté client, par exemple en représentant les données mises en cache de manière compacte et en ne stockant pas trop de résultats mis en cache ni de données obsolètes dans la mémoire du client.
 
Envisagez de mettre en cache les résultats du serveur dans le client.
Si les clients envoient souvent exactement la même requête plusieurs fois et que la valeur renvoyée ne change pas au fil du temps, vous devez implémenter un cache dans la bibliothèque cliente, indexé par les paramètres de la requête.
Envisagez d'utiliser IpcDataCache dans votre implémentation :
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);
    }
}
Pour obtenir un exemple complet, consultez android.app.admin.DevicePolicyManager.
IpcDataCache est disponible pour tout le code système, y compris les modules principaux.
Il existe également PropertyInvalidatedCache, qui est presque identique, mais qui n'est visible que par le framework. Utilisez IpcDataCache autant que possible.
Invalider les caches lors de modifications côté serveur
Si la valeur renvoyée par le serveur peut changer au fil du temps, implémentez un rappel pour observer les changements et enregistrez un rappel afin de pouvoir invalider le cache côté client en conséquence.
Invalider les caches entre les scénarios de tests unitaires
Dans une suite de tests unitaires, vous pouvez tester le code client par rapport à un test double plutôt qu'au serveur réel. Si c'est le cas, assurez-vous de vider tous les caches côté client entre les scénarios de test. Cela permet de préserver l'herméticité mutuelle des scénarios de test et d'empêcher qu'un scénario de test n'interfère avec un autre.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }
    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }
    ...
}
Lorsque vous écrivez des tests CTS qui exercent un client d'API utilisant la mise en cache en interne, le cache est un détail d'implémentation qui n'est pas exposé à l'auteur de l'API. Par conséquent, les tests CTS ne doivent pas nécessiter de connaissances spécifiques sur la mise en cache utilisée dans le code client.
Étudier les succès et les défauts de cache
Les IpcDataCache et PropertyInvalidatedCache peuvent imprimer des statistiques en direct :
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
  ...
Champs
Hits :
- Définition : nombre de fois où une donnée demandée a été trouvée dans le cache.
 - Importance : indique une récupération efficace et rapide des données, ce qui réduit la récupération de données inutiles.
 - En général, plus le nombre est élevé, mieux c'est.
 
Autorisations :
- Définition : nombre de fois où le cache a été vidé en raison d'une invalidation.
 - Motifs de l'autorisation :
- Invalidation : données obsolètes provenant du serveur.
 - Gestion de l'espace : libérer de l'espace pour de nouvelles données lorsque le cache est plein.
 
 - Un nombre élevé peut indiquer que les données changent fréquemment et qu'il existe une inefficacité potentielle.
 
Manquements :
- Définition : nombre de fois où le cache n'a pas pu fournir les données demandées.
 - Causes :
- Mise en cache inefficace : le cache est trop petit ou ne stocke pas les bonnes données.
 - Données qui changent fréquemment.
 - Les demandes envoyées pour la première fois.
 
 - Un nombre élevé suggère des problèmes de mise en cache potentiels.
 
Passer :
- Définition : instances où le cache n'a pas été utilisé du tout, même s'il aurait pu l'être.
 - Motifs de l'ignorance :
- Bouchonnage : spécifique aux mises à jour d'Android Package Manager, désactivation délibérée de la mise en cache en raison d'un volume d'appels élevé lors du démarrage.
 - Non défini : le cache existe, mais n'est pas initialisé. Le nonce n'a pas été défini, ce qui signifie que le cache n'a jamais été invalidé.
 - Contournement : décision intentionnelle de contourner le cache.
 
 - Un nombre élevé indique des inefficacités potentielles dans l'utilisation du cache.
 
Invalide :
- Définition : processus consistant à marquer les données mises en cache comme obsolètes.
 - Importance : fournit un signal indiquant que le système fonctionne avec les données les plus récentes, ce qui évite les erreurs et les incohérences.
 - Généralement déclenchée par le serveur propriétaire des données.
 
Taille actuelle :
- Définition : nombre actuel d'éléments dans le cache.
 - Importance : indique l'utilisation des ressources du cache et l'impact potentiel sur les performances du système.
 - Des valeurs plus élevées signifient généralement que le cache utilise plus de mémoire.
 
Taille maximale :
- Définition : quantité maximale d'espace allouée au cache.
 - Importance : détermine la capacité du cache et sa capacité à stocker des données.
 - Définir une taille maximale appropriée permet d'équilibrer l'efficacité du cache et l'utilisation de la mémoire. Une fois la taille maximale atteinte, un nouvel élément est ajouté en supprimant l'élément le moins récemment utilisé, ce qui peut indiquer une inefficacité.
 
Seuil maximal :
- Définition : taille maximale atteinte par le cache depuis sa création.
 - Importance : fournit des informations sur l'utilisation maximale du cache et la pression potentielle sur la mémoire.
 - La surveillance du point haut peut aider à identifier les éventuels goulots d'étranglement ou les zones à optimiser.
 
Dépassements :
- Définition : nombre de fois où le cache a dépassé sa taille maximale et a dû supprimer des données pour faire de la place pour de nouvelles entrées.
 - Importance : indique la pression du cache et la dégradation potentielle des performances en raison de l'éviction des données.
 - Un nombre élevé de dépassements suggère que la taille du cache doit peut-être être ajustée ou que la stratégie de mise en cache doit être réévaluée.
 
Les mêmes statistiques sont également disponibles dans un rapport de bug.
Ajuster la taille du cache
La taille des caches est limitée. Lorsque la taille maximale du cache est dépassée, les entrées sont supprimées dans l'ordre LRU.
- La mise en cache d'un trop petit nombre d'entrées peut avoir un impact négatif sur le taux d'accès au cache.
 - La mise en cache d'un trop grand nombre d'entrées augmente l'utilisation de la mémoire du cache.
 
Trouvez le juste équilibre pour votre cas d'utilisation.
Éliminer les appels clients redondants
Les clients peuvent envoyer la même requête au serveur plusieurs fois sur une courte période :
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();
  }
}
Envisagez de réutiliser les résultats des appels précédents :
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();
  }
}
Envisagez la mémoïsation côté client des réponses récentes du serveur
Les applications clientes peuvent interroger l'API à un rythme plus rapide que celui auquel le serveur de l'API peut produire des réponses réellement nouvelles. Dans ce cas, une approche efficace consiste à mémoriser la dernière réponse du serveur côté client avec un code temporel, et à renvoyer le résultat mémorisé sans interroger le serveur si le résultat mémorisé est suffisamment récent. L'auteur du client API peut déterminer la durée de la mémoïsation.
Par exemple, une application peut afficher des statistiques sur le trafic réseau à l'utilisateur en interrogeant les statistiques à chaque frame dessiné :
@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()));
}
L'application peut dessiner des frames à 60 Hz. Mais, hypothétiquement, le code client dans TrafficStats peut choisir d'interroger le serveur pour obtenir des statistiques au maximum une fois par seconde et, s'il est interrogé dans la seconde suivant une requête précédente, renvoyer la dernière valeur observée.
Cela est autorisé, car la documentation de l'API ne fournit aucun contrat concernant la fraîcheur des résultats renvoyés.
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
Envisagez la génération de code côté client au lieu des requêtes serveur
Si le serveur peut connaître les résultats de la requête au moment de la compilation, déterminez si le client peut également les connaître au moment de la compilation et si l'API peut être implémentée entièrement côté client.
Prenons l'exemple du code d'application suivant qui vérifie si l'appareil est une montre (c'est-à-dire s'il exécute Wear OS) :
public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Cette propriété de l'appareil est connue au moment de la compilation, plus précisément au moment où le framework a été compilé pour l'image de démarrage de cet appareil. Le code côté client pour hasSystemFeature peut renvoyer immédiatement un résultat connu, au lieu d'interroger le service système PackageManager à distance.
Dédoublonner les rappels de serveur dans le client
Enfin, le client API peut enregistrer des rappels auprès du serveur API pour être informé des événements.
Il est normal que les applications enregistrent plusieurs rappels pour les mêmes informations sous-jacentes. Plutôt que de demander au serveur de notifier le client une fois par rappel enregistré à l'aide d'IPC, la bibliothèque cliente doit avoir un rappel enregistré à l'aide d'IPC avec le serveur, puis notifier chaque rappel enregistré dans l'application.
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"];
  }
}