Gérer les applications mises en cache et figées

Lorsque vous utilisez Binder pour communiquer entre les processus, soyez particulièrement prudent lorsque le processus distant est dans un état mis en cache ou figé. Les appels vers des applications mises en cache ou figées peuvent entraîner leur plantage ou une consommation inutile de ressources.

États des applications mises en cache et figées

Android maintient les applications dans différents états pour gérer les ressources système telles que la mémoire et le processeur.

État mis en cache

Lorsqu'une application ne comporte aucun composant visible par l'utilisateur, comme des activités ou des services, elle peut être déplacée vers l'état mise en cache. Pour en savoir plus, consultez Processus et cycle de vie d'une application. Les applications mises en cache sont conservées en mémoire au cas où l'utilisateur y reviendrait, mais elles ne sont pas censées fonctionner activement.

Lors de la liaison d'un processus d'application à un autre, par exemple à l'aide de bindService, l'état du processus du serveur est élevé pour être au moins aussi important que le processus client (sauf lorsque Context#BIND_WAIVE_PRIORITY est spécifié). Par exemple, si le client n'est pas dans l'état mis en cache, le serveur ne l'est pas non plus.

Inversement, l'état d'un processus serveur ne détermine pas celui de ses clients. Un serveur peut donc avoir des connexions binder à des clients, le plus souvent sous la forme de rappels, et alors que le processus distant est dans l'état mis en cache, le serveur ne l'est pas.

Lorsque vous concevez des API où les rappels proviennent d'un processus élevé et sont transmis aux applications, envisagez de suspendre l'envoi des rappels lorsqu'une application entre dans l'état mis en cache et de le reprendre lorsqu'elle quitte cet état. Cela évite un travail inutile dans les processus d'application mis en cache.

Pour savoir quand les applications entrent ou sortent de l'état mis en cache, utilisez ActivityManager.addOnUidImportanceListener :

// in ActivityManager or Context
activityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

État figé

Le système peut geler une application mise en cache pour économiser des ressources. Lorsqu'une application est figée, elle ne reçoit aucun temps de processeur et ne peut effectuer aucune tâche. Pour en savoir plus, consultez Mise en veille des applications mises en cache.

Lorsqu'un processus envoie une transaction de liaison synchrone (non oneway) à un autre processus distant figé, le système arrête le processus distant. Cela empêche le thread d'appel du processus appelant de se bloquer indéfiniment en attendant que le processus distant soit dégelé, ce qui pourrait entraîner une famine de thread ou des blocages dans l'application appelante.

Lorsqu'un processus envoie une transaction Binder asynchrone (oneway) à une application figée (généralement en notifiant un rappel, qui est généralement une méthode oneway), la transaction est mise en mémoire tampon jusqu'à ce que le processus distant soit dégelé. Si le tampon déborde, le processus de l'application destinataire peut planter. De plus, les transactions mises en mémoire tampon peuvent devenir obsolètes au moment où le processus de l'application est dégelé et les traite.

Pour éviter de submerger les applications avec des événements obsolètes ou de saturer leurs tampons, vous devez suspendre l'envoi des rappels lorsque le processus de l'application destinataire est figé.

Pour savoir quand les applications sont figées ou défigées, utilisez IBinder.addFrozenStateChangeCallback :

// The binder token of the remote process
IBinder binder = service.getBinder();

// Keep track of frozen state
AtomicBoolean remoteFrozen = new AtomicBoolean(false);

// Update remoteFrozen when the remote process freezes or unfreezes
binder.addFrozenStateChangeCallback(
    myExecutor,
    new IBinder.FrozenStateChangeCallback() {
        @Override
        public void onFrozenStateChanged(boolean isFrozen) {
            remoteFrozen.set(isFrozen);
        }
    });

// When dispatching callbacks to the remote process, pause dispatch if frozen:
if (!remoteFrozen.get()) {
    // dispatch callback to remote process
}

Utiliser RemoteCallbackList

La classe RemoteCallbackList est une classe d'assistance permettant de gérer les listes de rappels IInterface que les processus à distance enregistrent. Cette classe gère automatiquement les notifications de fin de liaison et fournit des options pour gérer les rappels vers les applications figées.

Lorsque vous créez RemoteCallbackList, vous pouvez spécifier une règle d'appelant figée :

  • FROZEN_CALLEE_POLICY_DROP : les rappels aux applications figées sont supprimés silencieusement. Utilisez cette stratégie lorsque les événements survenus pendant la mise en cache de l'application ne sont pas importants pour l'application (par exemple, les événements de capteur en temps réel).
  • FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT : si plusieurs rappels sont diffusés lorsqu'une application est figée, seul le plus récent est mis en file d'attente et distribué lorsque l'application est dégelée. Cela est utile pour les rappels basés sur l'état, où seule la mise à jour d'état la plus récente est importante (par exemple, un rappel informant l'application du volume multimédia actuel).
  • FROZEN_CALLEE_POLICY_ENQUEUE_ALL : tous les rappels diffusés lorsqu'une application est figée sont mis en file d'attente et distribués lorsque l'application est dégelée. Soyez prudent avec cette stratégie, car elle peut entraîner des dépassements de tampon si trop de rappels sont mis en file d'attente ou une accumulation d'événements obsolètes.

