توضِّح هذه الصفحة كيفية استخدام تطبيقك لوظائف نظام التشغيل الجديدة عند تشغيله على إصدارات جديدة من نظام التشغيل مع الحفاظ على التوافق مع الأجهزة القديمة.
تكون الإشارات إلى واجهات برمجة تطبيقات NDK في تطبيقك تلقائيًا إشارات قوية. سيعمل أداة التحميل الديناميكية في Android على حلّها عند تحميل مكتبتك. في حال عدم العثور على الرموز، سيتم إيقاف التطبيق. وهذا يتناقض مع سلوك Java، حيث لن يتم طرح استثناء إلى أن يتم استدعاء واجهة برمجة التطبيقات المفقودة.
لهذا السبب، سيمنعك NDK من إنشاء إحالات قوية إلى
واجهات برمجة التطبيقات الأحدث من minSdkVersion
لتطبيقك. يحميك ذلك من
إرسال رمز عن طريق الخطأ كان يعمل أثناء الاختبار ولكن لن يتم تحميله
(سيتم طرح UnsatisfiedLinkError
من System.loadLibrary()
) على
الأجهزة القديمة. من ناحية أخرى، من الصعب كتابة رمز يستخدم واجهات برمجة تطبيقات
أحدث من minSdkVersion
في تطبيقك، لأنّك يجب أن تطلب واجهات برمجة التطبيقات باستخدام
dlopen()
وdlsym()
بدلاً من طلب دالة عادية.
ويتمثل البديل لاستخدام المراجع القوية في استخدام المراجع الضعيفة. يؤدي المراجع الضعيفة التي لا يتم العثور عليها عند تحميل المكتبة إلى ضبط عنوان
هذا الرمز على nullptr
بدلاً من تعذُّر تحميله. لا يزال يتعذّر
استدعاءها بأمان، ولكن طالما أنّ مواقع الاستدعاء محمية لمنع استدعاء
واجهة برمجة التطبيقات عندما تكون غير متاحة، يمكن تشغيل بقية الرمز البرمجي، ويمكنك
استدعاء واجهة برمجة التطبيقات بشكلٍ طبيعي بدون الحاجة إلى استخدام dlopen()
وdlsym()
.
لا تتطلّب إشارات واجهة برمجة التطبيقات الضعيفة دعمًا إضافيًا من الرابط الديناميكي، لذلك يمكن استخدامها مع أي إصدار من Android.
تفعيل مراجع واجهة برمجة التطبيقات الضعيفة في الإصدار
CMake
مرِّر -DANDROID_WEAK_API_DEFS=ON
عند تشغيل CMake. إذا كنت تستخدم CMake من خلال
externalNativeBuild
، أضِف ما يلي إلى build.gradle.kts
(أو البديل المتوافق مع
Groovy إذا كنت لا تزال تستخدم build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
أضِف ما يلي إلى ملف Application.mk
:
APP_WEAK_API_DEFS := true
إذا لم يكن لديك ملف Application.mk
، أنشِئه في الкаталог نفسه
الذي يتضمّن ملف Android.mk
. لا يلزم إجراء تغييرات إضافية على ملف
build.gradle.kts
(أو build.gradle
) لاستخدام أداة ndk-build.
أنظمة التصميم الأخرى
إذا كنت لا تستخدِم CMake أو ndk-build، يمكنك الرجوع إلى مستندات نظام الإنشاء لمعرفة ما إذا كانت هناك طريقة مقترَحة لتفعيل هذه الميزة. إذا كان نظام الإنشاء لا يتيح هذا الخيار بشكلٍ تلقائي، يمكنك تفعيل الميزة من خلال تمرير العلامات التالية عند الترجمة:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
يضبط الخيار الأول عناوين NDK للسماح بالإشارات الضعيفة. ويؤدي الخيار الثاني إلى تحويل التحذير بشأن طلبات البيانات غير الآمنة من واجهة برمجة التطبيقات إلى خطأ.
اطّلِع على دليل مشرفي نظام الإنشاء للحصول على مزيد من المعلومات.
طلبات البيانات المحمية من واجهة برمجة التطبيقات
لا تجعل هذه الميزة طلبات البيانات من واجهات برمجة التطبيقات الجديدة آمنة بشكلٍ سحري. إنّه يؤدي فقط إلى تأخير خطأ وقت التحميل إلى خطأ وقت الطلب. وتتمثل الفائدة في أنّه يمكنك حماية هذا الطلب في وقت التشغيل والرجوع إلى الإجراء الاحتياطي بشكل ملائم، سواء باستخدام طريقة تنفيذ بديلة أو إبلاغ المستخدم بأنّ هذه الميزة في التطبيق غير متاحة على جهازه، أو تجنُّب مسار الرمز البرمجي هذا بالكامل.
يمكن أن يُصدر Clang تحذيرًا (unguarded-availability
) عند إجراء
طلب غير محمي لواجهة برمجة تطبيقات غير متاحة minSdkVersion
لتطبيقك. إذا كنت تستخدم
ndk-build أو ملف سلسلة أدوات CMake، سيتم تفعيل هذا التحذير
تلقائيًا وسيتم ترقيته إلى خطأ عند تفعيل هذه الميزة.
في ما يلي مثال على بعض الرموز البرمجية التي تستخدم واجهة برمجة تطبيقات بشكل مشروط بدون
تفعيل هذه الميزة، باستخدام dlopen()
وdlsym()
:
void LogImageDecoderResult(int result) {
void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
dlsym(lib, "AImageDecoder_resultToString")
);
if (func == nullptr) {
LOG(INFO) << "cannot stringify result: " << result;
} else {
LOG(INFO) << func(result);
}
}
إنّه من الصعب قراءة هذا الرمز البرمجي، لأنّه يتضمّن بعض تكرار أسماء الدوال (وإذا كنت تكتب رمز C، تتضمّن أيضًا التوقيعات)، وسيتم إنشاؤه بنجاح، ولكن سيتم دائمًا استخدام الإجراء الاحتياطي في وقت التشغيل إذا أخطأت في كتابة اسم الدالة التي تم تمريرها
إلى dlsym
، وعليك استخدام هذا النمط لكل واجهة برمجة تطبيقات.
باستخدام مراجع واجهة برمجة التطبيقات الضعيفة، يمكن إعادة كتابة الدالة أعلاه على النحو التالي:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
في الخلفية، يُطلِق __builtin_available(android 31, *)
android_get_device_api_level()
ويخزِّن النتيجة ويقارنها مع 31
(وهو مستوى واجهة برمجة التطبيقات الذي أدخل AImageDecoder_resultToString()
).
إنّ أبسط طريقة لتحديد القيمة التي يجب استخدامها لسمة __builtin_available
هي محاولة الإنشاء بدون العنصر الحارس (أو عنصر حارس من نوع
__builtin_available(android 1, *)
) واتّباع التعليمات الواردة في رسالة الخطأ.
على سبيل المثال، سيؤدي طلب غير محمي إلى AImageDecoder_createFromAAsset()
باستخدام
minSdkVersion 24
إلى ظهور ما يلي:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
في هذه الحالة، يجب حماية المكالمة من خلال __builtin_available(android 30, *)
.
إذا لم يكن هناك خطأ في عملية الإنشاء، يعني ذلك أنّ واجهة برمجة التطبيقات متاحة دائمًا لملف
minSdkVersion
ولا حاجة إلى استخدام أداة حماية، أو أنّه تم ضبط إعدادات الإصدار بشكلٍ خاطئ وتم إيقاف تحذير
unguarded-availability
.
بدلاً من ذلك، سيعرض مرجع NDK API عبارة مماثلة لعبارة "تم طرحها في المستوى 30 لواجهة برمجة التطبيقات" لكل واجهة برمجة تطبيقات. إذا لم يكن هذا النص متوفّرًا، يعني ذلك أنّ واجهة برمجة التطبيقات متاحة لجميع مستويات واجهة برمجة التطبيقات المتوافقة.
تجنُّب تكرار حراس واجهة برمجة التطبيقات
في حال استخدام هذا الرمز، من المحتمل أن يكون لديك أقسام من الرموز في تطبيقك
لا يمكن استخدامها إلا على الأجهزة الجديدة بما يكفي. بدلاً من تكرار التحقق من
__builtin_available()
في كل دالة، يمكنك إضافة تعليق توضيحي يشير إلى أنّه يجب استخدام مستوى معيّن من واجهة برمجة التطبيقات في
الرمز البرمجي الخاص بك. على سبيل المثال، تمت إضافة واجهات برمجة تطبيقات ImageDecoder
نفسها في الإصدار 30 من واجهة برمجة التطبيقات، لذا بالنسبة إلى الدوالّ التي تستخدِم
واجهات برمجة التطبيقات هذه بشكل كبير، يمكنك إجراء ما يلي:
#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)
void DecodeImageWithImageDecoder() REQUIRES_API(30) {
// Call any APIs that were introduced in API 30 or newer without guards.
}
void DecodeImageFallback() {
// Pay the overhead to call the Java APIs via JNI, or use third-party image
// decoding libraries.
}
void DecodeImage() {
if (API_AT_LEAST(30)) {
DecodeImageWithImageDecoder();
} else {
DecodeImageFallback();
}
}
ميزات حراس واجهة برمجة التطبيقات
يهتم Clang كثيرًا بطريقة استخدام __builtin_available
. لا يعمل سوى الرمز الثابت
(على الرغم من أنّه قد يتم استبداله برمز ماكرو) if (__builtin_available(...))
. ولن تعمل حتى العمليات البسيطة مثل if (!__builtin_available(...))
(سيُصدر Clang تحذير unsupported-availability-guard
بالإضافة إلىunguarded-availability
). قد يتحسن هذا في إصدار مستقبلي من Clang. يمكنك الاطّلاع على
مشكلة LLVM 33161 للحصول على مزيد من المعلومات.
لا تنطبق عمليات التحقّق من unguarded-availability
إلا على نطاق الدالة التي يتم
استخدامها فيه. سيُصدر Clang التحذير حتى إذا كانت الدالة التي تتضمّن طلب بيانات من واجهة برمجة التطبيقات
لا يتم استدعاؤها إلا من نطاق محمي. لتجنُّب تكرار إجراءات التحقّق في
الرمز البرمجي الخاص بك، اطّلِع على تجنُّب تكرار إجراءات التحقّق من واجهة برمجة التطبيقات.
لماذا لا يكون هذا الخيار التلقائي؟
في حال عدم استخدامها بشكل صحيح، يتمثل الفرق بين مراجع واجهة برمجة التطبيقات القوية ومراجع واجهة برمجة التطبيقات الضعيفة في أنّ الأولى ستتعذّر إتمامها بسرعة وبشكل واضح، في حين لن تتعذّر إتمام المراجع الأخيرة إلى أن يتّخذ المستخدم إجراءً يؤدي إلى استدعاء واجهة برمجة التطبيقات التي لا تعمل. وعندما يحدث ذلك، لن تكون رسالة الخطأ واضحة، وستكون عبارة عن خطأ segfault بدلاً من خطأ "AFoo_bar() غير متاح" في وقت الترجمة. باستخدام المراجع القوية، تكون رسالة الخطأ أكثر وضوحًا، ويكون الإجراء "توقّف سريع" هو الإعداد التلقائي الأكثر أمانًا.
وبما أنّ هذه ميزة جديدة، تمّ كتابة القليل جدًا من الرموز البرمجية الحالية للتعامل مع هذا السلوك بأمان. من المحتمل أن تواجه دائمًا هذه المشكلة في الرموز البرمجية التابعة لجهات خارجية والتي لم يتم إنشاؤها بالتوافق مع Android، لذا لا تتوفّر حاليًا أي خطط لتغيير السلوك التلقائي.
ننصحك بشدّة باستخدام هذا الإجراء، ولكن بما أنّه سيصعّب رصد المشاكل وتصحيحها، عليك قبول هذه المخاطر عن وعي بدلاً من أن يتغيّر السلوك بدون علمك.
المحاذير
تعمل هذه الميزة مع معظم واجهات برمجة التطبيقات، ولكن هناك بعض الحالات التي لا تعمل فيها.
إنّ واجهات برمجة التطبيقات libc الأحدث هي الأقل احتمالًا أن تتسبب في حدوث مشاكل. على عكس بقية
واجهات برمجة تطبيقات Android، يتم تأمين هذه الواجهات باستخدام #if __ANDROID_API__ >= X
في العناوين
وليس فقط __INTRODUCED_IN(X)
، ما يمنع حتى البيان الضعيف
من الظهور. بما أنّ أقدم مستوى لواجهة برمجة التطبيقات يتيح استخدام حِزم NDK الحديثة هو r21، فإنّ واجهات برمجة التطبيقات libc الأكثر استخدامًا متاحة حاليًا. تتم إضافة واجهات برمجة تطبيقات libc الجديدة في كل
إصدار (راجِع status.md)، ولكن كلما كان الإصدار أحدث، زاد احتمال أن يكون
حالة استثنائية يحتاج إليها عدد قليل من المطوّرين. ومع ذلك، إذا كنت أحد
المطوّرين الذين يستخدمون dlsym()
، عليك مواصلة استخدام dlsym()
للاتّصال بواجهات برمجة التطبيقات هذه إذا كان minSdkVersion
لديك أقدم من واجهة برمجة التطبيقات. هذه مشكلة قابلة للحل،
ولكن يؤدي إجراء ذلك إلى خطر إيقاف توافق المصدر مع جميع التطبيقات (أي
رمز يحتوي على polyfills لواجهات برمجة تطبيقات libc لن يتم تجميعه بسبب عدم مطابقة سمات availability
في libc والإعلانات المحلية)، لذلك
لستُ متأكّدًا مما إذا كنا سنصلحها أو متى سنصلحها.
من المرجّح أن يواجه المزيد من المطوّرين مشكلة عندما تكون المكتبة التي تحتوي على واجهة برمجة التطبيقات الجديدة أحدث من minSdkVersion
. لا تتيح هذه الميزة سوى
تفعيل مراجع الرموز الضعيفة، ولا يتوفّر مرجع مكتبة ضعيف. على سبيل المثال، إذا كان minSdkVersion
هو 24، يمكنك ربط
libvulkan.so
وإجراء طلب بيانات محمي إلى vkBindBufferMemory2
، لأنّ
libvulkan.so
متاح على الأجهزة التي تعمل بالإصدار 24 من واجهة برمجة التطبيقات. من ناحية أخرى،
إذا كان minSdkVersion
هو 23، عليك الرجوع إلى dlopen
وdlsym
لأنّ المكتبة لن تكون متوفّرة على الأجهزة التي تتيح استخدام
واجهة برمجة التطبيقات 23 فقط. لا نعرف حلًا جيدًا لحلّ هذه المشكلة، ولكن في المدى الطويل، سيتم حلّها من تلقاء نفسها لأنّنا (كلما أمكن) لم نعُد نسمح باستخدام واجهات برمجة تطبيقات جديدة لإنشاء مكتبات جديدة.
لمؤلفي الكتب في المكتبة
إذا كنت تُطوّر مكتبة لاستخدامها في تطبيقات Android، يجب
تجنُّب استخدام هذه الميزة في رؤوسك العامة. ويمكن استخدامه بأمان في
الرمز البرمجي غير المضمّن، ولكن إذا كنت تعتمد على __builtin_available
في أي رمز في
العناوين، مثل الدوالّ المضمّنة أو تعريفات النماذج، ستجبر جميع
المستخدِمين على تفعيل هذه الميزة. ولأسباب مماثلة، لا نفعّل هذه الميزة تلقائيًا في NDK، لذا عليك تجنُّب اتخاذ هذا الخيار بالنيابة عن المستهلكين.
إذا كنت تتطلّب هذا السلوك في العناوين العامة، احرص على توثيق ذلك حتى يعرف المستخدمون أنّهم سيحتاجون إلى تفعيل الميزة ويعوا مخاطر إجراء ذلك.