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. לדוגמה, הצהרה על חבילה (חבילה בהתאמה אישית) היא לא 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 לא מוגדר, מערכת ה-build בודקת שהממשק תואם לאחור, אלא אם מצוין המאפיין 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 שהספרייה הגנרית שנוצרה זמינה עבורם.
  • 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 מריצה בדיקות תאימות בין גרסאות קפואות, וגם בין הגרסה העדכנית ביותר של העץ (ToT) לבין הגרסה הקפואה האחרונה.

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

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

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

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

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

  • 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 backend מקבל android.os.RemoteException עם הודעה שאומרת שה-API לא מיושם.

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

Parcelables

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

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

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

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

איגודים

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

כותרת גרסה ברמת המודול (C++‎ / NDK)

במודולים יציבים של AIDL עם גרסאות שמשתמשים ב-backend של cpp או ndk,‏ Soong יוצר באופן אוטומטי כותרת סינתטית ברמת המודול:

  • קצה עורפי של C++‎: #include "<module-name>.h"
  • קצה עורפי של NDK: #include "aidl/<module-name>.h"

הכותרת הזו מגדירה את פקודת המאקרו AIDL_VERSION_<MODULE_NAME> עם נקודות . ומקפים - בשם המודול שהומרו לקווים תחתונים באותיות רישיות _. לפקודת המאקרו מוקצה מספר הגרסה של מודול הממשק על סמך גרסת הספרייה שבה נעשה שימוש.

לדוגמה, אפשר להשתמש בפקודת המאקרו מ-android.hardware.foo-V2-ndk:

#include <aidl/android.hardware.foo.h>

#if AIDL_VERSION_ANDROID_HARDWARE_FOO >= 2
// Code for version 2 or higher
#endif

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

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

ה-build של Android מספק שגיאה כשמודול מסוים תלוי בגרסאות שונות של אותה ספריית 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_interface שיובאו למספר מודולים של aidl_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

הדגל שקובע את ההתנהגות הזו הוא 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++‎ backend,
    • 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. אין צורך לבטל את השיטות שמוטמעות בצד המרוחק (כי אתם בטוחים שהצד המרוחק נוצר כשהשיטות היו בתיאור של ממשק AIDL) במחלקת ברירת המחדל impl.

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

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

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

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

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