L'exemple suivant montre comment créer et utiliser une instance RemoteCallbackList qui supprime les rappels vers les applications figées :

RemoteCallbackList<IMyCallbackInterface> callbacks =
        new RemoteCallbackList.Builder<IMyCallbackInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(myExecutor)
                .build();

// Registering a callback:
callbacks.register(callback);

// Broadcasting to all registered callbacks:
callbacks.broadcast((callback) -> callback.onSomeEvent(eventData));

Si vous utilisez FROZEN_CALLEE_POLICY_DROP, le système n'appelle callback.onSomeEvent() que si le processus hébergeant le rappel n'est pas figé.

Services système et interactions avec les applications

Les services système interagissent souvent avec de nombreuses applications différentes à l'aide du binder. Étant donné que les applications peuvent passer à l'état mis en cache et à l'état figé, les services système doivent faire particulièrement attention à gérer ces interactions de manière fluide pour contribuer à maintenir la stabilité et les performances du système.

Les services système doivent déjà gérer les situations dans lesquelles les processus d'application sont arrêtés pour diverses raisons. Cela implique d'arrêter de travailler en leur nom et de ne pas essayer de continuer à envoyer des rappels à des processus morts. La prise en compte du gel des applications est une extension de cette responsabilité de surveillance existante.

Suivre les états des applications à partir des services système

Les services système, qui s'exécutent dans system_server ou en tant que daemons natifs, peuvent également utiliser les API décrites précédemment pour suivre l'importance et l'état figé des processus d'application :

  • ActivityManager.addOnUidImportanceListener : les services système peuvent enregistrer un écouteur pour suivre les modifications de l'importance de l'UID. Lorsqu'il reçoit un appel ou un rappel de binder d'une application, le service peut utiliser Binder.getCallingUid() pour obtenir l'UID et le corréler à l'état d'importance suivi par l'écouteur. Cela permet aux services système de savoir si l'application appelante est dans un état mis en cache.

  • IBinder.addFrozenStateChangeCallback : lorsqu'un service système reçoit un objet Binder d'une application (par exemple, dans le cadre d'un enregistrement pour les rappels), il doit enregistrer un FrozenStateChangeCallback sur cette instance IBinder spécifique. Cela permet d'informer directement le service système lorsque le processus d'application hébergeant ce binder est figé ou défigé.

Recommandations pour les services système

Nous recommandons que tous les services système susceptibles d'interagir avec les applications suivent l'état mis en cache et figé des processus d'application avec lesquels ils communiquent. Si vous ne le faites pas, vous risquez :

  • Consommation de ressources : effectuer des tâches pour des applications mises en cache et non visibles par l'utilisateur peut gaspiller des ressources système.
  • Plantages d'applications : les appels Binder synchrones aux applications figées les font planter. Les appels de binder asynchrones vers des applications figées entraînent un plantage si leur mémoire tampon de transaction asynchrone est saturée.
  • Comportement inattendu des applications : les applications dégivrées recevront immédiatement toutes les transactions de binder asynchrones mises en mémoire tampon qui leur ont été envoyées lorsqu'elles étaient gelées. Les applications peuvent passer une période indéfinie dans le congélateur. Les transactions mises en mémoire tampon peuvent donc être très anciennes.

Les services système utilisent souvent RemoteCallbackList pour gérer les rappels à distance et gérer automatiquement les processus arrêtés. Pour gérer les applications figées, étendez l'utilisation existante de RemoteCallbackList en appliquant une règle d'appelant figé, comme décrit dans Utiliser RemoteCallbackList.