Stable AIDL

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

  • ממשקי API מוגדרים במערכת ה-build באמצעות aidl_interfaces.
  • ממשקי API יכולים להכיל רק נתונים מובְנים. אובייקטים מסוג Parcelable שמייצגים את הסוגים המועדפים נוצרים באופן אוטומטי על סמך הגדרת ה-AIDL שלהם, והם עוברים באופן אוטומטי המרה לפורמט שניתן להעברה (marshalling) והמרה מפורמט שניתן להעברה (unmarshalling).
  • אפשר להצהיר על ממשקים כעל ממשקים יציבים (תואמים לדור קודם). במקרה כזה, ממשק ה-API שלהם מתועד ומנוהל בגרסאות בקובץ לצד ממשק ה-AIDL.

השוואה בין AIDL מובנה ל-AIDL יציב

ממשק AIDL מובנה מתייחס לסוגים שמוגדרים רק ב-AIDL. לדוגמה, הצהרה על parcelable (parcelable בהתאמה אישית) היא לא AIDL מובנה. חבילות Parcelable עם שדות שמוגדרים ב-AIDL נקראות חבילות Parcelable מובנות.

Stable AIDL דורש AIDL מובנה כדי שמערכת ה-build והקומפיילר יוכלו להבין אם השינויים שבוצעו ב-parcelables תואמים לאחור. עם זאת, לא כל הממשקים המובנים יציבים. כדי שממשק יהיה יציב, הוא צריך להשתמש רק בסוגים מובנים, וגם בתכונות הבאות של ניהול גרסאות. לעומת זאת, ממשק לא יציב אם משתמשים במערכת הליבה לבנייה כדי לבנות אותו או אם המשתנה unstable:true מוגדר.

הגדרה של ממשק AIDL

הגדרה של aidl_interface נראית כך:

