מדריך סגנון AIDL

השיטות המומלצות שמפורטות כאן משמשות כמדריך לפיתוח ממשקי AIDL בצורה יעילה תוך התמקדות בגמישות של הממשק, במיוחד כשמשתמשים ב-AIDL כדי להגדיר API או לבצע אינטראקציה עם ממשקי API.

אפשר להשתמש ב-AIDL כדי להגדיר API כשאפליקציות צריכות להתממשק אחת עם השנייה בתהליך ברקע או צריכות להתחבר למערכת. למידע נוסף על פיתוח ממשקי תכנות באפליקציות באמצעות AIDL, ראו שפת הגדרה לבניית ממשק Android‏ (AIDL). לדוגמאות של AIDL, ראו AIDL ל-HALs ו-Stable AIDL.

ניהול גרסאות

כל קובץ snapshot תואם לאחור של ממשק AIDL API תואם לגרסה. כדי לצלם תמונת מצב, מריצים את m <module-name>-freeze-api. בכל פעם שמשתחרר לקוח או שרת של ה-API (לדוגמה, ב-Mainline), צריך ליצור קובץ snapshot וליצור גרסה חדשה. כשמדובר בממשקי API מסוג מערכת לספק, זה אמור להתרחש בגרסה השנתית של הפלטפורמה.

פרטים נוספים ומידע על סוגי השינויים שמותר לבצע מפורטים במאמר ממשקי גרסאות.

הנחיות לעיצוב ממשקי API

כללי

1. מתעדים הכול

  • תיעוד כל שיטה לסמנטיקה, לארגומנטים, לשימוש בהחרגות מובנות, לחריגים ספציפיים לשירותים ולערך המוחזר.
  • מתעדים את הסמנטיקה של כל ממשק.
  • מתעדים את המשמעות הסמנטית של מערכות ערכים קבועים וקבועים.
  • מתעדים כל פרט שעלול להיות לא ברור למטמיע.
  • יש לציין דוגמאות במקרים הרלוונטיים.

2. חיפוי

יש להשתמש באותיות רישיות עם ניקוד גמל (camel case) באותיות רישיות לסוגים, ובאותיות רישיות עם ניקוד גמל באותיות רגילות לשיטות, לשדות ולארגומנטים. לדוגמה, MyParcelable לסוג parcelable ו-anArgument לארגומנט. בקיצורים, צריך להתייחס לקיצור כמילה (NFC -> Nfc).

[-Wconst-name] ערכי Enum וקבועים צריכים להיות ENUM_VALUE ו-CONSTANT_NAME

ממשקים

1. מתן שמות

[-Winterface-name] שם הממשק צריך להתחיל ב-I, כמו IFoo.

2. הימנעות מממשק גדול עם 'אובייקטים' שמבוססים על מזהה

מומלץ להשתמש בממשקי משנה כשיש הרבה קריאות שקשורות ל-API ספציפי. היתרונות של השיטה הזו:

  • התכונה הזו עוזרת להבין את הקוד של הלקוח או השרת
  • פשוט יותר לנהל את מחזור החיים של אובייקטים
  • היתרון של הקבצים האלה הוא שאי אפשר לזייף אותם.

לא מומלץ: ממשק גדול אחד עם אובייקטים שמבוססים על מזהה

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
}

מומלץ: ממשקים נפרדים

interface IManager {
    IFoo getFoo();
}

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

3. אין לשלב שיטות חד-כיווניות עם שיטות דו-כיווניות

[-Wmixed-oneway] אין לשלב בין שיטות חד-כיווניות לשיטות לא חד-כיווניות, כי זה מקשה על הלקוחות והשרתים להבין את מודל השרשור. באופן ספציפי, כשקוראים קוד לקוח של ממשק מסוים, צריך לחפש כל method אם ה-method הזה תיחסם או לא.

4. הימנעות מהחזרת קודי סטטוס

ה-methods צריכות להימנע מקודי סטטוס כערכי החזרה, כי לכל methods של AIDL יש קוד החזרת סטטוס מרומז. אפשר לעיין במאמרים ServiceSpecificException או EX_SERVICE_SPECIFIC. לפי הסכמה, הערכים האלה מוגדרים כקבועים בממשק AIDL. מידע מפורט יותר זמין בקטע טיפול בשגיאות בקצוות העורפי של AIDL.

5. מערכי נתונים כפרמטרים של פלט שנחשבים מזיקים

[-Wout-array] שיטות עם פרמטרים של מערך פלט, כמו void foo(out String[] ret), בדרך כלל לא מומלצות כי הלקוח צריך להצהיר על גודל מערך הפלט ולהקצות אותו ב-Java, ולכן השרת לא יכול לבחור את גודל מערך הפלט. זו התנהגות לא רצויה בגלל האופן שבו מערכים פועלים ב-Java (לא ניתן להעביר אותם מחדש). במקום זאת, עדיף להשתמש בממשקי API כמו String[] foo().

6. הימנעות מפרמטרים של Inout

[-Winout-parameter] הדבר עלול לבלבל לקוחות כי גם פרמטרים מסוג in נראים כמו פרמטרים מסוג out.

