AIDL אחורי

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

ל-AIDL יש את החלקים האחוריים הבאים:

אחורי שפה משטח API בניית מערכות
Java Java SDK/SystemApi (יציב*) את כל
NDK C++ libbinder_ndk (יציב*) aidl_interface
CPP C++ libbinder (לא יציב) את כל
חֲלוּדָה חֲלוּדָה libbinder_rs (לא יציב) aidl_interface
  • משטחי API אלה יציבים, אך רבים ממשקי ה-API, כגון אלו לניהול שירות, שמורים לשימוש פנימי בפלטפורמה ואינם זמינים לאפליקציות. למידע נוסף על אופן השימוש ב-AIDL באפליקציות, עיין בתיעוד למפתחים .
  • החלק האחורי של Rust הוצג באנדרואיד 12; הקצה האחורי של NDK היה זמין החל מ-Android 10.
  • ארגז Rust בנוי על גבי libbinder_ndk . APEXs משתמשים בארגז הקלסר באותו אופן כמו כל אחד אחר בצד המערכת. חלק החלודה נצרף לתוך APEX ונשלח בתוכו. זה תלוי ב- libbinder_ndk.so במחיצת המערכת.

בניית מערכות

בהתאם לקצה העורפי, ישנן שתי דרכים להדר AIDL לקוד stub. לפרטים נוספים על מערכות הבנייה, עיין ב- Soong Module Reference .

מערכת בניית ליבה

בכל מודול cc_ או java_ Android.bp (או המקבילות שלהם ל- Android.mk ), ניתן לציין קבצי .aidl כקובצי מקור. במקרה זה, נעשה שימוש ב-Java/CPP backends של AIDL (לא ב-NDK backend), והמחלקות לשימוש בקבצי AIDL המתאימים מתווספות למודול באופן אוטומטי. אפשרויות כגון local_include_dirs , שאומר למערכת ה-build את נתיב הבסיס לקבצי AIDL באותו מודול ניתן לציין במודולים אלו תחת aidl: group. שימו לב שהחלק האחורי של Rust מיועד רק לשימוש עם Rust. מודולי rust_ מטופלים בצורה שונה בכך שקובצי AIDL אינם מצוינים כקובצי מקור. במקום זאת, מודול aidl_interface מייצר rustlib בשם <aidl_interface name>-rust שניתן לקשר אליו. לפרטים נוספים, עיין בדוגמה של Rust AIDL .

aidl_interface

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

סוגים

אתה יכול לשקול את מהדר aidl כיישום ייחוס עבור טיפוסים. כאשר אתה יוצר ממשק, aidl --lang=<backend> ... כדי לראות את קובץ הממשק שנוצר. כאשר אתה משתמש במודול aidl_interface , אתה יכול להציג את הפלט ב- out/soong/.intermediates/<path to module>/ .

סוג Java/AIDL סוג C++ סוג NDK סוג חלודה
בוליאני bool bool bool
בייט int8_t int8_t i8
לְהַשְׁחִיר char16_t char16_t u16
int int32_t int32_t i32
ארוך int64_t int64_t i64
לָצוּף לָצוּף לָצוּף f32
לְהַכפִּיל לְהַכפִּיל לְהַכפִּיל f64
חוּט android::String16 std::string חוּט
android.os.Parcelable android::ניתן לחלק לא לא
IBinder אנדרואיד::IBinder ndk::SpAIBinder קלסר::SpIBinder
T[] std::vector<T> std::vector<T> ב: &T
בחוץ: Vec<T>
בייט[] std::vector<uint8_t> std::vector<int8_t> 1 ב: &[u8]
בחוץ: Vec<u8>
רשימה<T> std::vector<T> 2 std::vector<T> 3 ב: &[T] 4
בחוץ: Vec<T>
FileDescriptor android::base::unique_fd לא קלסר::parcel::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor קלסר::parcel::ParcelFileDescriptor
סוג ממשק (T) אנדרואיד::sp<T> std::shared_ptr<T> קלסר::חזק
סוג ניתן לחלוקה (T) ט ט ט
סוג איגוד (T) 5 ט ט ט
T[N] 6 std::array<T, N> std::array<T, N> [ת; נ]

1. באנדרואיד 12 ומעלה, מערכי בתים משתמשים ב-uint8_t במקום ב-int8_t מטעמי תאימות.

2. הקצה האחורי של C++ תומך ב- List<T> כאשר T הוא אחד מ- String , IBinder , ParcelFileDescriptor או parcelable. ב-Android T (ניסיוני AOSP) ומעלה, T יכול להיות כל סוג לא פרימיטיבי (כולל סוגי ממשקים) למעט מערכים. AOSP ממליצה להשתמש בסוגי מערכים כמו T[] , מכיוון שהם עובדים בכל הקצה האחורי.