aidl_interface {
    name: "my-aidl",
    srcs: ["srcs/aidl/**/*.aidl"],
    local_include_dir: "srcs/aidl",
    imports: ["other-aidl"],
    versions_with_info: [
        {
            version: "1",
            imports: ["other-aidl-V1"],
        },
        {
            version: "2",
            imports: ["other-aidl-V3"],
        }
    ],
    stability: "vintf",
    backend: {
        java: {
            enabled: true,
            platform_apis: true,
        },
        cpp: {
            enabled: true,
        },
        ndk: {
            enabled: true,
        },
        rust: {
            enabled: true,
        },
    },

}
  • name: השם של מודול ממשק AIDL שמזהה באופן ייחודי ממשק AIDL.
  • srcs: רשימת קובצי המקור של AIDL שמרכיבים את הממשק. הנתיב של סוג AIDL‏ Foo שמוגדר בחבילה com.acme צריך להיות <base_path>/com/acme/Foo.aidl, כאשר <base_path> יכול להיות כל ספרייה שקשורה לספרייה שבה נמצא Android.bp. בדוגמה הקודמת, <base_path> הוא srcs/aidl.
  • local_include_dir: הנתיב שממנו מתחיל שם החבילה. הוא תואם ל<base_path> שמוסבר למעלה.
  • imports: רשימה של מודולים של aidl_interface שהכלי הזה משתמש בהם. אם אחד מממשקי AIDL שלכם משתמש בממשק או ב-parcelable מ-aidl_interface אחר, צריך להזין כאן את השם שלו. אפשר להשתמש בשם לבד כדי להתייחס לגרסה האחרונה, או בשם עם סיומת הגרסה (למשל -V1) כדי להתייחס לגרסה ספציפית. האפשרות לציין גרסה נתמכת החל מ-Android 12
  • versions: הגרסאות הקודמות של הממשק שמוקפאות ב-api_dir. החל מ-Android 11, הגרסאות של versions מוקפאות ב-aidl_api/name. אם אין גרסאות קפואות של ממשק, אין צורך לציין את זה ולא יתבצעו בדיקות תאימות. השדה הזה הוחלף ב-versions_with_info ב-Android מגרסה 13 ואילך.
  • versions_with_info: רשימה של טאפלים, שכל אחד מהם מכיל את השם של גרסה קפואה ורשימה עם ייבוא גרסאות של מודולים אחרים של aidl_interface שהגרסה הזו של aidl_interface ייבאה. ההגדרה של גרסה V של ממשק AIDL‏ IFACE נמצאת בכתובת aidl_api/IFACE/V. השדה הזה נוסף ב-Android 13, ולא אמורים לשנות אותו ישירות ב-Android.bp. השדה נוסף או עודכן על ידי הפעלת *-update-api או *-freeze-api. בנוסף, השדה versions מועבר אוטומטית אל versions_with_info כשמשתמש מפעיל את *-update-api או את *-freeze-api.
  • stability: דגל אופציונלי להבטחת היציבות של הממשק הזה. האפשרות הזו תומכת רק ב-"vintf". אם המדיניות stability לא מוגדרת, מערכת הבנייה בודקת שהממשק תואם לאחור, אלא אם המדיניות unstable מוגדרת. הערך unset מתאים לממשק עם יציבות בהקשר של הקומפילציה הזו (כלומר, כל מה שקשור למערכת, למשל, מה שמופיע ב-system.img ובמחיצות קשורות, או כל מה שקשור לספק, למשל, מה שמופיע ב-vendor.img ובמחיצות קשורות). אם stability מוגדר כ-"vintf", זה מתאים להבטחת יציבות: הממשק חייב להישאר יציב כל עוד נעשה בו שימוש.
  • gen_trace: דגל אופציונלי להפעלה או להשבתה של המעקב. החל מ-Android 14, ערך ברירת המחדל הוא true עבור קצה העורפי (backend) של cpp ושל java.
  • host_supported: דגל אופציונלי שאם מגדירים אותו ל-true, הספריות שנוצרו זמינות לסביבת המארח.
  • unstable: דגל אופציונלי שמשמש לסימון הממשק הזה ככזה שלא צריך להיות יציב. כשההגדרה הזו היא true, מערכת ה-build לא יוצרת את קובץ ה-dump של ה-API עבור הממשק ולא דורשת לעדכן אותו.
  • frozen: דגל אופציונלי. אם הערך שלו הוא true, המשמעות היא שלא בוצעו שינויים בממשק מאז הגרסה הקודמת שלו. כך אפשר לבצע יותר בדיקות בזמן הבנייה. אם הערך הוא false, המשמעות היא שהממשק נמצא בפיתוח ושיש בו שינויים חדשים. לכן, הפעלת foo-freeze-api יוצרת גרסה חדשה ומשנה אוטומטית את הערך ל-true. הוצג ב-Android 14.
  • backend.<type>.enabled: הדגלים האלה משמשים להפעלה או להשבתה של כל אחד מהקצוות העורפיים שהקומפיילר של AIDL יוצר עבורם קוד. יש תמיכה בארבעה קצוות עורפיים: Java,‏ C++‎, ‏NDK ו-Rust. הקצה העורפי של Java,‏ C++‎ ו-NDK מופעל כברירת מחדל. אם לא צריך אף אחד משלושת ה-backends האלה, צריך להשבית אותו באופן מפורש. השפה Rust מושבתת כברירת מחדל עד Android‏ 15.
  • backend.<type>.apex_available: רשימת שמות APEX שהספרייה הגנרית שנוצרה זמינה עבורם.
  • backend.[cpp|java].gen_log: דגל אופציונלי שקובע אם ליצור קוד נוסף לאיסוף מידע על העסקה.
  • backend.[cpp|java].vndk.enabled: דגל אופציונלי שמאפשר להפוך את הממשק הזה לחלק מ-VNDK. ברירת המחדל היא false.
  • backend.[cpp|ndk].additional_shared_libraries: נוסף ב-Android 14. הדגל הזה מוסיף תלויות לספריות המקוריות. הסימון הזה שימושי עם ndk_header ועם cpp_header.
  • backend.java.sdk_version: דגל אופציונלי לציון הגרסה של ה-SDK שלפיה נבנתה ספריית ה-stub של Java. ערך ברירת המחדל הוא "system_current". לא צריך להגדיר את הערך הזה אם הערך של backend.java.platform_apis הוא true.
  • backend.java.platform_apis: דגל אופציונלי שצריך להגדיר אותו לערך true כשצריך לבנות את הספריות שנוצרו מול ה-API של הפלטפורמה ולא מול ה-SDK.

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

