הנחיות לשימוש ב-AIDL API

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

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

‫AIDL יציב עם @VintfStability משמש לממשקי HAL ומאפשר לעדכן לקוחות ושרתים באופן עצמאי. לשם כך נדרשת תאימות לאחור ונתונים מוּבְנִים.

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

ניהול גרסאות

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

כשממשק מוקפא (נשמר בספרייה aidl_api עם ניהול גרסאות), אסור לשנות אותו. אפשר לערוך רק את הספרייה current. אפשר להוסיף בבטחה שיטות לסוף של ממשק, שדות לסוף של parcelable, ערכי enum ל-enum וחברים לאיחוד.

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

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

יחסי תלות ב-Build

מודולים של Android לא יכולים להיות תלויים בכמה גרסאות שונות של הספריות שנוצרו מ-aidl_interface. הגרסאות השונות של הספריות מגדירות את אותם סוגים באותם מרחבי שמות. aidlמערכת ה-build של Android מזהה את הבעיה הזו ומציגה שגיאה בכל אחד מגרפי התלות שמסתיים בגרסאות לא תואמות של הספריות.

לכן, אם מודול מכיל הרבה תלויות עם תלויות משלהן, יכול להיות שיהיה קשה לעדכן גרסה אחת של ממשק משותף.

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

מומלץ להשתמש במודולים של *_defaults (כמו rust_defaults,‏ cc_defaults, ‏ java_defaults) כדי לארגן את התלויות בספריות שנוצרו. בדרך כלל יש ברירת מחדל לגרסה latest של הממשקים, וגם ברירות מחדל לגרסאות קודמות אם עדיין נעשה בהן שימוש.

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

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

כללי

1. תיעוד של כל דבר

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

2. חיפוי

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

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

3. לא נדרש ידע גלובלי

ממשקי API לא צריכים להניח שלמפתחים יש ידע כללי בכל בסיס הקוד או מומחיות ספציפית בתחום. כשמדובר במזהים ספציפיים לדומיין (כמו שמות מכשירים, מזהים או כינויים):

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

4. כל הנתונים מובנים ותואמים לגרסאות קודמות

נתונים לא מובְנים כמו string,‏ byte[] וזיכרון משותף צריכים להיות בפורמט יציב של התוכן שלהם, או להיות אטומים לצד אחד של הממשק.

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

באופן דומה, לא מבצעים סריאליזציה של אובייקטים ל-byte[] או לזיכרון משותף, אלא אם הם יציבים ותואמים לאחור. במקרים מסוימים, אפשר להשתמש בהערה @FixedSize כדי לשתף אובייקטים מסוג Parcelable ואיגודים בזיכרון משותף וב-Fast Message Queues.

ממשקים

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

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

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

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.

8. שימוש בבקשות ובתשובות ייחודיות

מקבצים את כל הפרמטרים הנדרשים בקלט אחד parcelable. צריך ליצור אובייקטים ייעודיים מסוג Parcelable לבקשה ולתגובה לכל שיטה בממשק, במקום להעביר פרימיטיבים (לדוגמה, צריך להשתמש ב-ComputeResponse compute(in ComputeRequest request) במקום להעביר משתנים נפרדים). כך אפשר להוסיף ארגומנטים חדשים בהמשך בלי לשנות את חתימת הפונקציה. מומלץ מאוד להשתמש בדפוס הזה אם צפוי שבעתיד יתווספו עוד פרמטרים, או אם לשיטה כבר יש יותר מארבעה פרמטרים.

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

אם לא נוצר method באמצעות התבנית הזו, אפשר לעבור לתבנית הזו על ידי יצירת method חדש עם בקשה ו-parcelable של תגובה, והוצאה משימוש של ה-method הישן. לדוגמה:

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.

אובייקטים מסוג Parcelable מובנה

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

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

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

parcelable User {
    String username;
}

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

parcelable User {
    String username;
    int id;
}

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

‫[-Wexplicit-default, -Wenum-explicit-default] מספקים ברירות מחדל מפורשות לשדות. כשמוסיפים שדות חדשים לאובייקט Parcelable, לקוחות ושרתים ישנים משמיטים אותם, אבל לקוחות ושרתים חדשים ממלאים את ערכי ברירת המחדל באופן אוטומטי.

3. שימוש ב-ParcelableHolder לתוספים של ספקים

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

4. מבני נתונים

  • כדי לייצג מפות, צריך להשתמש במערכים או ב-List של parcelables, כי AIDL לא תומך באופן מובנה בסוגי Map שניתנים לתרגום בטוח בכל הבק-אנדים המקוריים (לדוגמה, FeatureToScoreEntry[]).
  • כדי למנוע את הצורך במערכים מקבילים בעתיד, השתמשו במערכים של אובייקטים מסוג parcelable בשדות חוזרים, ולא במערכים של פרימיטיבים.
  • צריך להשתמש באובייקטים מוקלדים חזק parcelable במקום במחרוזות או ב-JSON שעברו סריאליזציה ב-IPC.
  • כדי לאפשר הרחבה בעתיד, כדאי להשתמש ב-enums במקום בערכים בוליאניים למצבים. במקרה של bitmask, כדאי להשתמש בסוג const int ולא בסוג enum כדי למנוע המרה מסורבלת בחלק מהקצה האחורי.

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

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

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

קבועים וספירות

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

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

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

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

בו-זמניות (concurrency) ופעולות אסינכרוניות

כדי למנוע חסימה, כדאי לטפל בפעולות ארוכות טווח באמצעות ממשק אסינכרוני (oneway).

אם שירות לא בוטח בלקוחות שלו, כל קריאה חוזרת שהוא מקבל מהלקוחות צריכה להיות ממשקי oneway. כך הלקוחות לא יוכלו לחסום את השירות ללא הגבלת זמן.

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