تُعد أفضل الممارسات الموضّحة هنا بمثابة دليل لتطوير واجهات AIDL بفعالية مع مراعاة مرونة الواجهة، لا سيما عند استخدام AIDL لتحديد واجهة برمجة تطبيقات مستقرة ومتوافقة مع الإصدارات السابقة.
يمكن استخدام AIDL لتحديد واجهة برمجة تطبيقات عندما تحتاج التطبيقات إلى التفاعل مع بعضها البعض في عملية في الخلفية أو تحتاج إلى التفاعل مع النظام.
يتم استخدام AIDL المستقر مع @VintfStability لواجهات HAL، ويسمح بتحديث العملاء والخوادم بشكلٍ مستقل. ويتطلّب ذلك التوافق مع الإصدارات السابقة والبيانات المنظَّمة.
لمزيد من المعلومات حول تطوير واجهات برمجة التطبيقات في التطبيقات باستخدام AIDL، يُرجى الاطّلاع على لغة تعريف واجهة نظام Android (AIDL). للاطّلاع على أمثلة على استخدام AIDL، يُرجى الاطّلاع على AIDL لواجهات HAL وStable AIDL.
تحديد الإصدار
يتوافق كل إصدار متوافق مع الإصدارات السابقة من واجهة برمجة تطبيقات AIDL مع إصدار.
لأخذ لقطة، شغِّل m <module-name>-freeze-api. عند إصدار عميل أو خادم لواجهة برمجة التطبيقات (على سبيل المثال، في إصدار Mainline)، عليك أخذ لقطة وإنشاء إصدار جديد. بالنسبة إلى واجهات برمجة التطبيقات من النظام إلى المورِّد، يجب أن يحدث ذلك مع مراجعة النظام الأساسي السنوية.
عند تجميد واجهة (حفظها في دليل aidl_api الذي تم تحديد إصداره)، يجب عدم تعديلها مطلقًا. يمكنك تعديل دليل current فقط. يمكنك بأمان إضافة طرق إلى نهاية واجهة، وحقول إلى نهاية عنصر قابل للتجزئة، ومعدِّدات إلى تعداد، وأعضاء إلى اتحاد.
يتلقّى العملاء الذين يستدعون طرقًا جديدة على خوادم قديمة الخطأ UNKNOWN_TRANSACTION، ويجب أن يعالجه العميل بشكلٍ سليم.
لمزيد من التفاصيل والمعلومات حول نوع التغييرات المسموح بها، يُرجى الاطّلاع على تحديد إصدارات الواجهات.
اعتمادات الإصدار
لا يمكن لوحدات Android الاعتماد على إصدارات متعدّدة ومختلفة من المكتبات التي تم إنشاؤها من aidl_interface. تحدّد الإصدارات المختلفة من المكتبات الأنواع نفسها في مساحات الأسماء نفسها. يرصد نظام التصميم aidl في Android هذه المشكلة ويُظهر خطأً مع كل من رسوم بيانية الاعتمادية التي تنتهي بإصدارات غير متطابقة من المكتبات.
قد يؤدي ذلك إلى صعوبة تحديث إصدار واحد من واجهة شائعة عندما تحتوي الوحدة على العديد من الاعتمادات مع اعتماداتها الخاصة.
يمكن للمطوّرين استخدام aidl_interface_defaults للإعلان عن اعتمادات واجهة مشترَكة على واجهات أخرى حتى لا تحتاج جميعها إلى التحديث بشكلٍ مستقل.
ننصح باستخدام وحدات *_defaults (مثل rust_defaults وcc_defaults وjava_defaults) لتنظيم الاعتمادات على المكتبات التي تم إنشاؤها. من الشائع أن يكون هناك إعداد تلقائي للإصدار latest من الواجهات، بالإضافة إلى الإعدادات التلقائية للإصدارات السابقة إذا كانت لا تزال مستخدَمة.
يمكن للمطوّرين استخدام aidl_interface_defaults للإعلان عن اعتمادات واجهة مشترَكة على واجهات أخرى حتى لا تحتاج جميعها إلى التحديث بشكلٍ مستقل.
إرشادات تصميم واجهة برمجة التطبيقات
بنود عامة
1. توثيق كل شيء
- يجب توثيق كل طريقة من حيث دلالاتها، ووسيطاتها، واستخدامها للاستثناءات المضمّنة، والاستثناءات الخاصة بالخدمة، والقيمة المعروضة.
- يجب توثيق كل واجهة من حيث دلالاتها.
- يجب توثيق المعنى الدلالي للتعدادات والثوابت.
- يجب توثيق أي شيء قد يكون غير واضح للمنفِّذ.
- يجب تقديم أمثلة عند الاقتضاء.
2. الغلاف
يجب استخدام التنسيق Camel Case العلوي للأنواع والتنسيق Camel Case السفلي للطرق والحقول والوسيطات. على سبيل المثال، MyParcelable لنوع قابل للتجزئة وanArgument
لوسيطة. بالنسبة إلى الأحرف الأولى، يجب اعتبارها كلمة واحدة (NFC -> Nfc).
[-Wconst-name] يجب أن تكون قيم التعداد والثوابت ENUM_VALUE و
CONSTANT_NAME
3. تجنُّب طلب معرفة شاملة
يجب ألا تفترض واجهات برمجة التطبيقات أنّ المطوّرين لديهم معرفة شاملة بقاعدة التعليمات البرمجية بأكملها أو خبرة محدّدة في مجال معيّن. عند التعامل مع المعرّفات الخاصة بالمجال (مثل أسماء الأجهزة أو أرقام التعريف أو المؤشرات):
- يجب أن تكون واضحًا وتوثِّق مصدر هذه المعرّفات وتنسيقها إذا كان من المهم أن يعرفها كلا الجانبَين من الواجهة.
- بدلاً من ذلك، يمكنك استخدام معرّفات خاصة بالواجهة (مثل عناصر الرابط أو الرموز المخصّصة) وجعل أحد الجانبَين يدير عملية الربط بالقيم الأساسية. يقلّل ذلك من حالات التعارض ويتجنّب مطالبة المستخدمين بفهم تفاصيل التنفيذ خارج نطاقهم.
4. تكون كل البيانات منظَّمة ومتوافقة مع الإصدارات السابقة
يجب أن يكون للبيانات غير المنظَّمة، مثل string وbyte[] والذاكرة المشترَكة، تنسيق مستقر لمحتوياتها، أو أن تكون غير واضحة لأحد جانبَي الواجهة.
على سبيل المثال، يمكن تلقّي وسيطة سلسلة مستخدَمة كرسالة خطأ لنتيجة وتسجيلها لأغراض تصحيح الأخطاء، ولكن يجب عدم تحليلها وتفسيرها لأنّ التنسيق والمحتويات قد لا يكونا متوافقَين مع الإصدارات السابقة. إذا كان الجانب الآخر من الواجهة بحاجة إلى معرفة الخطأ في وقت التشغيل، استخدِم تعدادًا أو ثابتًا أو ServiceSpecificException.
وبالمثل، لا تُسلسِل الكائنات في byte[] أو الذاكرة المشترَكة ما لم تكن مستقرة ومتوافقة مع الإصدارات السابقة. في بعض الحالات، يمكنك استخدام الشرح التوضيحي @FixedSize لمشاركة العناصر القابلة للتجزئة والاتحادات في الذاكرة المشترَكة و"صفوف الرسائل السريعة".
واجهات
1. التسمية
[-Winterface-name] يجب أن يبدأ اسم الواجهة بالحرف I، مثل IFoo.
2. تجنُّب الواجهات الكبيرة التي تحتوي على "كائنات" تستند إلى رقم تعريف
يُفضَّل استخدام الواجهات الفرعية عندما يكون هناك العديد من عمليات الاستدعاء المرتبطة بواجهة برمجة تطبيقات معيّنة. ويوفّر ذلك المزايا التالية:
- تسهيل فهم رمز العميل أو الخادم
- تسهيل دورة حياة الكائنات
- الاستفادة من عدم إمكانية تزوير الروابط.
إجراء لا يُنصح به: واجهة واحدة كبيرة تحتوي على كائنات تستند إلى رقم تعريف
interface IManager {
int getFooId();
void beginFoo(int id); // clients in other processes can guess an ID
void opFoo(int id);
void recycleFoo(int id); // ownership not handled by type
}
إجراء يُنصح به: واجهات فردية
interface IManager {
IFoo getFoo();
}
interface IFoo {
void begin(); // clients in other processes can't guess a binder
void op();
}
3. عدم الجمع بين الطرق أحادية الاتجاه وثنائية الاتجاه
[-Wmixed-oneway] لا تجمع بين الطرق أحادية الاتجاه والطرق غير أحادية الاتجاه، لأنّ ذلك يجعل فهم نموذج سلسلة المحادثات أمرًا معقدًا للعملاء والخوادم. على وجه التحديد، عند قراءة رمز العميل لواجهة معيّنة، عليك البحث عن كل طريقة لمعرفة ما إذا كانت هذه الطريقة ستحظر أم لا.
4. تجنُّب عرض رموز الحالة
يجب أن تتجنّب الطرق رموز الحالة كقيم معروضة، لأنّ جميع طرق AIDL تتضمّن رمز حالة ضمنيًا. يُرجى الاطّلاع على ServiceSpecificException أو EX_SERVICE_SPECIFIC. بموجب الاتفاقية، يتم تحديد هذه القيم كثوابت في واجهة AIDL. إذا كانت هناك حاجة إلى تأخير مخصّص أو بيانات خطأ فريدة إلى جانب الخطأ، فهذه هي الحالة الوحيدة التي يجب أن يمثّل فيها عنصر استجابة مخصّص خطأً. لمزيد من المعلومات التفصيلية، يُرجى الاطّلاع على
معالجة الأخطاء.
5. تُعد المصفوفات كمعلّمات إخراج ضارة
[-Wout-array] عادةً ما تكون الطرق التي تحتوي على معلّمات إخراج مصفوفة، مثل void foo(out String[] ret)، غير جيدة لأنّ حجم مصفوفة الإخراج يجب أن يعلنه العميل ويخصّصه في Java، وبالتالي لا يمكن للخادم اختيار حجم مصفوفة الإخراج. يحدث هذا السلوك غير المرغوب فيه بسبب طريقة عمل المصفوفات في Java (لا يمكن إعادة تخصيصها). بدلاً من ذلك، يُفضَّل استخدام واجهات برمجة التطبيقات مثل String[] foo().
6. تجنُّب معلّمات inout
[-Winout-parameter] قد يؤدي ذلك إلى إرباك العملاء لأنّ معلّمات in تبدو مثل معلّمات out.
7. تجنُّب معلّمات `out` و`inout` غير مصفوفة التي يمكن أن تكون فارغة
[-Wout-nullable] بما أنّ Java في الخلفية لا يعالج الشرح التوضيحي @nullable
بينما تعالجه الخلفيات الأخرى، قد يؤدي out/inout @nullable T إلى سلوك غير متّسق
في جميع الخلفيات. على سبيل المثال، يمكن للخلفيات غير Java ضبط معلّمة @nullable
out على القيمة null (في C++، ضبطها على std::nullopt) ولكن لا يمكن لعميل Java
قراءتها على أنّها null.
8. استخدام طلبات واستجابات فريدة
يجب تجميع جميع المعلّمات الضرورية في parcelable إدخال واحد.
يجب إنشاء عناصر `parcelable` مخصّصة للطلبات والاستجابات لكل طريقة واجهة بدلاً من تمرير القيم الأولية (على سبيل المثال، استخدِم ComputeResponse compute(in ComputeRequest request) بدلاً من تمرير متغيرات منفصلة). يسمح ذلك بإضافة وسيطات جديدة لاحقًا بدون تغيير توقيع الدالة. يُنصح بشدة بهذا النمط عندما يُتوقّع إضافة المزيد من المعلّمات في المستقبل، أو إذا كانت الطريقة تحتوي على أكثر من أربع معلّمات.
لن تستفيد الطرق التي لا تتطلّب مدخلات أو مخرجات إضافية من هذا الاقتراح. يمكن أن يؤدي التفكير بوضوح في كل حالة والحفاظ على المرونة للتغييرات المستقبلية إلى تقليل عدد الطرق التي تم إيقافها وتقليل التعقيد في الرمز المتوافق مع الإصدارات السابقة.
إذا لم يتم إنشاء طريقة باستخدام هذا النمط، يمكنك التبديل إلى هذا النمط من خلال إنشاء طريقة جديدة باستخدام عنصر `parcelable` للطلب والاستجابة وإيقاف الطريقة القديمة. على سبيل المثال:
void foo(int a, int b, int c); // original version, but deprecated in favor of the next version
void fooV2(in MyArg arg); // new version having int a, b, c, and d.
العناصر القابلة للتجزئة المنظَّمة
1. حالات الاستخدام
يجب استخدام العناصر القابلة للتجزئة المنظَّمة عندما يكون لديك أنواع بيانات متعدّدة لإرسالها.
أو عندما يكون لديك نوع بيانات واحد ولكنك تتوقّع أنّك ستحتاج إلى توسيعه في المستقبل. على سبيل المثال، لا تستخدِم String username. استخدِم عنصرًا قابلاً للتجزئة يمكن توسيعه، مثل ما يلي:
parcelable User {
String username;
}
بحيث يمكنك توسيعه في المستقبل على النحو التالي:
parcelable User {
String username;
int id;
}
2. توفير الإعدادات التلقائية بشكلٍ صريح
[-Wexplicit-default, -Wenum-explicit-default] يجب توفير إعدادات تلقائية صريحة للحقول. عند إضافة حقول جديدة إلى عنصر قابل للتجزئة، يتجاهلها العملاء والخوادم القديمة، ولكن يتم ملء القيم التلقائية تلقائيًا للعملاء والخوادم الجديدة.
3. استخدام `ParcelableHolder` للإضافات الخاصة بالمورِّد
إذا كنت تحدّد عنصر parcelable في AOSP يحتاج منفِّذو الأجهزة إلى توسيعه، عليك تضمين مثيل من ParcelableHolder في العنصر. يعمل ذلك كنقطة توسيع بدون إنشاء تعارضات في الدمج. يشبه ذلك إضافات الواجهة المرفقة
، ولكنّه يسمح للمنفِّذين بتضمين عنصر parcelable الخاص بهم إلى جانب عنصر parcelable الحالي
بدون إنشاء واجهة وأنواع خاصة بهم.
4. هياكل البيانات
- يجب استخدام المصفوفات أو
Listمن العناصر القابلة للتجزئة لتمثيل الخرائط، لأنّ AIDL لا تتيح بشكلٍ أصلي أنواعMapالتي يمكن ترجمتها بأمان في جميع الخلفيات الأصلية (على سبيل المثال،FeatureToScoreEntry[]). - يجب استخدام مصفوفات من كائنات
parcelableللحقول المتكرّرة بدلاً من مصفوفات القيم الأولية، وذلك لتجنُّب الحاجة إلى مصفوفات متوازية في المستقبل. - يجب استخدام كائنات
parcelableذات أنواع محدّدة بدلاً من السلاسل أو JSON التي تم تسلسلها عبر IPC. - يجب استخدام التعدادات بدلاً من القيم المنطقية للحالات للسماح بالتوسيع في المستقبل. بالنسبة إلى أقنعة البت، يجب استخدام
const intبدلاً من أنواعenumلتجنُّب عمليات التحويل المعقدة في بعض الخلفيات.
العناصر القابلة للتجزئة غير المنظَّمة
1. حالات الاستخدام
تتوفّر العناصر القابلة للتجزئة غير المنظَّمة في Java باستخدام @JavaOnlyStableParcelable وفي الخلفية NDK باستخدام @NdkOnlyStableParcelable. عادةً ما تكون هذه العناصر قديمة وموجودة ولا يمكن تنظيمها.
الثوابت والتعدادات
1. يجب أن تستخدم حقول البت حقولاً ثابتة
يجب أن تستخدم حقول البت حقولاً ثابتة (على سبيل المثال، const int FOO = 3; في واجهة).
2. يجب أن تكون التعدادات مجموعات مغلقة.
يجب أن تكون التعدادات مجموعات مغلقة. ملاحظة: يمكن لمالك الواجهة فقط إضافة عناصر التعداد. إذا كان على المورِّدين أو الشركات المصنّعة للمعدات الأصلية توسيع هذه الحقول، يجب استخدام آلية بديلة. يُفضَّل نقل وظائف المورِّد إلى المصدر كلما أمكن ذلك. ومع ذلك، في بعض الحالات، قد يُسمح بقيم المورِّد المخصّصة (على الرغم من أنّه يجب أن يكون لدى المورِّدين آلية لتحديد إصدار هذه القيم، ربما AIDL نفسه، ويجب ألا تتعارض مع بعضها البعض، ويجب ألا يتم عرض هذه القيم على تطبيقات الجهات الخارجية).
3. تجنُّب قيم مثل "NUM_ELEMENTS"
بما أنّه يتم تحديد إصدارات التعدادات، يجب تجنُّب القيم التي تشير إلى عدد القيم المتوفّرة. في C++، يمكن حلّ هذه المشكلة باستخدام enum_range<>. بالنسبة إلى Rust، استخدِم enum_values(). في Java، ليس هناك حلّ بعد.
إجراء لا يُنصح به: استخدام قيم مرقّمة
@Backing(type="int")
enum FruitType {
APPLE = 0,
BANANA = 1,
MANGO = 2,
NUM_TYPES, // BAD
}
4. تجنُّب البادئات واللاحقات الزائدة
[-Wredundant-name] يجب تجنُّب البادئات واللاحقات الزائدة أو المتكرّرة في الثوابت والمعدِّدات.
إجراء لا يُنصح به: استخدام بادئة زائدة
enum MyStatus {
STATUS_GOOD,
STATUS_BAD // BAD
}
إجراء يُنصح به: تسمية التعداد مباشرةً
enum MyStatus {
GOOD,
BAD
}
`FileDescriptor`
[-Wfile-descriptor] يُنصح بشدة بعدم استخدام FileDescriptor كوسيطة أو قيمة معروضة لطريقة واجهة AIDL. على وجه الخصوص، عند تنفيذ AIDL في Java، قد يؤدي ذلك إلى تسرُّب واصف الملف ما لم تتم معالجته بعناية. بشكلٍ أساسي، إذا قبلت FileDescriptor، عليك إغلاقه يدويًا عندما لا يعود قيد الاستخدام.
بالنسبة إلى الخلفيات الأصلية، يمكنك استخدامها بأمان لأنّ FileDescriptor يتم ربطه بـ unique_fd الذي يمكن إغلاقه تلقائيًا. ولكن بغض النظر عن لغة الخلفية التي ستستخدمها، من الحكمة عدم استخدام FileDescriptor على الإطلاق لأنّ ذلك سيحدّ من حريتك في تغيير لغة الخلفية في المستقبل.
بدلاً من ذلك، استخدِم ParcelFileDescriptor الذي يمكن إغلاقه تلقائيًا.
وحدات متغيّرة
يجب التأكّد من تضمين وحدات المتغيّرات في الاسم بحيث تكون وحداتها محدّدة جيدًا ومفهومة بدون الحاجة إلى الرجوع إلى المستندات.
أمثلة
long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good
double energy; // Bad
double energyMilliJoules; // Good
int frequency; // Bad
int frequencyHz; // Good
يجب أن تشير الطوابع الزمنية إلى مرجعها
يجب أن تشير الطوابع الزمنية (في الواقع، جميع الوحدات) بوضوح إلى وحداتها ونقاطها المرجعية.
أمثلة
/**
* Time since device boot in milliseconds
*/
long timestampMs;
/**
* UTC time received from the NTP server in units of milliseconds
* since January 1, 1970
*/
long utcTimeMs;
التزامن والعمليات غير المتزامنة
يجب معالجة العمليات الطويلة الأمد باستخدام واجهة غير متزامنة (oneway) لتجنُّب الحظر.
إذا كانت الخدمة لا تثق في عملائها، يجب أن تكون أي معاودة اتصال تتلقّاها من العملاء واجهات oneway. يمنع ذلك العملاء من حظر الخدمة إلى أجل غير مسمّى.
يجب تنظيم واجهات برمجة التطبيقات غير المتزامنة التي تتألف من عملية استدعاء أمامية، ووسيطات إدخال، وواجهة رد الاتصال للحصول على النتائج. يُرجى الاطّلاع على استخدام طلبات واستجابات فريدة للحصول على اقتراحات بشأن الوسيطات.