3. הקצה האחורי של NDK תומך ב- List<T> כאשר T הוא אחד מ- String , ParcelFileDescriptor או parcelable. ב-Android T (AOSP ניסיוני) ומעלה, T יכול להיות כל סוג לא פרימיטיבי למעט מערכים.

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

5. סוגי איגוד נתמכים באנדרואיד 12 ומעלה.

6. באנדרואיד T (ניסיוני AOSP) ומעלה, מערכים בגודל קבוע נתמכים. למערכים בגודל קבוע יכולים להיות מימדים מרובים (למשל int[3][4] ). ב-Java backend, מערכים בגודל קבוע מיוצגים כסוגי מערכים.

כיווניות (פנימה/ החוצה/פנימה)

כאשר מציינים את סוגי הארגומנטים לפונקציות, ניתן לציין אותם כמו in , out או inout . זה שולט באיזה כיוון מועבר מידע עבור שיחת IPC. in הוא כיוון ברירת המחדל, והוא מציין שהנתונים מועברים מהמתקשר אל המוקדן. out פירושו שהנתונים מועברים מהמתקשר אל המתקשר. inout הוא השילוב של שניהם. עם זאת, צוות אנדרואיד ממליץ להימנע משימוש במפרט הארגומנטים inout . אם אתה משתמש ב- inout עם ממשק גרסה ועם מתקשר ישן יותר, השדות הנוספים שקיימים רק אצל המתקשר יאופסו לערכי ברירת המחדל שלהם. לגבי Rust, סוג inout רגיל מקבל &mut Vec<T> , וסוג inout רשימה מקבל &mut Vec<T> .

UTF8/UTF16

עם ה-CPP backend אתה יכול לבחור אם מחרוזות הן utf-8 או utf-16. הכריז על @utf8InCpp String ב-AIDL כדי להמיר אותן אוטומטית ל-utf-8. הקצה האחורי של NDK ו-Rust תמיד משתמש במחרוזות utf-8. למידע נוסף על ההערה utf8InCpp , ראה הערות ב-AIDL .

בטלות

אתה יכול להוסיף הערות לסוגים שיכולים להיות null בקצה העורפי של Java עם @nullable כדי לחשוף ערכי null ל-CPP ו-NDK. ב-Rust backend סוגי @nullable אלה נחשפים כאפשרות Option<T> . שרתים מקוריים דוחים ערכי null כברירת מחדל. החריגים היחידים לכך הם סוגי interface ו- IBinder , שיכולים תמיד להיות null עבור קריאות NDK וכתיבת CPP/NDK. למידע נוסף על ההערה הניתנת לאפס, ראה הערות ב-AIDL nullable

פריטים מותאמים אישית

ב-C++ ו-Java backends במערכת ה-Byon הליבה, אתה יכול להכריז על parcelable שמיושם באופן ידני ב-backend של יעד (ב-C++ או ב-Java).

    package my.package;
    parcelable Foo;

או עם הצהרת כותרת C++:

    package my.package;
    parcelable Foo cpp_header "my/package/Foo.h";

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

חלודה לא תומכת בחבילות מותאמות אישית.

ערכי ברירת מחדל

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

    parcelable Foo {
      int numField = 42;
      String stringField = "string value";
      char charValue = 'a';
      ...
    }

ב-Java backend כאשר חסרים ערכי ברירת מחדל, שדות מאותחלים כערכים אפס עבור טיפוסים פרימיטיביים ו- null עבור טיפוסים לא פרימיטיביים.

ב-backends אחרים, שדות מאותחלים עם ערכי ברירת מחדל אתחולים כאשר ערכי ברירת מחדל אינם מוגדרים. לדוגמה, ב-C++ backend, שדות String מאותחלים כמחרוזת ריקה ושדות List<T> מאותחלים vector<T> . שדות @nullable כשדות null-value.

טיפול בשגיאות

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

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

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

ב-Java, השגיאות ממפות לחריגים, כגון android.os.RemoteException . עבור חריגים ספציפיים לשירות, Java משתמשת ב- android.os.ServiceSpecificException יחד עם השגיאה המוגדרת על ידי המשתמש.

קוד מקורי באנדרואיד אינו משתמש בחריגים. הקצה העורפי של CPP משתמש ב- android::binder::Status . הקצה האחורי של NDK משתמש ב- ndk::ScopedAStatus . כל שיטה שנוצרת על ידי AIDL מחזירה אחת מאלה, המייצגת את מצב השיטה. הקצה האחורי של Rust משתמש באותם ערכי קוד חריג כמו ה-NDK, אך ממיר אותם לשגיאות Rust מקוריות ( StatusCode , ExceptionCode ) לפני מסירתם למשתמש. עבור שגיאות ספציפיות לשירות, Status או ScopedAStatus שהוחזר משתמש ב- EX_SERVICE_SPECIFIC יחד עם השגיאה המוגדרת על ידי המשתמש.

