מדריך הסגנון של AIDL

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

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

ניהול גרסאות

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

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

הנחיות לעיצוב ממשקי 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 אם הוא יגרום לחסימה או לא.

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

מומלץ להימנע משימוש בקודי סטטוס כערכים חזרה בשיטות, כי לכל השיטות ב-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] הצגת ברירות מחדל מפורשות לשדות.

נכסי parcelable לא מובנים

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

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

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

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

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

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

ערכים מוגדרים מראש צריכים להיות קבוצות סגורות. הערה: רק הבעלים של הממשק יכול להוסיף רכיבי 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;