כתיבת קובצי AIDL

ממשקים ב-AIDL יציב דומים לממשקים רגילים, למעט העובדה שאסור להם להשתמש ב-parcelables לא מובנים (כי הם לא יציבים! ראו ממשקי AIDL מובנים לעומת יציבים). ההבדל העיקרי ב-AIDL יציב הוא באופן ההגדרה של parcelables. בעבר, אובייקטים מסוג Parcelable היו מוצהרים מראש. ב-AIDL יציב (ולכן מובנה), שדות ומשתנים מסוג Parcelable מוגדרים באופן מפורש.

// in a file like 'some/package/Thing.aidl'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}

אפשר להגדיר ערך ברירת מחדל (אבל לא חובה) בשדות boolean,‏ char,‏ float,‏ double,‏ byte,‏ int,‏ long ו-String. ב-Android 12, יש גם תמיכה בברירות מחדל לספירות שמוגדרות על ידי המשתמש. אם לא מציינים ערך ברירת מחדל, המערכת משתמשת בערך שדומה ל-0 או בערך ריק. אם לא מוגדר ערך ברירת מחדל לספירה, היא מאותחלת ל-0 גם אם אין ערך 0 בספירה.

שימוש בספריות stub

אחרי שמוסיפים ספריות stub כתלות למודול, אפשר לכלול אותן בקבצים. דוגמאות לספריות stub במערכת build (אפשר להשתמש גם ב-Android.mk להגדרות מודולים מדור קודם). שימו לב: בדוגמאות האלה הגרסה לא מופיעה, ולכן הן מייצגות שימוש בממשק לא יציב. אבל שמות של ממשקים עם גרסאות כוללים מידע נוסף. אפשר לעיין במאמר בנושא ניהול גרסאות של ממשקים.

cc_... {
    name: ...,
    // use `shared_libs:` to load your library and its transitive dependencies
    // dynamically
    shared_libs: ["my-module-name-cpp"],
    // use `static_libs:` to include the library in this binary and drop
    // transitive dependencies
    static_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // use `static_libs:` to add all jars and classes to this jar
    static_libs: ["my-module-name-java"],
    // use `libs:` to make these classes available during build time, but
    // not add them to the jar, in case the classes are already present on the
    // boot classpath (such as if it's in framework.jar) or another jar.
    libs: ["my-module-name-java"],
    // use `srcs:` with `-java-sources` if you want to add classes in this
    // library jar directly, but you get transitive dependencies from
    // somewhere else, such as the boot classpath or another jar.
    srcs: ["my-module-name-java-source", ...],
    ...
}
# or
rust_... {
    name: ...,
    rustlibs: ["my-module-name-rust"],
    ...
}

דוגמה ב-C++‎:

#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
    // use just like traditional AIDL

דוגמה ב-Java:

import some.package.IFoo;
import some.package.Thing;
...
    // use just like traditional AIDL

דוגמה ב-Rust:

use aidl_interface_name::aidl::some::package::{IFoo, Thing};
...
    // use just like traditional AIDL

ניהול גרסאות של ממשקים