7. הימנעות משימוש בפרמטרים מסוג out ו-inout עם הערך nullable שאינם מערך

[-Wout-nullable] מאחר שקצוות העורפי של Java לא מטפלים בהערה @nullable, בעוד שקצוות עורפי אחרים מטפלים בה, השימוש ב-out/inout @nullable T עלול להוביל להתנהגות לא עקבית בין קצוות עורפי שונים. לדוגמה, קצוות עורפיים שאינם של Java יכולים להגדיר את הפרמטר @nullable כ-null (ב-C++ כשמגדירים אותו כ-std::nullopt), אבל לקוח Java לא יכול לקרוא אותו כ-null.

נכסי מידע מובנה שניתן לחלק

1. מתי להשתמש?

כדאי להשתמש ב-parcelables מובְנים כשצריך לשלוח כמה סוגי נתונים.

לחלופין, כשיש לכם סוג נתונים יחיד אבל אתם מצפים להרחיב אותו בעתיד. לדוגמה, אין להשתמש ב-String username. משתמשים ב-Parcelable שניתן להרחבה, כמו:

parcelable User {
    String username;
}

כך תוכלו להרחיב אותו בעתיד, באופן הבא:

parcelable User {
    String username;
    int id;
}

2. מתן ברירות מחדל באופן מפורש

[-Wexplicit-default, -Wenum-explicit-default] הצגת ברירות מחדל מפורשות לשדות.

נכסים לא מובְנים שניתן לחלק לחלקים

1. מתי להשתמש?

רכיבי Parcelable לא מובְנים זמינים ב-Java באמצעות @JavaOnlyStableParcelable ובקצה העורפי של NDK באמצעות @NdkOnlyStableParcelable. בדרך כלל מדובר בפריטים ישנים וקיימים של parcelable שלא ניתן לבצע להם סיווג.

קבועים וממשקי enum

1. בשדות ביטים צריך להשתמש בשדות קבועים

בשדות ביטים צריך להשתמש בשדות קבועים (לדוגמה, const int FOO = 3; בממשק).

2. יש להגדיר את המאפיינים של enum כקבוצות סגורות.

ערכים צריכים להיות קבוצות סגורות. הערה: רק הבעלים של הממשק יכול להוסיף רכיבי enum. אם ספקים או יצרני ציוד מקור צריכים להרחיב את השדות האלה, הם צריכים להשתמש במנגנון חלופי. כשהדבר אפשרי, עדיף להעביר את הפונקציונליות של הספק ל-upstream. עם זאת, במקרים מסוימים יכול להיות שיורשו ערכים מותאמים אישית של ספקים (אבל לספקים צריך להיות מנגנון ליצירת גרסאות לכך, אולי AIDL עצמו, והם לא יכולים להיות בעלי התנגשויות ביניהם, והערכים האלה לא יכולים להיות חשופים לאפליקציות של צד שלישי).

3. הימנעו משימוש בערכים כמו 'NUM_ELEMENTS'

מאחר שערכים של enums מוגדרים לפי גרסאות, צריך להימנע משימוש בערכים שמציינים את מספר הערכים הקיימים. ב-C++‎ אפשר לעקוף את הבעיה באמצעות enum_range<>. ב-Rust, משתמשים ב-enum_values(). עדיין אין פתרון ל-Java.

לא מומלץ: שימוש בערכי מספרים

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. הימנעות מתחיליות וסיומת מיותרות

[-Wredundant-name] הימנעו משימוש בתחיליות ובסיומת מיותרות או חוזרות על עצמן בקבועים ובמספרים ממוספרים.

לא מומלץ: שימוש בקידומת יתירה

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

מומלץ: מתן שם ישירות למערך הערכים

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] לא מומלץ להשתמש ב-FileDescriptor כארגומנטים או כערך המוחזר של שיטת ממשק AIDL. במיוחד כש-AIDL מיושם ב-Java, הדבר עלול לגרום לדליפת מתאר קובץ, אלא אם מטפלים בזה בזהירות. בעיקרון, אם מאשרים FileDescriptor, צריך לסגור אותו באופן ידני כשלא משתמשים בו יותר.

בקצוות עורפיים מקומיים שומרים על הבטיחות כי FileDescriptor ממופה אל unique_fd, שאפשר לסגור אותו באופן אוטומטי. עם זאת, לא משנה באיזו שפה תשתמשו לקצה העורפי, מומלץ לא להשתמש ב-FileDescriptor בכלל, כי זה יגביל את היכולת שלכם לשנות את שפת הקצה העורפי בעתיד.

במקום זאת, אפשר להשתמש בכלי ParcelFileDescriptor, שניתן לסגירה אוטומטית.

יחידות משתנים

חשוב לוודא שהיחידות של המשתנים כלולות בשם, כדי שהיחידות יהיו מוגדרות ומובנות היטב בלי צורך להפנות למסמכים

דוגמאות

long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good

double energy; // Bad
double energyMilliJoules; // Good

int frequency; // Bad
int frequencyHz; // Good

חותמות זמן צריכות לציין את הייחוס שלהן

חותמות הזמן (למעשה, כל היחידות!) צריכות לציין בבירור את היחידות ואת נקודות העזר שלהן.

דוגמאות

/**
 * 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;