Stable AIDL

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

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

ממשק AIDL מובנה לעומת ממשק AIDL יציב

Structured AIDL מתייחס לסוגים שמוגדרים אך ורק ב-AIDL. לדוגמה, הצהרה על אובייקט שניתן להעברה (אובייקט שניתן להעברה בהתאמה אישית) היא לא 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 מוגדרת. הגדרה כלא מוגדרת מתאימה לממשק עם יציבות בהקשר של הקומפילציה (כלומר, כל הדברים במערכת, למשל, דברים ב-system.img ובמחיצות קשורות, או כל הדברים של הספק, למשל, דברים ב-vendor.img ובמחיצות קשורות). אם stability מוגדר כ-"vintf", זה מתאים להבטחת יציבות: הממשק חייב להישאר יציב כל עוד נעשה בו שימוש.
  • gen_trace: דגל אופציונלי להפעלה או להשבתה של המעקב. החל מ-Android 14, ברירת המחדל היא true עבור קצה העורפי 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 האלה, צריך להשבית אותו באופן מפורש. עד גרסה Android 15,‏ Rust מושבתת כברירת מחדל.
  • backend.<type>.apex_available: רשימת שמות ה-APEX שספריית ה-stub שנוצרה זמינה עבורם.
  • 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 גם אם אין סופר אפס.

שימוש בספריות 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 מריצה בדיקות תאימות בין גרסאות קפואות וגם בין Top of Tree‏ (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 gets ::android::UNKNOWN_TRANSACTION.
  • ndk backend gets STATUS_UNKNOWN_TRANSACTION.
  • קצה העורפי java מקבל את android.os.RemoteException עם הודעה שאומרת שה-API לא הוטמע.

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

Parcelables

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

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

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

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

איגודים

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

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

במרחב שמות של 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 עם גרסאות

AIDL build flag

הדגל שקובע את ההתנהגות הזו הוא 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, Service Manager לא יאפשר לשירות לרשום את הממשק והלקוחות לא ימצאו אותו.

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

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