الخلفيات المستندة إلى لغة تعريف واجهة نظام Android

تمثّل الواجهة الخلفية AIDL هدفًا لإنشاء رمز تجريبي. استخدِم دائمًا ملفات AIDL بلغة معيّنة مع وقت تشغيل محدّد. استنادًا إلى السياق، يجب استخدام برامج خلفية مختلفة للغة AIDL.

في الجدول التالي، يشير ثبات مساحة واجهة برمجة التطبيقات إلى إمكانية تجميع الرمز البرمجي مقابل مساحة واجهة برمجة التطبيقات هذه بطريقة يمكن من خلالها تسليم الرمز البرمجي بشكل مستقل عن ملف system.img libbinder.so الثنائي.

تتضمّن AIDL برامج الخلفية التالية:

الخلفية اللغة مساحة عرض واجهة برمجة التطبيقات أنظمة التصميم
Java Java حزمة تطوير البرامج أو SystemApi (إصدار ثابت*) الكل
NDK C++‎ libbinder_ndk (إصدار ثابت*) aidl_interface
تكلفة المكالمة الهاتفية C++‎ libbinder (غير مستقر) الكل
الصدأ (Rust) الصدأ (Rust) libbinder_rs (إصدار ثابت*) aidl_interface
  • تتسم مساحات واجهات برمجة التطبيقات هذه بالثبات، ولكن العديد من واجهات برمجة التطبيقات، مثل تلك الخاصة بإدارة الخدمات، مخصّصة للاستخدام الداخلي للمنصة ولا تتوفّر للتطبيقات. لمزيد من المعلومات حول كيفية استخدام AIDL في التطبيقات، يُرجى الاطّلاع على لغة تعريف واجهة نظام Android ‏(AIDL).
  • تم طرح الخلفية البرمجية المستندة إلى Rust في الإصدار 12 من نظام التشغيل Android، بينما تتوفّر الخلفية البرمجية المستندة إلى NDK منذ الإصدار 10 من نظام التشغيل Android.
  • تم إنشاء حزمة Rust استنادًا إلى libbinder_ndk، ما يتيح لها أن تكون ثابتة وقابلة للنقل. تستخدم حِزم APEX حزمة binder بالطريقة العادية على مستوى النظام. يتم تجميع جزء Rust في حزمة APEX وشحنه بداخلها. يعتمد هذا الجزء على libbinder_ndk.so في قسم النظام.

إنشاء أنظمة

استنادًا إلى الخلفية، تتوفّر طريقتان لتجميع AIDL في رمز بديل. لمزيد من التفاصيل حول أنظمة التصميم، يُرجى الاطّلاع على مرجع وحدات Soong.

نظام التصميم الأساسي

في أي cc_ أو java_ Android.bp module (أو في Android.mk المكافئات)، يمكنك تحديد ملفات AIDL (.aidl) كملفات مصدر. في هذه الحالة، يتم استخدام الخلفيات المستندة إلى Java أو CPP في AIDL (وليس الخلفية المستندة إلى NDK)، وتتم إضافة الفئات التي تستخدم ملفات AIDL المقابلة إلى الوحدة تلقائيًا. يمكنك تحديد خيارات مثل local_include_dirs (الذي يحدد لنظام التصميم المسار الجذر لملفات AIDL في هذا الوحدة) في هذه الوحدات ضمن مجموعة aidl:.

لا يمكن استخدام الواجهة الخلفية للغة Rust إلا مع Rust. يتم التعامل مع وحدات rust_ بشكل مختلف، إذ لا يتم تحديد ملفات AIDL كملفات مصدر. بدلاً من ذلك، ينتج عن الوحدة aidl_interface rustlib يُسمى aidl_interface_name-rust، ويمكن ربطه. لمعرفة التفاصيل، يُرجى الاطّلاع على مثال Rust AIDL.

aidl_interface

يجب أن تكون الأنواع المستخدَمة مع نظام الإنشاء aidl_interface منظَّمة. لكي تكون العناصر قابلة للتسلسل، يجب أن تحتوي على حقول مباشرةً وألا تكون تعريفات لأنواع محدّدة مباشرةً في اللغات المستهدَفة. لمعرفة كيفية توافق AIDL المنظَّمة مع AIDL الثابتة، يمكنك الاطّلاع على الفرق بين AIDL المنظَّمة وAIDL الثابتة.

