AIDL יציב

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

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

גרסת AIDL מובנית לעומת גרסת AIDL יציבה

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

כדי ליצור קובצי AIDL יציבים, צריך ליצור קובצי AIDL מובְנים כדי שמערכת ה-build והמְהַדר יוכלו להבין אם השינויים שבוצעו ב-Parcelables תואמים לאחור. עם זאת, לא כל הממשקים המובנים הם יציבים. כדי שהממשק יהיה יציב, צריך להשתמש בו רק בסוגי נתונים מובְנים, וגם בתכונות הבאות של ניהול גרסאות. לעומת זאת, ממשק לא יציב אם מערכת הליבה של ה-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 שלכם משתמש בממשק או ב-Parcelable מ-aidl_interface אחר, מציינים כאן את השם שלו. אפשר להשתמש בשם לבד כדי להתייחס לגרסה האחרונה, או בשם עם הסיומת של הגרסה (כמו -V1) כדי להתייחס לגרסה ספציפית. אפשר לציין גרסה החל מגרסה Android 12
  • versions: הגרסאות הקודמות של הממשק שקפואות מתחת ל-api_dir, החל מ-Android 11, versions קפואות מתחת ל-aidl_api/name. אם אין גרסאות קפואות של ממשק, לא צריך לציין זאת ולא יתבצעו בדיקות תאימות. השדה הזה הוחלף בשדה versions_with_info ל-Android 13 ואילך.
  • versions_with_info: רשימה של צמדי מחרוזות (tuples), שכל אחד מהם מכיל את השם של גרסת קריאו ורשימה של גרסאות ייבוא של מודולים אחרים של 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 מופעלים כברירת מחדל. אם לא צריך אף אחד משלושת הקצוות העורפי האלה, צריך להשבית אותו באופן מפורש. Rust מושבת כברירת מחדל עד Android 15.
  • 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 your preference 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 של המודול. במהלך ה-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" – כדי לבצע build, צריך להקפיא את כל ממשקי ה-AIDL היציבים עם השדה owner: שמוגדר כ-"aosp" או כ-"test".

יציבות של נתוני ייבוא

עדכון הגרסאות של הייבוא לגרסאות קפואות של ממשק תואם לאחור בשכבת Stable 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). השגיאה מתקבלת אם הלקוח שולח שילוב שמוגדר לשדה החדש לשרת ישן, או אם מדובר בלקוח ישן שמקבל את השילוב משרת חדש.

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

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

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

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

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

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

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

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

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

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

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

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

Cuttlefish ככלי פיתוח

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

כשהערך של RELEASE_AIDL_USE_UNFROZEN הוא true, הוא משמש לפיתוח גרסאות עתידיות של 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,
    • 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:

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

עבודה עם ממשקים ישנים יותר

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

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

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

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