استخدام واجهات برمجة تطبيقات أحدث

توضّح هذه الصفحة طريقة استخدام تطبيقك لوظائف نظام التشغيل الجديدة عند تشغيله على إصدارات جديدة من نظام التشغيل مع الحفاظ على التوافق مع الأجهزة القديمة.

بشكل افتراضي، تكون الإشارات إلى واجهات برمجة تطبيقات NDK في تطبيقك مراجع قوية. سيتعامل برنامج التحميل الديناميكي من Android بسرعة مع المشاكل عند تحميل المكتبة. وفي حال عدم العثور على هذه الرموز، سيتم إلغاء التطبيق. وهذا أمر مخالف لآلية عمل Java، إذ لا يتم طرح استثناء إلا عند استدعاء واجهة برمجة التطبيقات المفقودة.

ولهذا السبب، سيمنعك NDK من إنشاء مراجع قوية لواجهات برمجة التطبيقات الأحدث من minSdkVersion لتطبيقك. يوفّر لك ذلك الحماية من رمز الشحن الذي تم تشغيله عن طريق الخطأ أثناء الاختبار ولكن سيتعذر تحميله (سيتم طرح UnsatisfiedLinkError من System.loadLibrary()) على الأجهزة القديمة. من ناحية أخرى، يصعب كتابة رموز برمجية تستخدم واجهات برمجة تطبيقات أحدث من minSdkVersion لتطبيقك، إذ يجب استدعاء واجهات برمجة التطبيقات باستخدام dlopen() وdlsym() بدلاً من استدعاء دالة عادية.

بدلاً من استخدام مراجع قوية، يمكن استخدام مراجع ضعيفة. يؤدي وجود مرجع ضعيف عند تحميل المكتبة إلى ضبط عنوان هذا الرمز على nullptr بدلاً من تعذُّر التحميل. لا يمكن طلبها بشكل آمن، ولكن طالما أنّ مواقع الاتصال محمية لمنع استدعاء واجهة برمجة التطبيقات عندما لا تكون متاحة، يمكنك تشغيل باقي الرموز البرمجية، ويمكنك طلب واجهة برمجة التطبيقات بشكل طبيعي بدون الحاجة إلى استخدام dlopen() وdlsym().

لا تتطلب مراجع واجهة برمجة التطبيقات الضعيفة دعمًا إضافيًا من الرابط الديناميكي، لذا يمكن استخدامها مع أي إصدار من Android.

تفعيل مراجع واجهة برمجة التطبيقات الضعيفة في إصدارك

إنشاء فيديوهات Shorts

اجتياز -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 Toolchain، سيتم تفعيل هذا التحذير تلقائيًا والترويج له إلى خطأ عند تفعيل هذه الميزة.

في ما يلي مثال على بعض الرموز البرمجية التي تستفيد من استخدام مشروط لواجهة برمجة تطبيقات بدون تفعيل هذه الميزة، وذلك باستخدام 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 API نفسها في واجهة برمجة التطبيقات 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. راجِع المشكلة 33161 من LLVM للاطّلاع على مزيد من المعلومات.

لا تنطبق عمليات البحث عن unguarded-availability إلا على نطاق الدالة الذي يتم استخدامها فيه. سينبعث Clang التحذير حتى إذا كان يتم استدعاء الدالة مع طلب البيانات من واجهة برمجة التطبيقات فقط من نطاق محمي. لتجنّب تكرار برامج حماية واجهة برمجة التطبيقات، يمكنك الاطّلاع على تجنُّب تكرار إجراءات حماية واجهة برمجة التطبيقات.

لماذا ليس هذا هو الخيار التلقائي؟

ما لم يتم الاستخدام بشكل صحيح، يكمن الفرق بين المراجع القوية لواجهة برمجة التطبيقات والمراجع الضعيفة لواجهة برمجة التطبيقات في أنّ الأولى ستفشل بسرعة وبشكل واضح، بينما لن تفشل العلامة الثانية إلى أن يتخذ المستخدم إجراءً يؤدي إلى طلب واجهة برمجة التطبيقات غير المتوفّرة. عندما يحدث هذا، لن تكون رسالة الخطأ هي خطأ واضح في وقت التجميع "AFoo_bar() is not available"، ستكون الخطأ segError. مع وجود مراجع قوية، تكون رسالة الخطأ أكثر وضوحًا، والإخفاق هو الخيار الافتراضي الأكثر أمانًا.

