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

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

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

ניהול גרסאות

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

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

הנחיות לעיצוב API

כללי

1. תיעוד של הכול

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

2. חיפוי

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

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

ממשקים

1. מתן שמות

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

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

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

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

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

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

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

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

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

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

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

חבילות נתונים מובנות

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

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

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

parcelable User {
    String username;
}

כדי שבעתיד תוכלו להאריך את תקופת הניסיון באופן הבא:

parcelable User {
    String username;
    int id;
}

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

‫[-Wexplicit-default, -Wenum-explicit-default] מספקים ברירות מחדל מפורשות לשדות.

חבילות נתונים לא מובנות

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

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

קבועים וסוגי ספירה

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

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

2. סוגי enum צריכים להיות קבוצות סגורות.

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

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

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

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;