Guide de style AIDL

Les bonnes pratiques décrites ici servent de guide pour développer des interfaces AIDL efficacement et en tenant compte de la flexibilité de l'interface, en particulier lorsqu'AIDL est utilisé pour définir une API ou interagir avec des surfaces d'API.

AIDL peut être utilisé pour définir une API lorsque les applications doivent s'interfacer entre elles dans un processus en arrière-plan ou avec le système. Pour en savoir plus sur le développement d'interfaces de programmation dans des applications avec AIDL, consultez la page Langage de définition d'interface Android (AIDL). Pour obtenir des exemples d'utilisation d'AIDL en pratique, consultez les pages 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 m <module-name>-freeze-api. Chaque fois qu'un client ou un serveur de l'API est publié (par exemple, dans un train principal), vous devez prendre un instantané et créer une nouvelle version. Pour les API de système à fournisseur, cela devrait se produire lors de la révision annuelle de la plate-forme.

Pour en savoir plus sur les types de modifications autorisées, consultez la section Interfaces de gestion des versions.

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 renvoyée.
  • Documentez chaque interface pour sa sémantique.
  • Documentez la signification sémantique des énumérations et des constantes.
  • Documentez tout ce qui pourrait ne pas être clair pour un responsable de la mise en œuvre.
  • Donnez des exemples, le cas échéant.

2. Boîtier

Utilisez la casse supérieure camel case pour les types et la minuscule camel case 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 qu'il s'agit d'un mot (NFC -> Nfc).

[-Wconst-name] Les valeurs et les constantes des énumérations doivent être ENUM_VALUE et CONSTANT_NAME

Interfaces

1. Dénominations

[-Winterface-name] Un nom d'interface doit commencer par I, par exemple IFoo.

2. Évitez les interfaces volumineuses avec des "objets" basés sur les ID.

Préférez les sous-interfaces lorsqu'il existe de nombreux appels liés à une API spécifique. Cela offre les avantages suivants:

  • Facilite la compréhension du code client ou du serveur
  • Simplifie le cycle de vie des objets
  • Utilise le fait que les classeurs ne sont pas falsifiés.

Déconseillé: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
}

Recommandé:Interfaces individuelles

interface IManager {
    IFoo getFoo();
}

interface IFoo {
    void begin(); // clients in other processes can't guess a binder
    void op();
}

3. Ne pas mélanger les méthodes à sens unique et à double sens

[-Wmixed-oneway] Ne mélangez pas les méthodes unidirectionnelles avec des méthodes non à sens unique, car cela complique la compréhension du modèle de thread pour les clients et les serveurs. Plus précisément, lors de la lecture du code client d'une interface particulière, vous devez rechercher chaque méthode, qu'elle soit bloquée ou non.

4. Éviter de renvoyer des codes d'état

Les méthodes doivent éviter d'utiliser des codes d'état comme valeurs renvoyées, car toutes les méthodes AIDL ont un code de retour d'état implicite. Consultez ServiceSpecificException ou EX_SERVICE_SPECIFIC. Par convention, ces valeurs sont définies comme des constantes dans une interface AIDL. Pour en savoir plus, consultez la section Gestion des erreurs des backends AIDL.

5. Tableaux en tant que paramètres de sortie considérés comme dangereux

[-Wout-array] Les méthodes comportant des paramètres de sortie de tableau, tels que void foo(out String[] ret), sont généralement mauvaises, 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éaffectés). Préférez plutôt les API telles que String[] foo().

6. Éviter les paramètres inout

[-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 "out" et "inout" @nullable qui ne sont pas des tableaux

[-Wout-nullable] Étant donné que le backend Java ne gère pas l'annotation @nullable contrairement aux 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 de sortie @nullable sur null (en C++, en le définissant sur std::nullopt), mais le client Java ne peut pas le lire comme étant nul.

Parcelables structurés

1. Quand l'utiliser

Utilisez des parcelles structurées lorsque vous avez plusieurs types de données à envoyer.

ou lorsque vous disposez d'un seul type de données, mais que vous pensez être amené à l'étendre à l'avenir. Par exemple, n'utilisez pas String username. Utilisez un parcelable extensible comme celui-ci:

parcelable User {
    String username;
}

Pour pouvoir l'étendre à l'avenir, procédez comme suit:

parcelable User {
    String username;
    int id;
}

2. Fournir explicitement les valeurs par défaut

[-Wexplicit-default, -Wenum-explicit-default] Fournit des valeurs par défaut explicites pour les champs.

Parcelles non structurées

1. Quand l'utiliser

Les parcelables non structurés sont disponibles en Java avec @JavaOnlyStableParcelable et dans le backend du NDK avec @NdkOnlyStableParcelable. Généralement, il s'agit d'éléments parcelables anciens et 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 d'énumération. 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'utiliser la fonctionnalité d'amont du fournisseur. Toutefois, dans certains cas, les valeurs de fournisseurs personnalisées peuvent être autorisées (bien que les fournisseurs devraient mettre en place un mécanisme pour gérer cette version, peut-être AIDL lui-même, qu'elles ne doivent pas entrer en conflit entre elles et que ces valeurs ne doivent pas être exposées à des applications tierces).

3. Éviter les valeurs telles que "NUM_ELEMENTS"

Étant donné que les énumérations disposent de versions gérées, les valeurs indiquant le nombre de valeurs présentes doivent être évitées. En C++, il est possible de contourner ce problème avec enum_range<>. Pour Rust, utilisez enum_values(). Il n'existe pas encore de solution en Java.

Approche 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 les suffixes redondants

[-Wredundant-name] Évitez les préfixes et les suffixes redondants ou répétitifs dans les constantes et les énumérateurs.

Approche déconseillée:utilisation d'un préfixe redondant

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

Recommandé:Nommer directement l'énumération

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-Descriptor] L'utilisation de FileDescriptor comme argument ou comme 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 du descripteur de fichier, à moins d'être soigneusement traité. En gros, si vous acceptez une FileDescriptor, vous devez la fermer manuellement lorsqu'elle n'est plus utilisée.

Pour les backends natifs, vous êtes sûr, car FileDescriptor est mappé sur unique_fd, qui peut être fermé automatiquement. Cependant, quel que soit le langage du backend, il est préférable de ne PAS utiliser FileDescriptor du tout, car cela limitera votre liberté de modifier la langue du backend à l'avenir.

Utilisez plutôt ParcelFileDescriptor, qui peut être fermé automatiquement.

Unités variables

Assurez-vous que les unités variables sont incluses dans le nom afin que leurs unités soient bien définies et comprises sans qu'il soit nécessaire 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;