الأنواع

يمكنك اعتبار برنامج التحويل البرمجي aidl بمثابة تنفيذ مرجعي للأنواع. عند إنشاء واجهة، استدعِ aidl --lang=<backend> ... للاطّلاع على ملف الواجهة الناتج. عند استخدام الوحدة aidl_interface، يمكنك عرض الناتج في out/soong/.intermediates/<path to module>/.

نوع Java أو AIDL نوع C++‎ نوع NDK نوع الصدأ
boolean bool bool bool
byte8 int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
long int64_t int64_t i64
float float float f32
double double double f64
String android::String16 std::string في: &str
خارج: String
android.os.Parcelable android::Parcelable لا ينطبق لا ينطبق
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder9
T[] std::vector<T> std::vector<T> في: &[T]
خارج: Vec<T>
byte[] std::vector std::vector1 في: &[u8]
خارج: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 في: In: &[T]4
خارج: Vec<T>
FileDescriptor android::base::unique_fd لا ينطبق لا ينطبق
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor9
نوع الواجهة (T) android::sp<T> std::shared_ptr<T>7 binder::Strong<dyn T>9
نوع Parcelable (T) T T T9
نوع الاتحاد (T)5 T T T
T[N]6 std::array<T, N> std::array<T, N> [T; N]

1. في نظام التشغيل Android 12 أو الإصدارات الأحدث، تستخدم مصفوفات البايت uint8_t بدلاً من int8_t لأسباب تتعلّق بالتوافق.

2- تتيح الخلفية المستندة إلى C++‎ استخدام List<T> حيث يكون T أحد الأنواع التالية: String أو IBinder أو ParcelFileDescriptor أو قابل للتجزئة. في الإصدار 13 من نظام التشغيل Android أو الإصدارات الأحدث، يمكن أن يكون نوع T أي نوع غير أساسي (بما في ذلك أنواع الواجهات) باستثناء المصفوفات. تنصح AOSP باستخدام أنواع المصفوفات، مثل T[]، لأنّها تعمل في جميع الأنظمة الخلفية.

3- يتوافق برنامج NDK الخلفي مع List<T> حيث يكون T أحد الخيارات String أو ParcelFileDescriptor أو parcelable. في الإصدار 13 من نظام التشغيل Android أو الإصدارات الأحدث، يمكن أن يكون T أي نوع غير أساسي باستثناء المصفوفات.

4- يتم تمرير الأنواع بشكل مختلف إلى رمز Rust استنادًا إلى ما إذا كانت مدخلاً (وسيطًا) أو مخرجًا (قيمة معروضة).

5. تتوفّر أنواع الاتحاد في الإصدار 12 من نظام التشغيل Android والإصدارات الأحدث.

6. في نظام التشغيل Android 13 أو الإصدارات الأحدث، تتوفّر ميزة الصفائف ذات الحجم الثابت. يمكن أن تحتوي المصفوفات ذات الحجم الثابت على عدة أبعاد (على سبيل المثال، int[3][4]). في الخلفية المستندة إلى Java، يتم تمثيل المصفوفات ذات الحجم الثابت كأنواع مصفوفات.

7. لإنشاء مثيل لكائن SharedRefBase، استخدِم SharedRefBase::make\<My\>(... args ...). تنشئ هذه الدالة العنصر std::shared_ptr\<T\>، الذي تتم إدارته داخليًا أيضًا، في حال كان binder مملوكًا لعملية أخرى. يؤدي إنشاء الكائن بطرق أخرى إلى ملكية مزدوجة.

‫8. راجِع أيضًا نوع Java أو AIDL byte[].

9. في Rust، يتم ربط الأنواع التي لا تنفّذ Default (IBinder وParcelFileDescriptor وأنواع الواجهات والحزم غير المنظَّمة) بالنوع Option<T> عندما تكون هناك حاجة إلى قيمة تلقائية (مثل الحقول القابلة للتسلسل والحقول في الحزم، ومَعلمات out، والمصفوفات ذات الحجم الثابت في هذه السياقات)، حتى بدون التعليق التوضيحي @nullable. سيظل شرط عدم قبول القيم الخالية ساريًا في وقت التشغيل أثناء إنشاء الحِزم، وسيتم عرض StatusCode::UNEXPECTED_NULL إذا كانت القيمة None. بالنسبة إلى قيم إرجاع الطريقة، ومَعلمات in (التي يتم تمريرها كـ &T)، ومَعلمات inout (التي يتم تمريرها كـ &mut T)، لا يتم تضمين هذه الأنواع في Option<T> إلا إذا تمّت إضافة التعليق التوضيحي @nullable إليها.