הצהרה על מודול עם השם foo יוצרת גם יעד במערכת ה-build שאפשר להשתמש בו כדי לנהל את ה-API של המודול. כשמבצעים build, foo-freeze-api מוסיף הגדרת API חדשה ב-api_dir או ב-aidl_api/name, בהתאם לגרסת Android, ומוסיף קובץ .hash. שניהם מייצגים את הגרסה החדשה של הממשק. foo-freeze-api מעדכן גם את המאפיין versions_with_info כדי לשקף את הגרסה הנוספת, ואת imports כדי לשקף את הגרסה. בעצם, הערך imports ב-versions_with_info מועתק מהשדה imports. אבל הגרסה היציבה העדכנית מוגדרת ב-imports ב-versions_with_info עבור הייבוא, שלא כולל גרסה מפורשת. אחרי שמציינים את המאפיין versions_with_info, מערכת ה-build מריצה בדיקות תאימות בין גרסאות קפואות וגם בין הגרסה העדכנית ביותר של העץ (ToT) לבין הגרסה הקפואה האחרונה.

בנוסף, צריך לנהל את הגדרת ה-API של גרסת ToT. בכל פעם שמעדכנים API, מריצים את foo-update-api כדי לעדכן את aidl_api/name/current שמכיל את הגדרת ה-API של גרסת ה-ToT.

כדי לשמור על היציבות של ממשק, הבעלים יכולים להוסיף:

  • שיטות עד סוף ממשק (או שיטות עם סדרות חדשות שמוגדרות באופן מפורש)
  • אלמנטים לסוף של parcelable (נדרש להוסיף ברירת מחדל לכל אלמנט)
  • ערכים קבועים
  • ב-Android 11, מונים
  • ב-Android 12, שדות עד סוף האיחוד

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

כדי לבדוק שכל הממשקים קפואים לגרסה, אפשר לבנות עם הגדרת משתני הסביבה הבאים:

  • AIDL_FROZEN_REL=true m ... – כדי לבצע build צריך להקפיא את כל ממשקי AIDL היציבים שלא צוין בהם שדה owner:.
  • AIDL_FROZEN_OWNERS="aosp test" – כדי ליצור את הגרסה, צריך להקפיא את כל ממשקי AIDL היציבים, ולציין את השדה owner: כ-'aosp' או כ-'test'.

יציבות הייבוא

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

בפלטפורמת Android, הגרסה android.hardware.graphics.common היא הדוגמה הכי טובה לשדרוג גרסה מהסוג הזה.

שימוש בממשקים עם גרסאות

שיטות ממשק

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

  • cpp backend מקבל ::android::UNKNOWN_TRANSACTION.
  • ndk backend מקבל STATUS_UNKNOWN_TRANSACTION.
  • קצה העורפי java מקבל את android.os.RemoteException עם הודעה שאומרת שה-API לא מיושם.

במאמרים שאילתות של גרסאות ושימוש בערכי ברירת מחדל מוסבר איך לטפל בבעיה הזו.

Parcelables

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

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

סוגי enum וקבועים

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

איגודים

ניסיון לשלוח איחוד עם שדה חדש ייכשל אם המקבל ישן ולא יודע על השדה. ההטמעה אף פעם לא תראה את האיחוד עם השדה החדש. המערכת מתעלמת מהכשל אם מדובר בעסקה חד-כיוונית, אחרת השגיאה היא BAD_VALUE(ב-backend של C++‎ או NDK) או IllegalArgumentException(ב-backend של Java). השגיאה מתקבלת אם הלקוח שולח קבוצת איחוד לשדה החדש בשרת ישן, או אם מדובר בלקוח ישן שמקבל את האיחוד משרת חדש.

ניהול של כמה גרסאות

במרחב שמות של linker ב-Android יכולה להיות רק גרסה אחת של aidlממשק ספציפי כדי למנוע מצבים שבהם לסוגים שנוצרו יש כמה הגדרות.aidl ב-C++ יש את כלל ההגדרה היחידה (One Definition Rule,‏ ODR) שדורש הגדרה אחת בלבד של כל סמל.

ב-Android build מוצגת שגיאה כשמודול מסוים תלוי בגרסאות שונות של אותה ספרייה aidl_interface. יכול להיות שהמודול תלוי בספריות האלה באופן ישיר או עקיף, דרך התלויות של התלויות שלו. השגיאות האלה מציגות את תרשים התלות מהמודול שנכשל לגרסאות המתנגשות של ספריית aidl_interface. צריך לעדכן את כל התלויות כך שיכללו את אותה גרסה (בדרך כלל הגרסה האחרונה) של הספריות האלה.

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

