איידל יציב

אנדרואיד 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: ["1", "2"],
    stability: "vintf",
    backend: {
        java: {
            enabled: true,
            platform_apis: true,
        },
        cpp: {
            enabled: true,
        },
        ndk: {
            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 . אם אין גרסאות קפואות של ממשק, אין לציין זאת, ולא יהיו בדיקות תאימות.
  • stability : הדגל האופציונלי להבטחת היציבות של ממשק זה. כרגע תומך רק ב- "vintf" . אם זה לא מוגדר, זה מתאים לממשק עם יציבות בהקשר הקומפילציה הזה (לכן ניתן להשתמש בממשק שנטען כאן רק עם דברים שהידור ביחד, למשל ב-system.img). אם זה מוגדר ל- "vintf" , זה מתאים להבטחת יציבות: יש לשמור על הממשק יציב כל עוד נעשה בו שימוש.
  • gen_trace : הדגל האופציונלי להפעיל או לכבות את המעקב. ברירת המחדל היא false .
  • host_supported : הדגל האופציונלי שכאשר מוגדר כ- true הופך את הספריות שנוצרות לזמינות לסביבת המארח.
  • unstable : הדגל האופציונלי המשמש לסימון שממשק זה אינו צריך להיות יציב. כאשר זה מוגדר true , מערכת ה-build לא יוצרת את ה-API dump עבור הממשק וגם לא דורשת את עדכון זה.
  • backend.<type>.enabled : דגלים אלה מחליפים את כל אחד מהחלקים האחוריים שעבורם מהדר AIDL יפיק קוד. נכון לעכשיו, שלושה חלקים אחוריים נתמכים: java , cpp ו- ndk . הכלים האחוריים מופעלים כברירת מחדל. כאשר אין צורך ב-backend ספציפי, יש להשבית אותו במפורש.
  • 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.

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

כתיבת קבצי 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 . באנדרואיד 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"],
    ...
}

דוגמה ב-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

ממשקי גירסאות

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

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

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

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

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

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

שיטות ממשק

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

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

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

חפצים

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

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

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

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

איגודים

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

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

באנדרואיד 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. הראשון מיועד לאפליקציות, והשני מיועד לשימוש בפלטפורמה.

נניח שיש מודול עם name foo והגרסה האחרונה שלו היא 2 , והוא תומך גם ב-NDK וגם ב-C++. במקרה זה, AIDL מייצר את המודולים הבאים:

  • מבוסס על גרסה 1
    • foo-V1-(java|cpp|ndk|ndk_platform)
  • מבוסס על גרסה 2 (הגרסה היציבה האחרונה)
    • foo-V2-(java|cpp|ndk|ndk_platform)
  • מבוסס על גרסת ToT
    • foo-V3-(java|cpp|ndk|ndk_platform)

בהשוואה לאנדרואיד 11,

  • foo- backend , שהתייחס לגרסה היציבה האחרונה הופך ל- foo- V2 - backend
  • foo-unstable- backend , שהתייחס לגרסת foo- V3 - backend

שמות קבצי הפלט זהים תמיד לשמות המודולים.

  • מבוסס על גרסה 1: foo-V1-(cpp|ndk|ndk_platform).so
  • מבוסס על גרסה 2: foo-V2-(cpp|ndk|ndk_platform).so
  • מבוסס על גרסת ToT: foo-V3-(cpp|ndk|ndk_platform).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() באופן הבא:

class MyFoo extends IFoo.Stub {
    @Override
    public final int getInterfaceVersion() { return IFoo.VERSION; }

    @Override
    public final String getInterfaceHash() { return IFoo.HASH; }
}

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

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

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

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

דוגמה ב-C++ באנדרואיד T (ניסיוני AOSP) ואילך:

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