توضِّح هذه الصفحة كيفية استخدام تطبيقك لوظائف نظام التشغيل الجديدة عند تشغيله على إصدارات جديدة من نظام التشغيل مع الحفاظ على التوافق مع الأجهزة القديمة.
تكون الإشارات إلى واجهات برمجة تطبيقات 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 عبارة مماثلة لعبارة "تم طرحها في المستوى 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 Issue 33161 للحصول على مزيد من المعلومات.
لا تنطبق عمليات التحقّق من unguarded-availability
إلا على نطاق الدالة التي يتم
استخدامها فيه. سيُصدر Clang التحذير حتى إذا كانت الدالة التي تحتوي على طلب بيانات من واجهة برمجة التطبيقات
لا يتم استدعاؤها إلا من نطاق محمي. لتجنُّب تكرار إجراءات التحقّق في
الرمز البرمجي الخاص بك، اطّلِع على تجنُّب تكرار إجراءات التحقّق من واجهة برمجة التطبيقات.
لماذا لا يكون هذا الخيار التلقائي؟
في حال عدم استخدامها بشكل صحيح، يتمثل الفرق بين مراجع واجهة برمجة التطبيقات القوية ومراجع واجهة برمجة التطبيقات الضعيفة في أنّ الأولى ستتعذّر إتمامها بسرعة وبشكل واضح، في حين لن تتعذّر إتمام المراجع الأخيرة إلى أن يتّخذ المستخدم إجراءً يؤدي إلى استدعاء واجهة برمجة التطبيقات التي لا تعمل. وعندما يحدث ذلك، لن تكون رسالة الخطأ واضحة خطأ وقت الترجمة "AFoo_bar() غير متاح"، بل ستكون خطأ segfault. باستخدام المراجع القوية، تكون رسالة الخطأ أكثر وضوحًا، ويكون الإجراء "توقّف سريع" هو الإعداد التلقائي الأكثر أمانًا.
وبما أنّ هذه ميزة جديدة، تمّ كتابة القليل جدًا من الرموز البرمجية الحالية للتعامل مع هذا السلوك بأمان. من المحتمل أن تواجه دائمًا هذه المشكلة في الرموز البرمجية التابعة لجهات خارجية والتي لم يتم إنشاؤها بالتوافق مع Android، لذا لا تتوفّر حاليًا أي خطط لتغيير السلوك التلقائي.
ننصحك بشدّة باستخدام هذا الإجراء، ولكن بما أنّه سيصعّب رصد المشاكل وتصحيحها، عليك قبول هذه المخاطر عن وعي بدلاً من أن يتغيّر السلوك بدون علمك.
المحاذير
تعمل هذه الميزة مع معظم واجهات برمجة التطبيقات، ولكن هناك بعض الحالات التي لا تعمل فيها.
قبل الإصدار r28 من NDK، لم يكن هذا الإجراء يعمل مع واجهات برمجة التطبيقات libc أو libm.
من المرجّح أن يواجه المزيد من المطوّرين مشكلة عندما تكون المكتبة التي تحتوي على واجهة برمجة التطبيقات الجديدة أحدث من minSdkVersion
. لا تتيح هذه الميزة سوى
تفعيل مراجع الرموز الضعيفة، ولا يتوفّر مرجع مكتبة ضعيف. على سبيل المثال، إذا كان الإصدار minSdkVersion
هو 24، يمكنك ربط
libvulkan.so
وإجراء طلب بيانات محمي إلى vkBindBufferMemory2
، لأنّ
libvulkan.so
متاح على الأجهزة التي تعمل بالإصدار 24 من واجهة برمجة التطبيقات أو الإصدارات الأحدث. من ناحية أخرى،
إذا كان minSdkVersion
هو 23، عليك الرجوع إلى dlopen
وdlsym
لأنّ المكتبة لن تكون متوفّرة على الأجهزة التي تتيح استخدام
واجهة برمجة التطبيقات 23 فقط. لا نعرف حلًا جيدًا لحلّ هذه المشكلة، ولكن في المدّة الطويلة،
ستتم معالجتها تلقائيًا لأنّنا (كلما أمكن) لم نعُد نسمح
باستخدام واجهات برمجة تطبيقات جديدة لإنشاء مكتبات جديدة.
لمؤلفي المكتبة
إذا كنت تُطوّر مكتبة لاستخدامها في تطبيقات Android، يجب
تجنُّب استخدام هذه الميزة في رؤوسك العامة. يمكن استخدامها بأمان في
الرمز البرمجي غير المضمّن، ولكن إذا كنت تعتمد على __builtin_available
في أي رمز في
العناوين، مثل الدوالّ المضمّنة أو تعريفات النماذج، ستجبر جميع
المستخدِمين على تفعيل هذه الميزة. ولأسباب مماثلة، لا نفعّل هذه الميزة تلقائيًا في NDK، لذا عليك تجنُّب اتخاذ هذا الخيار بالنيابة عن المستهلكين.
إذا كنت تتطلّب هذا السلوك في العناوين العامة، احرص على توثيق ذلك حتى يعرف المستخدمون أنّهم سيحتاجون إلى تفعيل الميزة ويعوا مخاطر إجراء ذلك.