cc_defaults {
  name: "my.aidl.my-process-group-ndk-shared",
  shared_libs: ["my.aidl-V3-ndk"],
  ...
}

cc_library {
  name: "foo",
  defaults: ["my.aidl.my-process-group-ndk-shared"],
  ...
}

cc_binary {
  name: "bar",
  defaults: ["my.aidl.my-process-group-ndk-shared"],
  ...
}

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

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

aidl_interface_defaults {
  name: "android.popular.common-latest-defaults",
  imports: ["android.popular.common-V3"],
  ...
}

aidl_interface {
  name: "android.foo",
  defaults: ["my.aidl.latest-ndk-shared"],
  ...
}

aidl_interface {
  name: "android.bar",
  defaults: ["my.aidl.latest-ndk-shared"],
  ...
}

פיתוח מבוסס-סימון

אי אפשר להשתמש בממשקים שנמצאים בפיתוח (לא קפואים) במכשירים שמופצים, כי אין ערובה לכך שהם יהיו תואמים לאחור.

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

סימון ה-Build של AIDL

הדגל שקובע את ההתנהגות הזו הוא RELEASE_AIDL_USE_UNFROZEN, שמוגדר ב-build/release/build_flags.bzl. ‫true מציין שהגרסה הלא קפואה של הממשק נמצאת בשימוש בזמן הריצה, ו-false מציין שהספריות של הגרסאות הלא קפואות מתנהגות כמו הגרסה הקפואה האחרונה שלהן. אפשר לשנות את הערך של הדגל ל-true לצורך פיתוח מקומי, אבל צריך להחזיר אותו ל-false לפני הפרסום. בדרך כלל, הפיתוח מתבצע עם הגדרה שבה הדגל מוגדר לערך true.

מטריצת תאימות ומניפסטים

אובייקטים של ממשק הספק (אובייקטים של VINTF) מגדירים אילו גרסאות צפויות ואילו גרסאות מסופקות בכל אחד מהצדדים של ממשק הספק.

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

מטריצות

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

לדוגמה, כשמוסיפים גרסה 4 לא קפואה, משתמשים ב-<version>3-4</version>.

אחרי שגרסה 4 קפאה, אפשר להסיר את גרסה 3 מטבלת התאימות כי גרסה 4 הקפואה משמשת כש-RELEASE_AIDL_USE_UNFROZEN הוא false.

מניפסטים

ב-Android 15, בוצע שינוי ב-libvintf כדי לשנות את קובצי המניפסט בזמן ה-build על סמך הערך של RELEASE_AIDL_USE_UNFROZEN.

במניפסטים ובקטעי המניפסטים מוצהרת הגרסה של הממשק ששירות מסוים מיישם. כשמשתמשים בגרסה העדכנית של ממשק שלא הוקפאה, צריך לעדכן את המניפסט כדי לשקף את הגרסה החדשה. כש-RELEASE_AIDL_USE_UNFROZEN=false מתבצעת התאמה של רשומות המניפסט על ידי libvintf כדי לשקף את השינוי בספריית ה-AIDL שנוצרה. הגרסה עברה שינוי מהגרסה שלא הוקפאה, N, לגרסה האחרונה שהוקפאה, N - 1. לכן, המשתמשים לא צריכים לנהל כמה מניפסטים או קטעי מניפסטים לכל אחד מהשירותים שלהם.

שינויים בלקוח HAL

קוד הלקוח של HAL צריך להיות תואם לאחור לכל גרסה קודמת קפואה נתמכת. אם RELEASE_AIDL_USE_UNFROZEN הוא false, השירותים תמיד ייראו כמו הגרסה הקפואה האחרונה או גרסה קודמת (לדוגמה, קריאה לשיטות חדשות שלא הוקפאו מחזירה UNKNOWN_TRANSACTION, או שלשדות חדשים של parcelable יש ערכי ברירת מחדל). לקוחות של מסגרת Android נדרשים להיות תואמים לאחור עם גרסאות קודמות נוספות, אבל זהו פרט חדש עבור לקוחות של ספקים ולקוחות של ממשקים בבעלות שותפים.

