Les bonnes pratiques décrites ici servent de guide pour développer des interfaces AIDL de manière efficace et en tenant compte de la flexibilité de l'interface, en particulier lorsque AIDL est utilisé pour définir une API stable et rétrocompatible.
AIDL peut être utilisé pour définir une API lorsque des applications doivent communiquer entre elles dans un processus en arrière-plan ou avec le système.
L'AIDL stable avec @VintfStability est utilisé pour les interfaces HAL et permet aux clients et aux serveurs d'être mis à jour indépendamment. Cela nécessite une rétrocompatibilité et des données structurées.
Pour en savoir plus sur le développement d'interfaces de programmation dans des applications avec AIDL, consultez Langage de définition d'interface Android (AIDL). Pour obtenir des exemples pratiques d'AIDL, consultez AIDL pour les HAL et AIDL stable.
Gestion des versions
Chaque instantané rétrocompatible d'une API AIDL correspond à une version.
Pour prendre un instantané, exécutez la commande m <module-name>-freeze-api. Chaque fois qu'un client ou un serveur de l'API est publié (par exemple, dans un train Mainline), vous devez prendre un instantané et créer une nouvelle version. Pour les API système-fournisseur, cela devrait se produire avec la révision annuelle de la plate-forme.
Lorsqu'une interface est figée (enregistrée dans le répertoire aidl_api versionné), elle ne doit jamais être modifiée. Vous ne pouvez modifier que le répertoire current. Vous pouvez ajouter des méthodes à la fin d'une interface, des champs à la fin d'un élément Parcelable, des énumérateurs à un énumérateur et des membres à une union sans risque.
Les clients qui appellent de nouvelles méthodes sur des serveurs plus anciens reçoivent une erreur UNKNOWN_TRANSACTION, qui doit être gérée correctement par le client.
Pour en savoir plus et obtenir des informations sur les types de modifications autorisés, consultez Interfaces de gestion des versions.
Créer des dépendances
Les modules Android ne peuvent pas dépendre de plusieurs versions différentes des bibliothèques générées à partir d'un aidl_interface. Les différentes versions des bibliothèques définissent les mêmes types dans les mêmes espaces de noms. Le système de compilation aidl d'Android identifie ce problème et génère une erreur pour chacun des graphiques de dépendances qui se terminent par des versions incompatibles des bibliothèques.
Il peut alors être difficile de mettre à jour une version d'une interface commune lorsqu'un module contient de nombreuses dépendances avec leurs propres dépendances.
Les développeurs peuvent utiliser aidl_interface_defaults pour déclarer les dépendances d'une interface partagée sur d'autres interfaces afin qu'elles n'aient pas toutes besoin d'être mises à jour indépendamment.
Nous vous recommandons d'utiliser des modules *_defaults (comme rust_defaults, cc_defaults, java_defaults) pour organiser les dépendances sur les bibliothèques générées. Il est courant d'avoir une valeur par défaut pour la version latest des interfaces, ainsi que pour les versions précédentes si elles sont encore utilisées.
Les développeurs peuvent utiliser aidl_interface_defaults pour déclarer les dépendances d'une interface partagée sur d'autres interfaces afin qu'elles n'aient pas toutes besoin d'être mises à jour indépendamment.
Consignes de conception d'API
Général
1. Tout documenter
- Documentez chaque méthode pour sa sémantique, ses arguments, son utilisation des exceptions intégrées, ses exceptions spécifiques au service et sa valeur de retour.
- Documentez la sémantique de chaque interface.
- Documentez la signification sémantique des énumérations et des constantes.
- Documentez tout ce qui pourrait ne pas être clair pour un développeur.
- Fournissez des exemples, si nécessaire.
2. Boîtier
Utilisez la casse camel supérieure pour les types et la casse camel inférieure pour les méthodes, les champs et les arguments. Par exemple, MyParcelable pour un type Parcelable et anArgument pour un argument. Pour les acronymes, considérez-les comme des mots (NFC -> Nfc).
[-Wconst-name] Les valeurs d'énumération et les constantes doivent être ENUM_VALUE et CONSTANT_NAME
3. Éviter d'exiger des connaissances globales
Les API ne doivent pas supposer que les développeurs ont une connaissance globale de l'ensemble du code ou une expertise spécifique dans un domaine. Lorsque vous traitez des identifiants spécifiques à un domaine (comme des noms, des ID ou des pseudos d'appareils) :
- Soyez explicite et documentez l'origine et le format de ces identifiants s'il est important que les deux côtés de l'interface les connaissent.
- Vous pouvez également utiliser des identifiants spécifiques à l'interface (tels que des objets Binder ou des jetons personnalisés) et faire en sorte qu'un côté gère le mappage vers les valeurs sous-jacentes. Cela réduit les collisions et évite aux utilisateurs d'avoir à comprendre les détails de l'implémentation en dehors de leur domaine.
4. Toutes les données sont structurées et rétrocompatibles.
Les données non structurées telles que string, byte[] et la mémoire partagée doivent avoir un format stable pour leur contenu ou être opaques à l'une des parties de l'interface.
Par exemple, un argument de chaîne utilisé comme message d'erreur pour un résultat peut être reçu et consigné pour le débogage, mais il ne doit pas être analysé ni interprété, car le format et le contenu peuvent ne pas être rétrocompatibles. Si l'autre côté de l'interface doit connaître l'erreur au moment de l'exécution, utilisez une énumération, une constante ou ServiceSpecificException.
De même, ne sérialisez pas d'objets dans byte[] ni dans la mémoire partagée, sauf s'ils sont stables et rétrocompatibles. Dans certains cas, vous pouvez utiliser l'annotation @FixedSize pour partager des éléments Parcelable et des unions dans la mémoire partagée et les files d'attente de messages rapides.
Interfaces
1. Dénomination
[-Winterface-name] Le nom d'une interface doit commencer par I, comme IFoo.
2. Éviter les grandes interfaces avec des "objets" basés sur des ID
Privilégiez les sous-interfaces lorsqu'il existe de nombreux appels liés à une API spécifique. Cela présente les avantages suivants :
- Facilite la compréhension du code client ou serveur
- Simplifie le cycle de vie des objets
- Tire parti de l'inviolabilité des liants.
Approche déconseillée : une interface unique et volumineuse avec des objets basés sur des ID
interface IManager {
int getFooId();
void beginFoo(int id); // clients in other processes can guess an ID
void opFoo(int id);
void recycleFoo(int id); // ownership not handled by type
}
Recommandation : interfaces individuelles
interface IManager {
IFoo getFoo();
}
interface IFoo {
void begin(); // clients in other processes can't guess a binder
void op();
}
3. Ne mélangez pas les méthodes unidirectionnelles et bidirectionnelles.
[-Wmixed-oneway] Ne mélangez pas les méthodes à sens unique avec les méthodes non à sens unique, car cela complique la compréhension du modèle de threading pour les clients et les serveurs. Plus précisément, lorsque vous lisez le code client d'une interface particulière, vous devez rechercher pour chaque méthode si elle bloquera ou non.
4. Éviter de renvoyer des codes d'état
Les méthodes doivent éviter les codes d'état comme valeurs de retour, car toutes les méthodes AIDL ont un code d'état de retour implicite. Consultez ServiceSpecificException ou EX_SERVICE_SPECIFIC. Par convention, ces valeurs sont définies comme des constantes dans une interface AIDL. Si un délai personnalisé ou des données d'erreur uniques sont nécessaires en plus d'une erreur, c'est la seule fois où un objet de réponse personnalisé doit représenter une erreur. Pour en savoir plus, consultez la section Gestion des erreurs.
5. Les tableaux en tant que paramètres de sortie sont considérés comme dangereux
[-Wout-array] Les méthodes comportant des paramètres de sortie de tableau, comme void foo(out String[] ret), sont généralement à éviter, car la taille du tableau de sortie doit être déclarée et allouée par le client en Java. Le serveur ne peut donc pas choisir la taille de la sortie du tableau. Ce comportement indésirable est dû au fonctionnement des tableaux en Java (ils ne peuvent pas être réalloués). Privilégiez plutôt les API telles que String[] foo().
6. Éviter les paramètres d'entrée/sortie
[-Winout-parameter] Cela peut dérouter les clients, car même les paramètres in ressemblent à des paramètres out.
7. Éviter les paramètres @nullable non matriciels out et inout
[-Wout-nullable] Étant donné que le backend Java ne gère pas l'annotation @nullable, contrairement à d'autres backends, out/inout @nullable T peut entraîner un comportement incohérent entre les backends. Par exemple, les backends non Java peuvent définir un paramètre @nullable de sortie sur "null" (en C++, en le définissant sur std::nullopt), mais le client Java ne peut pas le lire comme "null".
8. Utiliser des requêtes et des réponses uniques
Regroupez tous les paramètres nécessaires dans une seule entrée parcelable.
Créez des objets Parcelable de requête et de réponse dédiés pour chaque méthode d'interface au lieu de transmettre des primitives (par exemple, utilisez ComputeResponse compute(in ComputeRequest request) au lieu de transmettre des variables distinctes). Cela permet d'ajouter de nouveaux arguments ultérieurement sans modifier la signature de la fonction. Ce modèle est fortement suggéré lorsqu'il est prévu d'ajouter d'autres paramètres à l'avenir ou si une méthode comporte déjà plus de quatre paramètres.
Les méthodes qui ne nécessitent pas d'entrées ni de sorties supplémentaires ne bénéficieront pas de cette suggestion. En réfléchissant explicitement à chaque cas et en restant flexible pour les futurs changements, vous pouvez réduire le nombre de méthodes obsolètes et la complexité du code rétrocompatible.
Si une méthode n'a pas été créée à l'aide de ce modèle, vous pouvez passer à ce modèle en créant une méthode avec un objet Parcelable de requête et de réponse, et en déconseillant l'ancienne méthode. Exemple :
void foo(int a, int b, int c); // original version, but deprecated in favor of the next version
void fooV2(in MyArg arg); // new version having int a, b, c, and d.
Parcelables structurés
1. Quand l'utiliser
Utilisez des parcelables structurés lorsque vous avez plusieurs types de données à envoyer.
Ou encore, lorsque vous n'avez qu'un seul type de données, mais que vous prévoyez de l'étendre à l'avenir. Par exemple, n'utilisez pas String username. Utilisez un élément Parcelable extensible, comme suit :
parcelable User {
String username;
}
Vous pourrez ainsi l'étendre ultérieurement, comme suit :
parcelable User {
String username;
int id;
}
2. Fournir des valeurs par défaut de manière explicite
[-Wexplicit-default, -Wenum-explicit-default] Fournissez des valeurs par défaut explicites pour les champs. Lorsque de nouveaux champs sont ajoutés à un objet Parcelable, les anciens clients et serveurs les abandonnent, mais les valeurs par défaut sont automatiquement renseignées pour les nouveaux clients et serveurs.
3. Utiliser ParcelableHolder pour les extensions de fournisseur
Si vous définissez un parcelable AOSP que les implémenteurs d'appareils doivent étendre, intégrez une instance de ParcelableHolder dans votre objet. Cela sert de point d'extension sans créer de conflits de fusion. Cela ressemble aux extensions d'interface associées, mais permet aux implémenteurs d'inclure leur propre parcelable aux côtés du parcelable existant sans créer leur propre interface ni leurs propres types.
4. Structures de données
- Utilisez des tableaux ou
Listde Parcelables pour représenter les cartes, car AIDL n'est pas compatible en mode natif avec les typesMapqui se traduisent de manière sécurisée sur tous les backends natifs (par exemple,FeatureToScoreEntry[]). - Utilisez des tableaux d'objets
parcelablepour les champs répétés plutôt que des tableaux de primitives, afin d'éviter d'avoir besoin de tableaux parallèles à l'avenir. - Utilisez des objets
parcelableà typage fort au lieu de chaînes sérialisées ou de JSON sur IPC. - Utilisez des énumérations au lieu de valeurs booléennes pour les états afin de permettre une expansion future. Pour les masques de bits, utilisez les types
const intplutôt queenumpour éviter les conversions fastidieuses dans certains backends.
Parcelables non structurés
1. Quand l'utiliser
Les Parcelables non structurés sont disponibles en Java avec @JavaOnlyStableParcelable et dans le backend NDK avec @NdkOnlyStableParcelable. Il s'agit généralement d'anciens éléments Parcelable existants qui ne peuvent pas être structurés.
Constantes et énumérations
1. Les champs de bits doivent utiliser des champs constants
Les champs de bits doivent utiliser des champs constants (par exemple, const int FOO = 3; dans une interface).
2. Les énumérations doivent être des ensembles fermés.
Les énumérations doivent être des ensembles fermés. Remarque : Seul le propriétaire de l'interface peut ajouter des éléments enum. Si les fournisseurs ou les OEM doivent étendre ces champs, un autre mécanisme est nécessaire. Dans la mesure du possible, il est préférable d'intégrer les fonctionnalités du fournisseur en amont. Toutefois, dans certains cas, des valeurs de fournisseur personnalisées peuvent être autorisées (les fournisseurs doivent toutefois disposer d'un mécanisme pour les versionner, peut-être AIDL lui-même, elles ne doivent pas être en conflit les unes avec les autres et ces valeurs ne doivent pas être exposées aux applications tierces).
3. Évitez les valeurs telles que "NUM_ELEMENTS".
Étant donné que les énumérations sont versionnées, il convient d'éviter les valeurs qui indiquent le nombre de valeurs présentes. En C++, il est possible de contourner ce problème avec enum_range<>. Pour Rust, utilisez enum_values(). En Java, aucune solution n'est encore disponible.
Option déconseillée : utiliser des valeurs numérotées
@Backing(type="int")
enum FruitType {
APPLE = 0,
BANANA = 1,
MANGO = 2,
NUM_TYPES, // BAD
}
4. Éviter les préfixes et suffixes redondants
[-Wredundant-name] Évitez les préfixes et suffixes redondants ou répétitifs dans les constantes et les énumérateurs.
Option déconseillée : utiliser un préfixe redondant
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
Recommandation : Nommer directement l'énumération
enum MyStatus {
GOOD,
BAD
}
FileDescriptor
[-Wfile-descriptor] L'utilisation de FileDescriptor comme argument ou valeur de retour d'une méthode d'interface AIDL est fortement déconseillée. En particulier, lorsque l'AIDL est implémenté en Java, cela peut entraîner une fuite de descripteur de fichier, sauf si elle est gérée avec soin. En gros, si vous acceptez un FileDescriptor, vous devez le fermer manuellement lorsqu'il n'est plus utilisé.
Pour les backends natifs, vous êtes en sécurité, car FileDescriptor correspond à unique_fd, qui est auto-fermant. Toutefois, quel que soit le langage de backend que vous utiliserez, il est judicieux de ne PAS utiliser FileDescriptor du tout, car cela limitera votre liberté de changer de langage de backend à l'avenir.
Utilisez plutôt ParcelFileDescriptor, qui peut être fermé automatiquement.
Unités de variables
Assurez-vous que les unités de variables sont incluses dans le nom afin qu'elles soient bien définies et comprises sans avoir besoin de consulter la documentation.
Exemples
long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good
double energy; // Bad
double energyMilliJoules; // Good
int frequency; // Bad
int frequencyHz; // Good
Les codes temporels doivent indiquer leur référence
Les codes temporels (en fait, toutes les unités) doivent indiquer clairement leurs unités et leurs points de référence.
Exemples
/**
* Time since device boot in milliseconds
*/
long timestampMs;
/**
* UTC time received from the NTP server in units of milliseconds
* since January 1, 1970
*/
long utcTimeMs;
Concurrence et opérations asynchrones
Gérez les opérations de longue durée avec une interface asynchrone (oneway) pour éviter le blocage.
Si un service ne fait pas confiance à ses clients, tous les rappels qu'il reçoit des clients doivent être des interfaces oneway. Cela empêche les clients de bloquer le service indéfiniment.
Structurez les API asynchrones composées d'un appel de transfert, d'arguments d'entrée et d'une interface de rappel pour obtenir les résultats. Consultez Utiliser des requêtes et des réponses uniques pour obtenir des recommandations sur les arguments.