ניתן למצוא את סוגי השגיאות המובנים בקבצים הבאים:

אחורי הַגדָרָה
Java android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
חֲלוּדָה android/binder_status.h

שימוש ב-backends שונים

הוראות אלה הן ספציפיות לקוד פלטפורמת אנדרואיד. דוגמאות אלה משתמשות בסוג מוגדר, my.package.IFoo . לקבלת הוראות כיצד להשתמש ב-Rust Backend, עיין בדוגמה של Rust AIDL בדף של Android Rust Patterns .

ייבוא ​​סוגי

בין אם הסוג המוגדר הוא ממשק, ניתן לחלוקה או איחוד, אתה יכול לייבא אותו ב-Java:

    import my.package.IFoo;

או ב-CPP backend:

    #include <my/package/IFoo.h>

או בקצה האחורי של NDK (שימו לב למרחב השמות הנוסף של aidl ):

    #include <aidl/my/package/IFoo.h>

או בחלק האחורי של Rust:

    use my_package::aidl::my::package::IFoo;

למרות שאתה יכול לייבא סוג מקונן ב-Java, בקצה האחורי של CPP/NDK עליך לכלול את הכותרת עבור סוג השורש שלו. לדוגמה, בעת ייבוא Bar מסוג מקונן המוגדר ב- my/package/IFoo.aidl ( IFoo הוא סוג השורש של הקובץ) עליך לכלול <my/package/IFoo.h> עבור ה-CPP backend (או <aidl/my/package/IFoo.h> עבור הקצה האחורי של NDK).

הטמעת שירותים

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

    package my.package;
    interface IFoo {
        int doFoo();
    }

ב-Java, עליך להרחיב מהמחלקה הזו:

    import my.package.IFoo;
    public class MyFoo extends IFoo.Stub {
        @Override
        int doFoo() { ... }
    }

ב-CPP backend:

    #include <my/package/BnFoo.h>
    class MyFoo : public my::package::BnFoo {
        android::binder::Status doFoo(int32_t* out) override;
    }

בקצה האחורי של NDK (שים לב למרחב השמות הנוסף של aidl ):

    #include <aidl/my/package/BnFoo.h>
    class MyFoo : public aidl::my::package::BnFoo {
        ndk::ScopedAStatus doFoo(int32_t* out) override;
    }