שינויים בהטמעה של HAL

ההבדל הכי גדול בפיתוח HAL עם פיתוח מבוסס-דגלים הוא הדרישה שהטמעות HAL יהיו תואמות לאחור לגרסה הקודמת הקפואה כדי לפעול כש-RELEASE_AIDL_USE_UNFROZEN הוא false. תאימות לאחור בהטמעות ובקוד המכשיר היא תרגול חדש. שימוש בממשקי API עם גרסאות

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

דוגמה: לממשק יש שלוש גרסאות קפואות. הממשק מתעדכן עם שיטה חדשה. הלקוח והשירות יעודכנו לשימוש בספרייה החדשה בגרסה 4. מכיוון שספריית V4 מבוססת על גרסה לא קפואה של הממשק, היא מתנהגת כמו הגרסה הקפואה האחרונה, גרסה 3, כש-RELEASE_AIDL_USE_UNFROZEN הוא false, ומונעת את השימוש בשיטה החדשה.

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

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

// Get the callback along with the version of the callback
ScopedAStatus RegisterMyCallback(const std::shared_ptr<IMyCallback>& cb) override {
    mMyCallback = cb;
    // Get the version of the callback for later when we call methods on it
    auto status = mMyCallback->getInterfaceVersion(&mMyCallbackVersion);
    return status;
}

// Example of using the callback later
void NotifyCallbackLater() {
  // From the latest frozen version (V2)
  mMyCallback->foo();
  // Call this method from the unfrozen V3 only if the callback is at least V3
  if (mMyCallbackVersion >= 3) {
    mMyCallback->bar();
  }
}

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

אי אפשר לשלוח או לקבל סוגים חדשים שנוספו בגרסה הזו שלא הוקפאה דרך הממשק.

ההטמעה אף פעם לא מקבלת קריאה לשיטות חדשות מלקוחות כלשהם כש-RELEASE_AIDL_USE_UNFROZEN הוא false.

חשוב להשתמש בספירות חדשות רק בגרסה שבה הן הוצגו, ולא בגרסה הקודמת.

בדרך כלל, משתמשים ב-foo->getInterfaceVersion() כדי לראות באיזו גרסה משתמש הממשק המרוחק. אבל אם אתם משתמשים בתמיכה בניהול גרסאות מבוסס-דגלים, אתם מטמיעים שתי גרסאות שונות, ולכן כדאי לכם לקבל את הגרסה של הממשק הנוכחי. אפשר לעשות את זה באמצעות קבלת גרסת הממשק של האובייקט הנוכחי, לדוגמה, this->getInterfaceVersion() או השיטות האחרות של my_ver. מידע נוסף זמין במאמר בנושא שאילתות לגבי גרסת הממשק של אובייקט מרוחק.

ממשקי VINTF יציבים חדשים

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

אפשר להוסיף את השירותים באופן מותנה על סמך הערך של הדגל RELEASE_AIDL_USE_UNFROZEN בקובץ ה-makefile של המכשיר:

ifeq ($(RELEASE_AIDL_USE_UNFROZEN),true)
PRODUCT_PACKAGES += \
    android.hardware.health.storage-service
endif

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

ממשקי תוספים יציבים חדשים של VINTF

לממשקי תוספים חדשים אין גרסה קודמת שאפשר לחזור אליה, ומכיוון שהם לא רשומים ב-ServiceManager או מוצהרים במניפסטים של VINTF, אי אפשר להשתמש ב-IServiceManager::isDeclared() כדי לקבוע מתי לצרף את ממשק התוסף לממשק אחר.

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

בדיקות VTS של vts_treble_vintf_vendor_test ו-vts_treble_vintf_framework_test מזהות מתי נעשה שימוש בממשק תוסף לא קפוא במכשיר שיצא לשוק, ומציגות שגיאה.

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

