השיטות המומלצות שמפורטות כאן הן מדריך לפיתוח יעיל של ממשקי 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 ואיברים לאיחוד.
לקוחות שקוראים לשיטות חדשות בשרתים ישנים מקבלים את השגיאה UNKNOWN_TRANSACTION, והלקוח צריך לטפל בה בצורה חלקה.
למידע נוסף על סוגי השינויים שמותר לבצע, קראו את המאמר ממשקי ניהול גרסאות.
יחסי תלות של גרסאות build
מודולים של Android לא יכולים להיות תלויים בכמה גרסאות שונות של הספריות שנוצרו מ-aidl_interface. הגרסאות השונות של הספריות מגדירות את אותם סוגים באותם מרחבי שמות. מערכת ה-build של aidl ב-Android מזהה את הבעיה הזו ומציגה שגיאה בכל אחד מתרשימי התלות שמסתיימים בגרסאות הלא תואמות של הספריות.
בגלל זה, יכול להיות קשה לעדכן גרסה אחת של ממשק נפוץ כשמודול מכיל הרבה יחסי תלות עם יחסי תלות משלהם.
מפתחים יכולים להשתמש ב-aidl_interface_defaults כדי להצהיר על יחסי התלות של ממשק משותף בממשקים אחרים, כך שלא צריך לעדכן את כולם בנפרד.
אנחנו ממליצים להשתמש במודולים של *_defaults (כמו rust_defaults, cc_defaults, java_defaults) כדי לארגן את יחסי התלות בספריות שנוצרו. בדרך כלל יש ברירת מחדל לגרסה latest של הממשקים, וגם ברירות מחדל לגרסאות קודמות אם עדיין משתמשים בהן.
מפתחים יכולים להשתמש ב-aidl_interface_defaults כדי להצהיר על יחסי התלות של ממשק משותף בממשקים אחרים, כך שלא צריך לעדכן את כולם בנפרד.
הנחיות לעיצוב API
כללי
1. תעדו הכול
- תעדו כל שיטה מבחינת הסמנטיקה, הארגומנטים, השימוש בחריגים מובנים, חריגים ספציפיים לשירות וערך ההחזרה.
- תעדו כל ממשק לפי הסמנטיקה שלו.
- תעדו את המשמעות הסמנטית של ערכי enum וקבועים.
- תעדו כל דבר שעשוי להיות לא ברור למטמיע.
- ספקו דוגמאות במקומות הרלוונטיים.
2. חיפוי
השתמשו באותיות רישיות בתחילת כל מילה (Upper Camel Case) לסוגים, ובאותיות רישיות בתחילת כל מילה חוץ מהראשונה (Lower Camel Case) לשיטות, לשדות ולארגומנטים. לדוגמה, MyParcelable לסוג שניתן להעביר ו-anArgument לארגומנט. ראשי תיבות נחשבים למילה (NFC -> Nfc).
[-Wconst-name] ערכי טיפוסים בני מנייה (enum) וקבועים צריכים להיות ENUM_VALUE ו-CONSTANT_NAME
3. הימנעו מדרישה לידע גלובלי
ממשקי API לא צריכים להניח שלמפתחים יש ידע גלובלי על כל בסיס הקוד או מומחיות בתחום ספציפי. כשמדובר במזהים ספציפיים לדומיין (כמו שמות מכשירים, מזהים או כינויים):
- חשוב לציין במפורש ולתעד את המקור של המזהים האלה ואת הפורמט שלהם, אם חשוב ששני הצדדים של הממשק יכירו אותם.
- לחלופין, אפשר להשתמש במזהים ספציפיים לממשק (כמו אובייקטים של Binder או אסימונים בהתאמה אישית) ולגרום לצד אחד לנהל את המיפוי לערכים הבסיסיים. כך אפשר לצמצם התנגשויות ולמנוע מצב שבו המשתמשים צריכים להבין את פרטי ההטמעה מחוץ לאזור שלהם.
4. כל הנתונים מובְנים ותואמים לדורות קודמים
נתונים לא מובְנים כמו string, byte[] וזיכרון משותף חייבים להיות בפורמט יציב, או להיות אטומים לצד אחד של הממשק.
לדוגמה, אפשר לקבל ולתעד ארגומנט של מחרוזת שמשמש כהודעת שגיאה לתוצאה לצורך ניפוי באגים, אבל אסור לנתח ולפרש אותו כי יכול להיות שהפורמט והתוכן לא תואמים לדורות קודמים. אם הצד השני של הממשק צריך לדעת מהי השגיאה בזמן הריצה, צריך להשתמש ב-enum, בקבוע או ב-ServiceSpecificException.
באופן דומה, אל תבצעו סריאליזציה של אובייקטים ל-byte[] או לזיכרון משותף, אלא אם הם יציבים ותואמים לדורות קודמים. במקרים מסוימים, אפשר להשתמש בהערה @FixedSize כדי לשתף parcelables ואיחודים בזיכרון משותף ובתורי הודעות מהירים.
ממשקים
1. מתן שמות
[-Winterface-name] שם של ממשק צריך להתחיל ב-I, כמו IFoo.
2. הימנעו מממשק גדול עם "אובייקטים" שמבוססים על מזהים
עדיף להשתמש בממשקי משנה כשיש הרבה קריאות שקשורות ל-API ספציפי. היתרונות של השימוש במקור:
- הופך את הקוד של הלקוח או השרת לקל יותר להבנה
- מחזור חיים פשוט יותר של אובייקטים
- מנצל את העובדה שאי אפשר לזייף את ה-binder.
לא מומלץ: ממשק אחד גדול עם אובייקטים מבוססי-מזהה
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] Don't mix one-way with non-oneway methods, because it makes understanding the threading model complicated for clients and servers. באופן ספציפי, כשקוראים קוד לקוח של ממשק מסוים, צריך לבדוק אם כל אחת מהשיטות תחסום או לא.
4. הימנעו מהחזרת קודי סטטוס
בשיטות לא צריך להשתמש בקודי סטטוס כערכים מוחזרים, כי לכל שיטות AIDL יש קוד סטטוס מוחזר משתמע. אפשר לקרוא מידע נוסף במאמרים ServiceSpecificException או EX_SERVICE_SPECIFIC. לפי המוסכמה, הערכים האלה מוגדרים כקבועים בממשק AIDL. אם נדרש עיכוב מותאם אישית או נתוני שגיאה ייחודיים לצד שגיאה, זהו המקרה היחיד שבו אובייקט תגובה מותאם אישית צריך לייצג שגיאה. למידע נוסף, קראו את המאמר טיפול בשגיאות.
5. מערכים כפרמטרים של פלט נחשבים כמזיקים
[-Wout-array] שיטות עם פרמטרים של פלט מערך, כמו void foo(out String[] ret), הן בדרך כלל לא טובות כי הלקוח צריך להצהיר על גודל מערך הפלט ולהקצות אותו ב-Java, ולכן השרת לא יכול לבחור את גודל פלט המערך. ההתנהגות הלא רצויה הזו מתרחשת בגלל האופן שבו מערכים פועלים ב-Java (אי אפשר להקצות אותם מחדש). במקום זאת, עדיף להשתמש בממשקי API כמו String[] foo().
6. הימנעו מפרמטרים של קלט/פלט (inout)
[-Winout-parameter] This can confuse clients because even in parameters look
like out parameters.
7. הימנעו מפרמטרים מסוג out ו-inout שאינם מערכים וניתנים לערך null
[-Wout-nullable] מכיוון שה-Java backend לא מטפל בהערה @nullable, בעוד ש-backends אחרים כן, out/inout @nullable T עלול להוביל להתנהגות לא עקבית ב-backends. לדוגמה, בקצוות עורפיים שאינם Java אפשר להגדיר פרמטר out @nullable
כ-null (ב-C++, מגדירים אותו כ-std::nullopt) אבל לקוח Java
לא יכול לקרוא אותו כ-null.
8. שימוש בבקשות ובתשובות ייחודיות
מקבוצים את כל הפרמטרים הנחוצים לקלט אחד parcelable.
צרו אובייקטים ייעודיים מסוג Parcelable לבקשות ולתגובות לכל שיטת ממשק במקום להעביר פרימיטיבים (לדוגמה, השתמשו ב-ComputeResponse compute(in ComputeRequest request) במקום להעביר משתנים נפרדים). כך אפשר להוסיף ארגומנטים חדשים בהמשך בלי לשנות את החתימה של הפונקציה. מומלץ מאוד להשתמש בתבנית הזו אם צפוי שיתווספו עוד פרמטרים בעתיד, או אם לשיטה כבר יש יותר מארבעה פרמטרים.
ההצעה הזו לא רלוונטית לשיטות שלא דורשות ערכי קלט או פלט נוספים. אם תחשבו על כל מקרה בנפרד ותשמרו על גמישות לשינויים עתידיים, תוכלו להפחית את מספר השיטות שהוצאו משימוש ואת המורכבות של הקוד שתואם לדורות קודמים.
אם לא יצרתם את ה-method באמצעות התבנית הזו, אתם יכולים לעבור לתבנית הזו על ידי יצירת method חדש עם בקשה ותגובה שניתנות להעברה, ולהוציא משימוש את ה-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. - כדי לאפשר הרחבה בעתיד, השתמשו בערכי enum במקום בערכים בוליאניים למצבים. למסכות סיביות, השתמשו בסוגי
const intבמקוםenumכדי להימנע מהמרה מסורבלת בחלק מהקצוות העורפיים.
אובייקטים לא מובְנים שניתנים להעברה (Parcelable)
1. מתי להשתמש?
אפשר להשתמש ב-Parcelable לא מובנה ב-Java באמצעות @JavaOnlyStableParcelable וב-NDK backend באמצעות @NdkOnlyStableParcelable. בדרך כלל, מדובר ב-parcelables ישנים וקיימים שלא ניתן להגדיר את המבנה שלהם.
קבועים וספירות
1. שדות סיביות צריכים להשתמש בשדות קבועים
שדות סיביות צריכים להשתמש בשדות קבועים (לדוגמה, const int FOO = 3; בממשק).
2. הערכים של Enums צריכים להיות קבוצות סגורות.
הערכים של Enums צריכים להיות קבוצות סגורות. הערה: רק הבעלים של הממשק יכול להוסיף רכיבי enum. אם ספקים או יצרני ציוד מקורי צריכים להרחיב את השדות האלה, נדרש מנגנון חלופי. כשאפשר, עדיף להעביר את הפונקציונליות של הספק במעלה הזרם. עם זאת, במקרים מסוימים יכול להיות שיתאפשרו ערכים מותאמים אישית של ספקים (אבל לספקים צריך להיות מנגנון להגדרת גרסאות, אולי 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
enum MyStatus {
GOOD,
BAD
}
FileDescriptor
[-Wfile-descriptor] מומלץ מאוד לא להשתמש ב-FileDescriptor כארגומנט או כערך ההחזרה של שיטת ממשק AIDL. במיוחד כש-AIDL מוטמע ב-Java, זה עלול לגרום לדליפה של תיאור הקובץ, אלא אם מטפלים בזה בזהירות. בעיקרון, אם אתם מאשרים FileDescriptor, אתם צריכים לסגור אותו ידנית כשאתם לא משתמשים בו יותר.
ב-backends מקוריים, אתם בטוחים כי 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;
פעולות בו-זמניות ואסינכרוניות
כדי למנוע חסימה, צריך לטפל בפעולות שנמשכות זמן רב באמצעות ממשק אסינכרוני (oneway).
אם שירות לא נותן אמון בלקוחות שלו, כל הקריאות החוזרות (callbacks) שהוא מקבל מהלקוחות צריכות להיות ממשקי oneway. כך הלקוחות לא יוכלו לחסום את השירות ללא הגבלת זמן.
ממשקי API אסינכרוניים מורכבים מקריאה קדימה, ארגומנטים של קלט וממשק קריאה חוזרת לקבלת תוצאות. בקטע שימוש בבקשות ובתגובות ייחודיות מפורטות המלצות לארגומנטים.