איידל יציב

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

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

הגדרת ממשק 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: ["ohter-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 המרכיבים את הממשק. הנתיב עבור Foo מסוג AIDL המוגדר בחבילה 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 ) כדי להתייחס לגרסה ספציפית. ציון גרסה נתמך מאז אנדרואיד 12
  • versions : הגרסאות הקודמות של הממשק שהוקפאו תחת api_dir , החל באנדרואיד 11, versions מוקפאות תחת aidl_api/ name . אם אין גרסאות קפואות של ממשק, אין לציין זאת, ולא יהיו בדיקות תאימות. שדה זה הוחלף ב- versions_with_info עבור 13 ומעלה.
  • versions_with_info : רשימת tuples, שכל אחת מהן מכילה שם של גרסה קפואה ורשימה עם ייבוא ​​גרסאות של מודולי aidl_interface אחרים שגרסה זו של aidl_interface ייבאה. ההגדרה של גרסה V של ממשק AIDL IFACE ממוקמת ב- aidl_api/ IFACE / V . שדה זה הוצג באנדרואיד 13, והוא לא אמור להשתנות ישירות ב-Android.bp. השדה מתווסף או מתעדכן על ידי הפעלת *-update-api או *-freeze-api . כמו כן, שדות versions מועברים אוטומטית ל- versions_with_info כאשר משתמש מפעיל *-update-api או *-freeze-api .
  • stability : הדגל האופציונלי להבטחת היציבות של ממשק זה. כרגע תומך רק ב- "vintf" . אם זה לא מוגדר, זה מתאים לממשק עם יציבות בהקשר הקומפילציה הזה (לכן ניתן להשתמש בממשק שנטען כאן רק עם דברים שהידור ביחד, למשל ב-system.img). אם זה מוגדר ל- "vintf" , זה מתאים להבטחת יציבות: יש לשמור על הממשק יציב כל עוד נעשה בו שימוש.
  • gen_trace : הדגל האופציונלי להפעיל או לכבות את המעקב. ברירת המחדל היא false .
  • host_supported : הדגל האופציונלי שכאשר מוגדר כ- true הופך את הספריות שנוצרות לזמינות לסביבת המארח.
  • unstable : הדגל האופציונלי המשמש לסימון שממשק זה אינו צריך להיות יציב. כאשר זה מוגדר true , מערכת ה-build לא יוצרת את ה-API dump עבור הממשק וגם לא דורשת את עדכון זה.
  • backend.<type>.enabled : דגלים אלה מחליפים את כל אחד מהחלקים האחוריים שעבורם מהדר AIDL מייצר קוד. נכון לעכשיו, ארבעה קצה אחורי נתמכים: Java, C++, NDK ו-Rust. הקצה האחורי של Java, C++ ו-NDK מופעלים כברירת מחדל. אם אין צורך באחד משלושת הקצה האחורי הללו, יש להשבית אותו במפורש. חלודה מושבתת כברירת מחדל.
  • backend.<type>.apex_available : רשימת שמות ה-APEX שספריית העצבים שנוצרה זמינה עבורם.
  • backend.[cpp|java].gen_log : הדגל האופציונלי השולט אם ליצור קוד נוסף לאיסוף מידע על העסקה.
  • backend.[cpp|java].vndk.enabled : הדגל האופציונלי להפוך את הממשק הזה לחלק מ-VNDK. ברירת המחדל היא false .
  • backend.java.platform_apis : הדגל האופציונלי השולט אם ספריית ה-Stubs של Java בנויה מול ממשקי ה-API הפרטיים מהפלטפורמה. זה צריך להיות מוגדר כ"נכון "true" כאשר stability מוגדרת ל "vintf" .
  • backend.java.sdk_version : הדגל האופציונלי לציון הגרסה של ה-SDK שספריית ה-Stubs של Java בנויה נגדה. ברירת המחדל היא "system_current" . אין להגדיר זאת כאשר backend.java.platform_apis נכון.
  • backend.java.platform_apis : הדגל האופציונלי שאמור להיות מוגדר כ- true כאשר הספריות שנוצרות צריכות לבנות מול ה-API של הפלטפורמה ולא מול ה-SDK.

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

כתיבת קבצי AIDL

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

שימוש בספריות בדל

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

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

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

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

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

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

יציבות היבוא

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

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

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

שיטות ממשק

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

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

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

חפצים

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

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

מינונים וקבועים

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

איגודים

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

כללי מתן שמות של מודול

ב-Android 11, עבור כל שילוב של הגרסאות והחלקים האחוריים המופעלים, מודול ספריית בדל נוצר באופן אוטומטי. כדי להתייחס למודול ספריית קטעים ספציפי לקישור, אל תשתמש בשם של מודול aidl_interface , אלא בשם של מודול ספריית קטעים, שהוא ifacename - version - backend , שבו

  • ifacename : שם מודול aidl_interface
  • version היא אחת מהן
    • V version-number עבור הגרסאות הקפואות
    • V latest-frozen-version-number + 1 קצה העץ (טרם הוקפאה)
  • backend הוא אחד מהם
    • java עבור הקצה האחורי של Java,
    • cpp עבור הקצה האחורי של C++,
    • ndk או ndk_platform עבור הקצה האחורי של NDK. הראשון מיועד לאפליקציות, והשני מיועד לשימוש בפלטפורמה,
    • rust לחלק האחורי של חלודה.

נניח שיש מודול עם name 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)

בהשוואה לאנדרואיד 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

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

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

אנדרואיד 10 מוסיף מספר שיטות ממשק מטא עבור ה-AIDL היציב.

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

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

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

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

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

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

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

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