‫Cuttlefish ככלי פיתוח

בכל שנה אחרי שה-VINTF קפוא, אנחנו משנים את מטריצת התאימות של ה-framework‏ (FCM) target-level ואת PRODUCT_SHIPPING_API_LEVEL של Cuttlefish, כדי שהם ישקפו את המכשירים שיושקו עם הגרסה של השנה הבאה. אנחנו משנים את target-level וPRODUCT_SHIPPING_API_LEVEL כדי לוודא שיש מכשיר השקה שנבדק ועומד בדרישות החדשות של הגרסה הבאה בשנה הבאה.

כש-RELEASE_AIDL_USE_UNFROZEN הוא true, נעשה שימוש ב-Cuttlefish לפיתוח של גרסאות Android עתידיות. היא מטרגטת את רמת ה-FCM של גרסת Android של השנה הבאה, PRODUCT_SHIPPING_API_LEVEL, ולכן היא צריכה לעמוד בדרישות התוכנה של הספק (VSR) של הגרסה הבאה.

כש-RELEASE_AIDL_USE_UNFROZEN הוא false, ל-Cuttlefish יש את target-level ו-PRODUCT_SHIPPING_API_LEVEL הקודמים כדי לשקף מכשיר שמופץ. ב-Android 14 ובגרסאות קודמות, ההבחנה הזו מתבצעת באמצעות ענפים שונים של Git שלא כוללים את השינוי ב-FCM target-level, ברמת ה-API של המשלוח או בכל קוד אחר שמיועד לגרסה הבאה.

כללים למתן שמות למודולים

ב-Android 11, לכל שילוב של הגרסאות והקצה העורפי המופעל, נוצר באופן אוטומטי מודול של ספריית stub. כדי להתייחס למודול ספציפי של ספריית stub לקישור, לא משתמשים בשם של מודול aidl_interface, אלא בשם של מודול ספריית ה-stub, שהוא ifacename-version-backend, כאשר

  • ifacename: השם של מודול aidl_interface
  • version הוא אחד מהערכים הבאים:
    • Vversion-number לגרסאות הקפואות
    • Vlatest-frozen-version-number + 1 לגרסה העדכנית ביותר (שעדיין לא קפאה)
  • backend הוא אחד מהערכים הבאים:
    • java ל-Java backend,
    • cpp ל-C++‎ בצד השרת,
    • ndk או ndk_platform עבור העורף של NDK. הראשון מיועד לאפליקציות, והשני מיועד לשימוש בפלטפורמה עד Android 13. ב-Android מגרסה 13 ואילך, משתמשים רק ב-ndk.
    • rust ל-Rust backend.

נניח שיש מודול בשם foo והגרסה האחרונה שלו היא 2, והוא תומך גם ב-NDK וגם ב-C++. במקרה כזה, AIDL יוצר את המודולים הבאים:

  • על סמך גרסה 1
    • foo-V1-(java|cpp|ndk|ndk_platform|rust)
  • מבוסס על גרסה 2 (הגרסה היציבה העדכנית)
    • foo-V2-(java|cpp|ndk|ndk_platform|rust)
  • על סמך גרסת ToT
    • foo-V3-(java|cpp|ndk|ndk_platform|rust)

בהשוואה ל-Android 11:

  • foo-backend, שהתייחס לגרסה היציבה האחרונה, הופך ל-foo-V2-backend
  • foo-unstable-backend, שהתייחס לגרסת ה-ToT הופך ל-foo-V3-backend

שמות קובצי הפלט תמיד זהים לשמות המודולים.

  • על סמך גרסה 1: foo-V1-(cpp|ndk|ndk_platform|rust).so
  • על סמך גרסה 2: foo-V2-(cpp|ndk|ndk_platform|rust).so
  • על סמך גרסת ToT: foo-V3-(cpp|ndk|ndk_platform|rust).so

שימו לב: קומפיילר AIDL לא יוצר מודול גרסה unstable או מודול ללא גרסה לממשק AIDL יציב. החל מ-Android 12, שם המודול שנוצר מממשק AIDL יציב תמיד כולל את הגרסה שלו.