ولأن هذه ميزة جديدة، تتم كتابة عدد قليل جدًا من التعليمات البرمجية الموجودة للتعامل مع هذا السلوك بأمان. من المحتمل أن تواجه دائمًا هذه المشكلة الرمز البرمجي التابع لجهة خارجية الذي لم تتم كتابته مع مراعاة نظام Android، لذلك ليس هناك حاليًا أي خطط لتغيير السلوك التلقائي على الإطلاق.

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

محاذير

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

وأقل احتمالاً لحدوث مشكلة هو واجهات برمجة تطبيقات libc الجديدة. وعلى عكس باقي واجهات برمجة تطبيقات Android، تتم حماية هذه الواجهات باستخدام #if __ANDROID_API__ >= X في العناوين وليس فقط __INTRODUCED_IN(X)، ما يمنع ظهور التعريف الضعيف. ونظرًا لأنّ حِزم NDK الحديثة ذات مستوى واجهة برمجة تطبيقات هي r21، تتوفّر واجهات برمجة تطبيقات libc الأكثر شيوعًا. تتم إضافة واجهات برمجة تطبيقات libc جديدة في كل إصدار (راجِع status.md)، ولكن كلما كانت أحدث، زاد احتمال أن تكون حالة قصوى لا يحتاج إليها عدد قليل من المطوّرين. ومع ذلك، إذا كنت أحد هؤلاء المطوّرين، عليك الآن مواصلة استخدام dlsym() لطلب البيانات من واجهات برمجة التطبيقات هذه إذا كان minSdkVersion أقدم من واجهة برمجة التطبيقات. يمكن حلّ هذه المشكلة، إلا أنّ ذلك قد يؤدي إلى تعطُّل توافق جميع التطبيقات مع جميع التطبيقات (لأنّ أي رمز برمجي يحتوي على polyfills من واجهات برمجة تطبيقات libc سيتعذّر تجميعهما بسبب سمات availability غير المتطابقة ضمن التعريفات المحلية وlibc)، لذا لسنا متأكدين مما إذا كنّا سنصلح هذه المشكلة أم لا.

وهذه الحالة التي يُرجح أن يواجهها المزيد من المطوّرين هي عندما تكون المكتبة التي تحتوي على واجهة برمجة التطبيقات الجديدة أحدث من minSdkVersion. لا تتيح هذه الميزة سوى مراجع الرموز الضعيفة؛ ولا يوجد ما يسمى بمرجع مكتبة ضعيف. على سبيل المثال، إذا كان عمر "minSdkVersion" 24 عامًا، يمكنك ربط libvulkan.so وإجراء مكالمة محمية إلى vkBindBufferMemory2، لأنّ "libvulkan.so" متوفّر على الأجهزة التي تبدأ بالواجهة برمجة التطبيقات 24. في المقابل، إذا كان عمر minSdkVersion هو 23 عامًا، يجب الرجوع إلى dlopen وdlsym لأن المكتبة لن تكون متوفّرة على الجهاز الذي يتوافق مع واجهة برمجة التطبيقات 23 فقط. لا نعرف حلاً جيدًا لإصلاح هذه الحالة، ولكن على المدى الطويل سيحل تلقائيًا لأننا (متى أمكن) لم نعد نسمح لواجهات برمجة التطبيقات الجديدة بإنشاء مكتبات جديدة.

لمؤلفي المكتبة

إذا كنت تقوم بتطوير مكتبة لاستخدامها في تطبيقات Android، فيجب عليك تجنب استخدام هذه الميزة في عناوينك العامة. ويمكن استخدام هذه الميزة بشكل آمن مع إدراج رموز غير صالحة، ولكن إذا اعتمدت على __builtin_available في أي رمز مضمَّن في العناوين، مثل الدوال المضمَّنة أو تعريفات النماذج، سيكون عليك فرض تفعيل هذه الميزة على جميع العملاء. للأسباب نفسها التي تمنعنا من تفعيل هذه الميزة تلقائيًا في NDK، يجب أن تتجنّب إجراء هذا الاختيار نيابةً عن المستهلكين.

إذا كان هذا الإجراء مطلوبًا في العناوين العامة، فتأكد من توثيق ذلك حتى يعرف كلا المستخدمين أنهم سيحتاجون إلى تمكين الميزة وعلى دراية بمخاطر إجراء ذلك.