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

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

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

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

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

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

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

إنشاء تصميمات الإعلانات

مرِّر -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، يجب أن تتجنّب إجراء هذا الاختيار نيابةً عن المستهلكين.

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