שיטות חדשות של ממשק מטא

ב-Android 10 נוספו כמה שיטות לממשק מטא ל-AIDL יציב.

שאילתה לגבי גרסת הממשק של האובייקט המרוחק

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

דוגמה לשימוש ב-cpp backend:

sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::HASH;
std::string remote_hash = foo->getInterfaceHash();

דוגמה לשימוש בקצה העורפי ndk (ובקצה העורפי ndk_platform):

IFoo* foo = ... // the remote object
int32_t my_ver = IFoo::version;
int32_t remote_ver = 0;
if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::hash;
std::string remote_hash;
foo->getInterfaceHash(&remote_hash);

דוגמה לשימוש ב-java backend:

IFoo foo = ... // the remote object
int myVer = IFoo.VERSION;
int remoteVer = foo.getInterfaceVersion();
if (remoteVer < myVer) {
  // the remote side is using an older interface
}

String myHash = IFoo.HASH;
String remoteHash = foo.getInterfaceHash();

בשפת Java, הצד המרוחק חייב להטמיע את getInterfaceVersion() ואת getInterfaceHash() באופן הבא (משתמשים ב-super במקום ב-IFoo כדי למנוע טעויות בהעתקה ובהדבקה). יכול להיות שיהיה צורך בהערה @SuppressWarnings("static") כדי להשבית את האזהרות, בהתאם להגדרה של javac):

class MyFoo extends IFoo.Stub {
    @Override
    public final int getInterfaceVersion() { return super.VERSION; }

    @Override
    public final String getInterfaceHash() { return super.HASH; }
}

הסיבה לכך היא שהמחלקות שנוצרו (IFoo,‏ IFoo.Stub וכו') משותפות בין הלקוח לשרת (לדוגמה, המחלקות יכולות להיות בנתיב המחלקות של האתחול). כשמשתפים כיתות, השרת מקושר גם לגרסה החדשה ביותר של הכיתות, גם אם הוא נוצר עם גרסה ישנה יותר של הממשק. אם ממשק המטא הזה מיושם במחלקה המשותפת, הוא תמיד מחזיר את הגרסה החדשה ביותר. עם זאת, אם מטמיעים את השיטה כמו בדוגמה שלמעלה, מספר הגרסה של הממשק מוטמע בקוד של השרת (כי IFoo.VERSION הוא static final int שמוטמע בשורה כשמפנים אליו), ולכן השיטה יכולה להחזיר את הגרסה המדויקת שבה השרת נבנה.

איך עובדים עם ממשקים ישנים

יכול להיות שהלקוח עודכן לגרסה החדשה יותר של ממשק AIDL, אבל השרת משתמש בממשק AIDL הישן. במקרים כאלה, הפעלת method בממשק ישן מחזירה UNKNOWN_TRANSACTION.

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

דוגמה ב-C++ ב-Android 13 ואילך:

class MyDefault : public IFooDefault {
  Status anAddedMethod(...) {
   // do something default
  }
};

// once per an interface in a process
IFoo::setDefaultImpl(::android::sp<MyDefault>::make());

foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                         // remote side is not implementing it

דוגמה ב-Java:

IFoo.Stub.setDefaultImpl(new IFoo.Default() {
    @Override
    public xxx anAddedMethod(...)  throws RemoteException {
        // do something default
    }
}); // once per an interface in a process

foo.anAddedMethod(...);

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

המרת AIDL קיים ל-AIDL מובנה או יציב

אם יש לכם ממשק AIDL קיים וקוד שמשתמש בו, אתם יכולים לבצע את השלבים הבאים כדי להמיר את הממשק לממשק AIDL יציב.

  1. מזהים את כל יחסי התלות של הממשק. לכל חבילה שהממשק תלוי בה, צריך לקבוע אם החבילה מוגדרת ב-AIDL יציב. אם לא מוגדר, צריך להמיר את החבילה.

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

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