الاتجاه (in وout وinout)

عند تحديد أنواع وسيطات الدوال، يمكنك تحديدها على أنّها in أو out أو inout. تتحكّم هذه السمة في اتجاه تمرير المعلومات عند إجراء طلب IPC.

  • يشير محدّد وسيطة in إلى أنّه يتم تمرير البيانات من المتصل إلى المتلقي. المحدّد in هو الاتجاه التلقائي، ولكن إذا كان بإمكان أنواع البيانات أن تكون out أيضًا، عليك تحديد الاتجاه.

  • يعني محدّد وسيطة out أنّه يتم تمرير البيانات من الدالة المستدعاة إلى الدالة المستدعِية.

  • محدّد وسيطة inout هو مزيج من كليهما. ومع ذلك، ننصحك بتجنُّب استخدام محدّد الوسيطة inout. إذا كنت تستخدم inout مع واجهة ذات إصدار ومستدعى أقدم، ستتم إعادة ضبط الحقول الإضافية المتوفّرة في المتصل فقط إلى قيمها التلقائية. في ما يتعلق بلغة Rust، يتلقّى النوع العادي inout القيمة &mut T، ويتلقّى النوع inout من القائمة القيمة &mut Vec<T>.

interface IRepeatExamples {
    MyParcelable RepeatParcelable(MyParcelable token); // implicitly 'in'
    MyParcelable RepeatParcelableWithIn(in MyParcelable token);
    void RepeatParcelableWithInAndOut(in MyParcelable param, out MyParcelable result);
    void RepeatParcelableWithInOut(inout MyParcelable param);
}

UTF-8 وUTF-16

باستخدام الخلفية البرمجية للغة C++، يمكنك اختيار ما إذا كانت السلاسل بتنسيق UTF-8 أو UTF-16. يمكنك تعريف السلاسل على أنّها @utf8InCpp String في AIDL لتحويلها تلقائيًا إلى UTF-8. تستخدم الخلفيات البرمجية NDK وRust سلاسل UTF-8 دائمًا. لمزيد من المعلومات حول التعليق التوضيحي utf8InCpp، يُرجى الاطّلاع على utf8InCpp.

إمكانية قبول القيم الفارغة

يمكنك إضافة تعليقات توضيحية إلى الأنواع التي يمكن أن تكون فارغة باستخدام @nullable. لمزيد من المعلومات حول التعليق التوضيحي nullable، يمكنك الاطّلاع على nullable.

Custom parcelables

العنصر القابل للتسلسل المخصّص هو عنصر قابل للتسلسل يتم تنفيذه يدويًا في الخلفية المستهدَفة. لا تستخدِم عناصر Parcelable المخصّصة إلا عندما تحاول إضافة دعم للغات أخرى إلى عنصر Parcelable مخصّص حالي لا يمكن تغييره.

في ما يلي مثال على تعريف AIDL قابل للتسلسل:

    package my.pack.age;
    parcelable Foo;

يؤدي ذلك تلقائيًا إلى تعريف عنصر Java قابل للتسلسل حيث my.pack.age.Foo هو فئة Java تنفّذ الواجهة Parcelable.

لتعريف حزمة قابلة للتجزئة مخصّصة في AIDL، استخدِم cpp_header:

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