בגב החלודה:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFoo};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    impl IFoo for MyFoo {
        fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

רישום וקבלת שירותים

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

ב-Java:

    import android.os.ServiceManager;
    // registering
    ServiceManager.addService("service-name", myService);
    // getting
    myService = IFoo.Stub.asInterface(ServiceManager.getService("service-name"));
    // waiting until service comes up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForService("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForDeclaredService("service-name"));

ב-CPP backend:

    #include <binder/IServiceManager.h>
    // registering
    defaultServiceManager()->addService(String16("service-name"), myService);
    // getting
    status_t err = getService<IFoo>(String16("service-name"), &myService);
    // waiting until service comes up (new in Android 11)
    myService = waitForService<IFoo>(String16("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = waitForDeclaredService<IFoo>(String16("service-name"));

בקצה האחורי של NDK (שים לב למרחב השמות הנוסף של aidl ):

    #include <android/binder_manager.h>
    // registering
    status_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // getting
    myService = IFoo::fromBinder(SpAIBinder(AServiceManager_getService("service-name")));
    // is a service declared in the VINTF manifest
    // VINTF services have the type in the interface instance name.
    bool isDeclared = AServiceManager_isDeclared("android.hardware.light.ILights/default");
    // wait until a service is available (if isDeclared or you know it's available)
    myService = IFoo::fromBinder(SpAIBinder(AServiceManager_waitForService("service-name")));

בגב החלודה:

use myfoo::MyFoo;
use binder;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_binder(
        my_service,
        BinderFeatures::default(),
    );
    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
    // Does not return - spawn or perform any work you mean to do before this call.
    binder::ProcessState::join_thread_pool()
}

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

  • ב-Java, השתמש ב- android.os.IBinder::linkToDeath .
  • בקצה העורפי של CPP, השתמש android::IBinder::linkToDeath .
  • בקצה העורפי של NDK, השתמש ב- AIBinder_linkToDeath .
  • בקצה העורפי של Rust, צור אובייקט DeathRecipient , ואז קרא my_binder.link_to_death(&mut my_death_recipient) . שים לב שמכיוון DeathRecipient יש את ההתקשרות חזרה, עליך לשמור על האובייקט הזה בחיים כל עוד אתה רוצה לקבל התראות.

דוחות באגים וממשק API לניפוי באגים לשירותים

כאשר דיווחי באגים פועלים (לדוגמה, עם adb bugreport ), הם אוספים מידע מכל רחבי המערכת כדי לסייע באיתור בעיות שונות. עבור שירותי AIDL, דיווחי באגים משתמשים ב- dumpsys הבינארי בכל השירותים הרשומים במנהל השירות כדי לזרוק את המידע שלהם לדוח הבאג. אתה יכול גם להשתמש ב- dumpsys הפקודה כדי לקבל מידע משירות עם dumpsys SERVICE [ARGS] . ב-C++ ו-Java backends, אתה יכול לשלוט בסדר שבו השירותים יוזרקו על ידי שימוש בארגומנטים נוספים כדי addService . אתה יכול גם להשתמש dumpsys --pid SERVICE כדי לקבל את ה-PID של שירות בזמן ניפוי באגים.

כדי להוסיף פלט מותאם אישית לשירות שלך, אתה יכול לעקוף את שיטת ה- dump באובייקט השרת שלך כמו שאתה מיישם כל שיטת IPC אחרת המוגדרת בקובץ AIDL. כשאתה עושה זאת, עליך להגביל את ההשלכה להרשאת האפליקציה android.permission.DUMP או להגביל את ההשלכה למזהי UID ספציפיים.

ב-Java backend:

    @Override
    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
        @Nullable String[] args) {...}

ב-CPP backend:

    status_t dump(int, const android::android::Vector<android::String16>&) override;

בקצה האחורי של NDK:

    binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;

ב-Rust backend, בעת הטמעת הממשק, ציין את הדברים הבאים (במקום לאפשר לו כברירת מחדל):

    fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>

מקבל באופן דינמי מתאר ממשק

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

ב-Java, אתה יכול לקבל את מתאר הממשק עם קוד כגון:

    service = /* get ahold of service object */
    ... = service.asBinder().getInterfaceDescriptor();

ב-CPP backend:

    service = /* get ahold of service object */
    ... = IInterface::asBinder(service)->getInterfaceDescriptor();

הקצה האחורי של NDK ו-Rust אינם תומכים בפונקציונליות זו.

מקבל באופן סטטי מתאר ממשק

לפעמים (כגון בעת ​​רישום שירותי @VintfStability ), אתה צריך לדעת מהו מתאר הממשק באופן סטטי. ב-Java, אתה יכול לקבל את המתאר על ידי הוספת קוד כגון:

    import my.package.IFoo;
    ... IFoo.DESCRIPTOR

ב-CPP backend:

    #include <my/package/BnFoo.h>
    ... my::package::BnFoo::descriptor

בקצה האחורי של NDK (שים לב למרחב השמות הנוסף של aidl ):

    #include <aidl/my/package/BnFoo.h>
    ... aidl::my::package::BnFoo::descriptor

בגב החלודה:

    aidl::my::package::BnFoo::get_descriptor()

Enum Range

ב-backends מקוריים, אתה יכול לחזור על הערכים האפשריים ש-enum יכול לקחת על עצמו. בגלל שיקולי גודל קוד, זה לא נתמך ב-Java כרגע.

עבור enum MyEnum המוגדר ב-AIDL, האיטרציה מסופקת כדלקמן.

ב-CPP backend:

    ::android::enum_range<MyEnum>()

בקצה האחורי של NDK:

   ::ndk::enum_range<MyEnum>()

בגב החלודה:

    MyEnum::enum_range()

ניהול שרשור

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

עבור ה-Java backend, מאגר ה-Threadpool יכול רק להגדיל את גודלו (מכיוון שהוא כבר התחיל):

    BinderInternal.setMaxThreads(<new larger value>);

עבור ה-CPP backend, הפעולות הבאות זמינות:

    // set max threadpool count (default is 15)
    status_t err = ProcessState::self()->setThreadPoolMaxThreadCount(numThreads);
    // create threadpool
    ProcessState::self()->startThreadPool();
    // add current thread to threadpool (adds thread to max thread count)
    IPCThreadState::self()->joinThreadPool();

באופן דומה, בקצה האחורי של NDK:

    bool success = ABinderProcess_setThreadPoolMaxThreadCount(numThreads);
    ABinderProcess_startThreadPool();
    ABinderProcess_joinThreadPool();

בגב החלודה:

    binder::ProcessState::start_thread_pool();
    binder::add_service(“myservice”, my_service_binder).expect(“Failed to register service?”);
    binder::ProcessState::join_thread_pool();