AIDL יציב

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

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

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

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

כדי להשתמש ב-Stable AIDL נדרש פרוטוקול AIDL מובנה, כדי שמערכת ה-build והמהדר (compiler) יוכלו להבין אם שינויים שבוצעו במגרשים עומדים בדרישות של תאימות לאחור. עם זאת, לא כל הממשקים המובנים הם יציבים. כדי שהממשק יהיה יציב, הוא צריך להשתמש רק בסוגים מובנים, והוא צריך גם להשתמש בתכונות הבאות של ניהול גרסאות. לעומת זאת, ממשק לא יציב אם מערכת הליבה של ה-build משמשת כדי לבנות אותו, או אם מגדירים את 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 משתמש בממשק או בחבילה מ-aidl_interface אחר, ציינו את השם שלו כאן. זה יכול להיות השם עצמו, כדי להתייחס לגרסה העדכנית ביותר, או השם עם הסיומת של הגרסה (למשל -V1) כדי להתייחס לגרסה ספציפית. ציון הגרסה נתמך מאז Android 12
  • versions: הגרסאות הקודמות של הממשק שקפואות מתחת ל-api_dir, החל מ-Android 11, versions קפואות מתחת ל-aidl_api/name. אם אין גרסאות קפואות של ממשק, אין לציין את זה ולא יהיו בדיקות תאימות. השדה הזה הוחלף ב-versions_with_info ל-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 לא יוצרת את תמונת ה-API של הממשק ולא דורשת לעדכן אותו.
  • frozen: הדגל האופציונלי כשמגדירים את הערך true הוא שבממשק אין שינויים מאז הגרסה הקודמת של הממשק. כך מתאפשרת יותר בדיקות בזמן ה-build. אם המדיניות מוגדרת לערך false, הממשק נמצא בשלבי פיתוח ומתבצעים בו שינויים חדשים. לכן הרצה של foo-freeze-api תיצור גרסה חדשה ותשנה את הערך באופן אוטומטי ל-true. התכונה הושקה ב-Android 14.
  • backend.<type>.enabled: הדגלים האלה מפעילים או משביתים את כל הקצוות העורפיים שהמהדר של AIDL יוצר עבורם קוד. נכון לעכשיו יש תמיכה בארבעה קצוות עורפיים: Java, C++ , NDK ו-Rust. הקצוות העורפיים של Java, C++ ו-NDK מופעלים כברירת מחדל. אם אין צורך באחד משלושת הקצוות העורפיים האלה, צריך להשבית אותו באופן מפורש. חלודה מושבתת כברירת מחדל עד Android 15 (AOSP ניסיוני).
  • 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 יציב דומים לממשקים רגילים, אלא אם מותר להשתמש בהם במגרשים לא מובנים (כי הם לא יציבים, תוכלו לקרוא הסבר מובנה לעומת AIDL). ההבדל העיקרי ב-AIDL יציב הוא האופן שבו מגרשים מוגדרים. בעבר, היה הצהרה קדימה על מגרשים. ב-AIDL יציב (ולכן מובנה), השדות והמשתנים של מגרשים מוגדרים באופן מפורש.

// 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: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // can also be shared_libs if desire is to load a library and share
    // it among multiple users or if you only need access to constants
    static_libs: ["my-module-name-java"],
    ...
}
# 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

דוגמה בחלודה:

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

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

ההצהרה על מודול בשם foo יוצרת גם יעד במערכת ה-build, שבו אפשר להשתמש כדי לנהל את ה-API של המודול. אחרי הפיתוח, 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.

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

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

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

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

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

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

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

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

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

שיטות ממשק

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

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

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

מגרשים

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

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

ערכים של טיפוסים בני מנייה (enum) וקבועים

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

איגודים

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

פיתוח מבוסס-סימונים

אי אפשר להשתמש בממשקים בתהליך פיתוח במכשירים להפצה, כי לא מובטח שהם יהיו תואמים לאחור.

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

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

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

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

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

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

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

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

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

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

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

כשהערך של RELEASE_AIDL_USE_UNFROZEN הוא false, יש לו ה-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,
    • cpp עבור הקצה העורפי של C++,
    • ndk או ndk_platform בשביל הקצה העורפי של NDK. הראשון מיועד לאפליקציות והשני לשימוש בפלטפורמה.
    • 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, שנקרא גרסת התנאים וההגבלות הופכת ל-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

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

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

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

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

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

דוגמה עם הקצה העורפי cpp:

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:

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

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

המרה של AIDL קיים ל-AIDL מובנה/יציב

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

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

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

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