يبدو تنفيذ C++ في my/pack/age/Foo.h على النحو التالي:

    #include <binder/Parcelable.h>

    class MyCustomParcelable : public android::Parcelable {
    public:
        status_t writeToParcel(Parcel* parcel) const override;
        status_t readFromParcel(const Parcel* parcel) override;

        std::string toString() const;
        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

لتعريف حزمة NDK مخصّصة يمكن نقلها في AIDL، استخدِم ndk_header:

    package my.pack.age;
    parcelable Foo ndk_header "android/pack/age/Foo.h";

يبدو تنفيذ NDK في android/pack/age/Foo.h على النحو التالي:

    #include <android/binder_parcel.h>

    class MyCustomParcelable {
    public:

        binder_status_t writeToParcel(AParcel* _Nonnull parcel) const;
        binder_status_t readFromParcel(const AParcel* _Nonnull parcel);

        std::string toString() const;

        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

في Android 15، للإعلان عن حزمة مخصّصة قابلة للتسلسل في Rust في AIDL، استخدِم rust_type:

package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";

يبدو تنفيذ Rust في rust_crate/src/lib.rs على النحو التالي:

use binder::{
    binder_impl::{BorrowedParcel, UnstructuredParcelable},
    impl_deserialize_for_unstructured_parcelable, impl_serialize_for_unstructured_parcelable,
    StatusCode,
};

#[derive(Clone, Debug, Eq, PartialEq)]
struct Foo {
    pub bar: String,
}

impl UnstructuredParcelable for Foo {
    fn write_to_parcel(&self, parcel: &mut BorrowedParcel) -> Result<(), StatusCode> {
        parcel.write(&self.bar)?;
        Ok(())
    }

    fn from_parcel(parcel: &BorrowedParcel) -> Result<Self, StatusCode> {
        let bar = parcel.read()?;
        Ok(Self { bar })
    }
}

impl_deserialize_for_unstructured_parcelable!(Foo);
impl_serialize_for_unstructured_parcelable!(Foo);

بعد ذلك، يمكنك استخدام هذا العنصر القابل للتسلسل كنوع في ملفات AIDL، ولكن لن يتم إنشاؤه بواسطة AIDL. توفير عوامل التشغيل < و== لحزم CPP المخصّصة وNDK backend لاستخدامها في union

القيم التلقائية

يمكن أن تحدّد العناصر القابلة للتسلسل قيمًا تلقائية لكل حقل من الحقول الأساسية وحقول String ومصفوفات هذه الأنواع.

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

في الخلفية المستندة إلى Java، عند عدم توفّر القيم التلقائية، يتم ضبط الحقول على قيم صفرية للأنواع الأساسية وnull للأنواع غير الأساسية.

في الأنظمة الخلفية الأخرى، يتمّ ضبط الحقول على القيم التلقائية عند عدم تحديد القيم التلقائية. على سبيل المثال، في الخلفية المستندة إلى C++‎، يتم ضبط قيمة الحقول String على سلسلة فارغة، ويتم ضبط قيمة الحقول List<T> على vector<T> فارغ. يتم ضبط الحقول @nullable على أنّها حقول ذات قيمة فارغة.

اتحادات

يتم وضع علامات على اتحادات AIDL، وتتشابه ميزاتها في جميع الخلفيات. يتم إنشاؤها وفقًا للقيمة التلقائية للحقل الأول، وتتضمّن طريقة خاصة بكل لغة للتفاعل معها:

    union Foo {
      int intField;
      long longField;
      String stringField;
      MyParcelable parcelableField;
      ...
    }

مثال على Java

    Foo u = Foo.intField(42);              // construct

    if (u.getTag() == Foo.intField) {      // tag query
      // use u.getIntField()               // getter
    }

    u.setStringField("abc");               // setter

مثال على C++ وNDK

    Foo u;                                            // default constructor

    assert (u.getTag() == Foo::intField);             // tag query
    assert (u.get<Foo::intField>() == 0);             // getter

    u.set<Foo::stringField>("abc");                   // setter

    assert (u == Foo::make<Foo::stringField>("abc")); // make<tag>(value)

مثال على Rust

في Rust، يتم تنفيذ الاتحادات كأنواع تعدادية ولا تتضمّن دوال جلب وتعيين صريحة.

    let mut u = Foo::Default();              // default constructor
    match u {                                // tag match + get
      Foo::IntField(x) => assert!(x == 0);
      Foo::LongField(x) => panic!("Default constructed to first field");
      Foo::StringField(x) => panic!("Default constructed to first field");
      Foo::ParcelableField(x) => panic!("Default constructed to first field");
      ...
    }
    u = Foo::StringField("abc".to_string()); // set

معالجة الأخطاء

يوفّر نظام التشغيل Android أنواع أخطاء مدمجة يمكن أن تستخدمها الخدمات عند الإبلاغ عن الأخطاء. تستخدمها أدوات الربط ويمكن لأي خدمات تنفّذ واجهة أداة ربط استخدامها. ويتم توثيق استخدامها بشكل جيد في تعريف AIDL، ولا تتطلّب أي حالة أو نوع القيمة التي تم إرجاعها يحدّده المستخدم.

مَعلمات الإخراج التي تتضمّن أخطاء

عندما تعرض إحدى دوال AIDL خطأً، قد لا يتم تهيئة الدالة أو تعديل مَعلمات الإخراج. على وجه التحديد، قد يتم تعديل مَعلمات الإخراج إذا حدث الخطأ أثناء عملية إلغاء التجميع، وليس أثناء معالجة العملية نفسها. بشكل عام، عند تلقّي خطأ من دالة AIDL، يجب اعتبار جميع المَعلمات inout وout بالإضافة إلى القيمة المعروضة (التي تعمل كمَعلمة out في بعض الأنظمة الخلفية) في حالة غير محدّدة.

قيم الخطأ المطلوب استخدامها

يمكن استخدام العديد من قيم الخطأ المضمّنة في أي واجهات AIDL، ولكن يتم التعامل مع بعضها بطريقة خاصة. على سبيل المثال، يمكن استخدام EX_UNSUPPORTED_OPERATION وEX_ILLEGAL_ARGUMENT عند وصف حالة الخطأ، ولكن يجب عدم استخدام EX_TRANSACTION_FAILED لأنّ البنية الأساسية تتعامل معه بشكل خاص. يمكنك الاطّلاع على التعريفات الخاصة بالخادم الخلفي للحصول على مزيد من المعلومات حول هذه القيم المضمّنة.

إذا كانت واجهة AIDL تتطلّب قيم خطأ إضافية لا تغطّيها أنواع الأخطاء المضمّنة، يمكنها استخدام الخطأ المضمّن الخاص بالخدمة والذي يتيح تضمين قيمة خطأ خاصة بالخدمة يحدّدها المستخدم. يتم عادةً تحديد هذه الأخطاء الخاصة بالخدمة في واجهة AIDL كـ const int أو int-backed enum، ولا يتم تحليلها بواسطة binder.

في Java، يتم ربط الأخطاء بالاستثناءات، مثل android.os.RemoteException. بالنسبة إلى الاستثناءات الخاصة بالخدمة، تستخدم Java android.os.ServiceSpecificException بالإضافة إلى الخطأ الذي يحدّده المستخدم.

لا يستخدم الرمز البرمجي الأصلي في Android الاستثناءات. تستخدم الواجهة الخلفية لـ CPP android::binder::Status. تستخدم الواجهة الخلفية لـ NDK ndk::ScopedAStatus. تعرض كل طريقة تم إنشاؤها بواسطة AIDL إحدى هذه القيم التي تمثّل حالة الطريقة. يستخدم خادم Rust الخلفي قيم رموز الاستثناء نفسها المستخدَمة في NDK، ولكنّه يحوّلها إلى أخطاء Rust أصلية (StatusCode وExceptionCode) قبل تسليمها إلى المستخدم. بالنسبة إلى الأخطاء الخاصة بالخدمة، تستخدم القيمتان المعروضتان Status أو ScopedAStatus EX_SERVICE_SPECIFIC بالإضافة إلى الخطأ الذي يحدّده المستخدم.

يمكن العثور على أنواع الأخطاء المضمّنة في الملفات التالية:

الخلفية التعريف
Java android/os/Parcel.java
تكلفة المكالمة الهاتفية binder/Status.h
NDK android/binder_status.h
الصدأ (Rust) android/binder_status.h

استخدام أنظمة خلفية مختلفة

هذه التعليمات خاصة برمز نظام Android الأساسي. تستخدم هذه الأمثلة نوعًا محددًا، وهو my.package.IFoo. للحصول على تعليمات حول كيفية استخدام الخلفية المستندة إلى Rust، راجِع مثال AIDL المستند إلى Rust في أنماط Android Rust.

أنواع عمليات الاستيراد

سواء كان النوع المحدّد واجهة أو قابلاً للتسلسل أو اتحادًا، يمكنك استيراده في Java على النحو التالي:

import my.package.IFoo;

أو في الخلفية الخاصة ببرنامج CPP:

#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 (أو <aidl/my/package/IFoo.h> في الخلفية المستندة إلى NDK).

تنفيذ واجهة

لتنفيذ واجهة، يجب أن ترث من فئة العنصر النائب الأصلية. يُطلق على تنفيذ واجهة اسم خدمة غالبًا عند تسجيلها لدى مدير الخدمة أو android.app.ActivityManager، ويُطلق عليها اسم دالة رد الاتصال عند تسجيلها من قِبل عميل إحدى الخدمات. ومع ذلك، يتم استخدام مجموعة متنوعة من الأسماء لوصف عمليات تنفيذ الواجهة، وذلك حسب الاستخدام الدقيق. تقرأ فئة العنصر النائب الأوامر من برنامج تشغيل الرابط وتنفّذ الطرق التي تنفّذها. لنفترض أنّ لديك ملف AIDL على النحو التالي:

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

في Java، يجب أن توسّع نطاق الفئة Stub التي تم إنشاؤها:

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

في خلفية CPP:

    #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;
    }

في الخلفية المستندة إلى Rust:

    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(())
        }
    }

