AIDL יציב

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

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

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

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

דוגמה ב-Rust:

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

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

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

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

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

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

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

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

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

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

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

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

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

שיטות ממשק

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

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

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

Parcelables

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

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

ערכים מוגדרים מראש וקבועים

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

איגודים

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

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

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

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

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

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

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