أو باستخدام Rust غير المتزامن:

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

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

    impl Interface for MyFoo {}

    #[async_trait]
    impl IFooAsyncServer for MyFoo {
        async fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

التسجيل والحصول على الخدمات

عادةً ما يتم تسجيل الخدمات في نظام Android الأساسي باستخدام عملية servicemanager. بالإضافة إلى واجهات برمجة التطبيقات التالية، تتحقّق بعض واجهات برمجة التطبيقات من توفّر الخدمة (أي أنّها تعرض النتائج على الفور إذا كانت الخدمة غير متاحة). راجِع واجهة servicemanager المقابلة للحصول على التفاصيل الدقيقة. لا يمكنك تنفيذ هذه العمليات إلا عند تجميع التطبيق على نظام Android الأساسي.

في Java:

    import android.os.ServiceManager;
    // registering
    ServiceManager.addService("service-name", myService);
    // return if service is started now
    myService = IFoo.Stub.asInterface(ServiceManager.checkService("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:

    #include <binder/IServiceManager.h>
    // registering
    defaultServiceManager()->addService(String16("service-name"), myService);
    // return if service is started now
    status_t err = checkService<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
    binder_exception_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // return if service is started now
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_checkService("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(ndk::SpAIBinder(AServiceManager_waitForService("service-name")));

في الخلفية المستندة إلى Rust:

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()
}

في الخلفية غير المتزامنة للغة Rust، مع وقت تشغيل ذي سلسلة واحدة:

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

#[tokio::main(flavor = "current_thread")]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleeps forever, but does not join the binder threadpool.
    // Spawned tasks run on this thread.
    std::future::pending().await
}

يتمثل أحد الاختلافات المهمة عن الخيارات الأخرى في أنّك لا تستدعي join_thread_pool عند استخدام Rust غير المتزامن ووقت تشغيل ذي سلسلة محادثات واحدة. والسبب في ذلك هو أنّك تحتاج إلى توفير سلسلة محادثات لـ Tokio يمكنها تنفيذ المهام التي تم إنشاؤها. في المثال التالي، يؤدي مؤشر الترابط الرئيسي هذا الغرض. يتم تنفيذ أي مهام تم إنشاؤها باستخدام tokio::spawn في سلسلة المحادثات الرئيسية.

في الخلفية غير المتزامنة للغة Rust، مع وقت تشغيل متعدد الخيوط:

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

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleep forever.
    tokio::task::block_in_place(|| {
        binder::ProcessState::join_thread_pool();
    });
}

باستخدام وقت تشغيل Tokio المتعدد مؤشرات الترابط، لا يتم تنفيذ المهام التي تم إنشاؤها على مؤشر الترابط الرئيسي. لذلك، من المنطقي استدعاء join_thread_pool في سلسلة التعليمات الرئيسية حتى لا تكون سلسلة التعليمات الرئيسية غير نشطة. يجب تضمين الاستدعاء في block_in_place لمغادرة سياق غير متزامن.

يمكنك طلب تلقّي إشعار عند توقّف خدمة تستضيف رابطًا. يمكن أن يساعد ذلك في تجنُّب تسريب وكلاء معاودة الاتصال أو المساعدة في استرداد الأخطاء. يجب إجراء هذه الطلبات على عناصر وكيل الرابط.

  • في Java، استخدِم android.os.IBinder::linkToDeath.
  • في الخلفية الخاصة بمنصة CPP، استخدِم android::IBinder::linkToDeath.
  • في الخلفية الأصلية (NDK)، استخدِم AIBinder_linkToDeath. استخدِم دائمًا AIBinder_DeathRecipient_setOnUnlinked للتحكّم في مدة صلاحية ملف تعريف الارتباط الخاص بمستلِم إشعار الإيقاف.
  • في الخلفية المستندة إلى Rust، أنشئ عنصر DeathRecipient، ثم استدعِ my_binder.link_to_death(&mut my_death_recipient). يُرجى العِلم أنّه بما أنّ DeathRecipient يملك دالة معاودة الاتصال، عليك إبقاء هذا العنصر نشطًا طالما أردت تلقّي الإشعارات.

معلومات المتصل

عند تلقّي مكالمة kernel binder، تتوفّر معلومات المتّصل في عدة واجهات برمجة تطبيقات. يشير معرّف العملية (PID) إلى معرّف عملية Linux التي ترسل معاملة. يشير رقم تعريف المستخدم (UI) إلى رقم تعريف مستخدم Linux. عند تلقّي مكالمة أحادية الاتجاه، يكون معرّف العملية (PID) للمتصل هو 0. خارج سياق معاملة الرابط، تعرض هذه الدوال رقم تعريف العملية (PID) ومعرّف المستخدم (UID) للعملية الحالية.

في الخلفية المستندة إلى Java:

    ... = Binder.getCallingPid();
    ... = Binder.getCallingUid();

في خلفية CPP:

    ... = IPCThreadState::self()->getCallingPid();
    ... = IPCThreadState::self()->getCallingUid();

في الخلفية الخاصة بـ NDK:

    ... = AIBinder_getCallingPid();
    ... = AIBinder_getCallingUid();

في الخلفية المستندة إلى Rust، عند تنفيذ الواجهة، حدِّد ما يلي (بدلاً من السماح باستخدام القيمة التلقائية):

    ... = ThreadState::get_calling_pid();
    ... = ThreadState::get_calling_uid();

تقارير الأخطاء وواجهة برمجة التطبيقات لتصحيح الأخطاء في الخدمات

عند تشغيل تقارير الأخطاء (على سبيل المثال، باستخدام adb bugreport)، يتم جمع معلومات من جميع أنحاء النظام للمساعدة في تصحيح الأخطاء المختلفة. بالنسبة إلى خدمات AIDL، تستخدم تقارير الأخطاء الملف الثنائي dumpsys في جميع الخدمات المسجّلة لدى مدير الخدمات لتفريغ معلوماتها في تقرير الأخطاء. يمكنك أيضًا استخدام dumpsys في سطر الأوامر للحصول على معلومات من خدمة باستخدام dumpsys SERVICE [ARGS]. في الخلفيتَين C++ وJava، يمكنك التحكّم في ترتيب عرض الخدمات باستخدام وسيطات إضافية للأمر addService. يمكنك أيضًا استخدام dumpsys --pid SERVICE للحصول على معرّف العملية (PID) الخاص بإحدى الخدمات أثناء تصحيح الأخطاء.

لإضافة ناتج مخصّص إلى خدمتك، عليك إلغاء طريقة dump في عنصر الخادم كما تفعل عند تنفيذ أي طريقة أخرى من طرق الاتصال بين العمليات (IPC) محدّدة في ملف AIDL. عند إجراء ذلك، يجب حصر عملية التفريغ على إذن التطبيق android.permission.DUMP أو حصرها على معرّفات UID معيّنة.

في الخلفية المستندة إلى Java:

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

في خلفية CPP:

    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، عند تنفيذ الواجهة، حدِّد ما يلي (بدلاً من السماح باستخدام القيمة التلقائية):

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

استخدام المؤشرات الضعيفة

يمكنك الاحتفاظ بمرجع ضعيف إلى عنصر رابط.

على الرغم من أنّ Java تتيح استخدام WeakReference، إلا أنّها لا تتيح استخدام مراجع الرابط الضعيف في الطبقة الأصلية.

في الخلفية البرمجية لـ CPP، يكون النوع الضعيف هو wp<IFoo>.

في الخلفية الخاصة بـ NDK، استخدِم ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

AIBinder* binder = ...;
ScopedAIBinder_Weak myWeakReference = ScopedAIBinder_Weak(AIBinder_Weak_new(binder));

في الخلفية المستندة إلى Rust، استخدِم WpIBinder أو Weak<IFoo>:

let weak_interface = myIface.downgrade();
let weak_binder = myIface.as_binder().downgrade();

الحصول على واصف الواجهة بشكل ديناميكي

يحدّد واصف الواجهة نوع الواجهة. ويكون ذلك مفيدًا عند تصحيح الأخطاء أو عند توفّر رابط غير معروف.

في Java، يمكنك الحصول على واصف الواجهة باستخدام رمز مثل:

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

في خلفية CPP:

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

لا تتوافق الخلفيتان البرمجيتان NDK وRust مع هذه الإمكانية.

الحصول على واصف الواجهة بشكل ثابت

في بعض الأحيان (مثل عند تسجيل خدمات @VintfStability)، يجب أن تعرف وصف الواجهة بشكل ثابت. في Java، يمكنك الحصول على الواصف من خلال إضافة رمز مثل:

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

في خلفية CPP:

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

في الخلفية الأصلية (NDK) (لاحظ مساحة الاسم الإضافية aidl):

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

في الخلفية المستندة إلى Rust:

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

نطاق التعداد

في الأنظمة الخلفية الأصلية، يمكنك تكرار القيم المحتملة التي يمكن أن تتخذها قيمة التعداد. لا تتوفّر هذه الميزة في Java بسبب اعتبارات حجم الرمز.

بالنسبة إلى النوع enum MyEnum المحدّد في AIDL، يتم توفير التكرار على النحو التالي.

في خلفية CPP:

    ::android::enum_range<MyEnum>()

في الخلفية الخاصة بـ NDK:

   ::ndk::enum_range<MyEnum>()

في الخلفية المستندة إلى Rust:

    MyEnum::enum_values()

إدارة سلاسل المحادثات

يحتفظ كل مثيل من libbinder في إحدى العمليات بمجموعة سلاسل محادثات واحدة. في معظم حالات الاستخدام، يجب أن يكون هذا العدد هو مجمع سلاسل محادثات واحد، تتم مشاركته بين جميع الأنظمة الخلفية. الاستثناء الوحيد هو إذا كان رمز المورّد يحمّل نسخة أخرى من libbinder للتواصل مع /dev/vndbinder. يتم ذلك على عقدة رابط منفصلة، وبالتالي لا تتم مشاركة مجموعة سلاسل التنفيذ.

بالنسبة إلى الخلفية المستندة إلى Java، يمكن زيادة حجم مجموعة سلاسل التنفيذ فقط (لأنّها بدأت من قبل):

    BinderInternal.setMaxThreads(<new larger value>);

بالنسبة إلى الخلفية البرمجية لمنصة CPP، تتوفّر العمليات التالية:

    // 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();

في الخلفية المستندة إلى Rust:

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

مع الخلفية غير المتزامنة في Rust، تحتاج إلى مجموعتَي سلاسل التعليمات: binder وTokio. وهذا يعني أنّ التطبيقات التي تستخدم Rust غير المتزامن تحتاج إلى مراعاة بعض الاعتبارات الخاصة، خاصةً عندما يتعلق الأمر باستخدام join_thread_pool. يمكنك الاطّلاع على القسم الخاص بتسجيل الخدمات للحصول على مزيد من المعلومات حول هذا الموضوع.

الأسماء المحجوزة

تحتفظ لغات C++‎ وJava وRust ببعض الأسماء ككلمات رئيسية أو للاستخدام الخاص باللغة. على الرغم من أنّ AIDL لا يفرض قيودًا استنادًا إلى قواعد اللغة، إلا أنّ استخدام أسماء حقول أو أنواع تتطابق مع اسم محجوز يمكن أن يؤدي إلى تعذُّر التجميع في C++‎ أو Java. في Rust، تتم إعادة تسمية الحقل أو النوع باستخدام بنية المعرّف الأولي، ويمكن الوصول إليه باستخدام البادئة r#.

ننصحك بتجنُّب استخدام الأسماء المحجوزة في تعريفات AIDL حيثما أمكن ذلك لتجنُّب عمليات الربط غير المريحة أو تعذُّر التجميع بشكل كامل.

إذا كانت لديك أسماء محجوزة في تعريفات AIDL، يمكنك بأمان إعادة تسمية الحقول مع الحفاظ على توافق البروتوكول. قد تحتاج إلى تعديل الرمز البرمجي لمواصلة الإنشاء، ولكن ستستمر أي برامج تم إنشاؤها في العمل بشكل متوافق.

الأسماء التي يجب تجنُّبها: