نصائح من "مبادرة أخبار Google"

JNI هي الواجهة الأصلية لـ Java. وتحدّد هذه السياسة طريقة لرمز البايت الذي يجمعه Android من رمز برمجي مُدار (مكتوب بلغات برمجة Java أو Kotlin) للتفاعل مع الرموز البرمجية الأصلية (المكتوبة بلغة C/C++ ). ويُعدّ JNI محايدًا لدى البائع ولديها دعم لتحميل الرمز من المكتبات المشتركة الديناميكية، وهو مرهق في بعض الأحيان يكون فعّالاً في بعض الأحيان.

ملاحظة: بما أنّ Android يحوّل لغة Kotlin إلى رمز بايت متوافق مع ART بطريقة مشابهة للغة البرمجة Java، يمكنك تطبيق الإرشادات الواردة في هذه الصفحة على كلّ من لغتَي البرمجة Kotlin وJava من حيث بنية JNI والتكاليف المرتبطة بها. لمعرفة المزيد من المعلومات، يمكنك الاطّلاع على Kotlin وAndroid.

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

لتصفّح مراجع JNI العالمية ومعرفة مكان إنشاء مراجع JNI العامة وحذفها، استخدِم عرض كومة JNI في Memory Profiler في Android Studio 3.2 والإصدارات الأحدث.

نصائح عامة

جرِّب تقليل تأثير طبقة JNI. ثمة عوامل متعددة يجب أخذها في الاعتبار هنا. يجب أن يحاول حلّ JNI أن يتّبع هذه الإرشادات (المُدرَجة أدناه حسب ترتيب الأهمية، بدءًا من الأكثر أهمية):

  • الحدّ من تنظيم الموارد في طبقة "مبادرة أخبار Google" (JNI). إنّ تقسيم طبقة JNI يؤدي إلى تكاليف غير بسيطة. حاوِل تصميم واجهة تقلِّل من كمية البيانات التي تحتاج إلى تنظيمها ومعدّل تكرار تنظيم البيانات.
  • تجنَّب الاتصال غير المتزامن بين الرمز المكتوب بلغة برمجة مُدارة ورمز مكتوب بلغة C++ متى أمكن ذلك. سيؤدي ذلك إلى تسهيل صيانة واجهة JNI. يمكنك عادةً تبسيط التحديثات غير المتزامنة لواجهة المستخدم من خلال إبقاء التحديث غير المتزامن بلغة واجهة المستخدم نفسها. على سبيل المثال، بدلاً من استدعاء دالة C++ من سلسلة واجهة المستخدم في رمز Java من خلال JNI، من الأفضل إجراء معاودة الاتصال بين سلسلتَين بلغة البرمجة Java بحيث تُجري إحداهما طلب حظر C++ للحظر ثم تُرسِل إشعارًا إلى سلسلة محادثات واجهة المستخدم عند اكتمال طلب الحظر.
  • قلِّل عدد سلاسل المحادثات التي تحتاج إلى لمسها أو لمسها من خلال \"مبادرة أخبار Google\". إذا كنت بحاجة إلى استخدام مجموعات سلاسل المحادثات بلغتَي Java وC++ ، حاوِل إبقاء اتصال JNI بين مالكي المجموعة بدلاً من سلاسل المحادثات الفردية.
  • يمكنك الاحتفاظ برمز الواجهة في عدد قليل من مواقع مصادر C++ وJava التي يمكن تحديدها بسهولة لتسهيل عمليات إعادة الهيكلة المستقبلية. ننصحك باستخدام مكتبة الإنشاء التلقائي لمبادرة JNI حسبما تقتضي الحاجة.

JavaVM وJNIEnv

تُعرّف JNI بنيتين رئيسيتين للبيانات، وهما "JavaVM" و "JNIEnv". كلاهما يشير في الأساس إلى مؤشرات لجداول الدوال. (في إصدار C++، تكون عبارة عن فئات بها مؤشر إلى جدول دالة ودالة عضو لكل دالة JNI تقوم بتوجيه من خلال الجدول). توفر JavaVM وظائف "واجهة الاستدعاء"، التي تتيح لك إنشاء جهاز JavaVM وإتلافه. نظريًا، يمكن أن يكون لديك عدة أجهزة JavaVM لكل عملية، لكن Android يسمح بواحدة فقط.

توفر JNIEnv معظم دوال JNI. تتلقّى جميع الدوال الأصلية دالة JNIEnv كوسيطة أولى، باستثناء طُرق @CriticalNative. يمكنك الاطّلاع على طلبات بحث مدمجة أسرع.

يُستخدم JNIEnv للتخزين على المستوى المحلي لسلسلة المحادثات. لهذا السبب، لا يمكنك مشاركة JNIEnv بين سلاسل المحادثات. إذا لم تتوفّر طريقة أخرى في أحد الرموز للحصول على JNIEnv، عليك مشاركة JavaVM واستخدام GetEnv لاكتشاف JNIEnv الخاصة بسلسلة المحادثات. (لنفترض أنّ هناك موقعًا إلكترونيًا، راجِع AttachCurrentThread أدناه.)

تختلف إعلانات C لـ JNIEnv وJavaVM عن إعلانات C++. يوفر ملف التضمين "jni.h" تحديدات طباعيّة مختلفة بناءً على ما إذا كان مُضمّنًا في C أو C++. لهذا السبب، من غير المُجدي تضمين وسيطات JNIEnv في ملفات العناوين المضمّنة باللغتين. (ضع طريقة أخرى: إذا كان ملف العنوان يتطلب #ifdef __cplusplus، قد تحتاج إلى إجراء بعض الإجراءات الإضافية إذا كان أي شيء في ذلك العنوان يشير إلى JNIEnv).

Threads

جميع سلاسل المحادثات هي سلاسل في نظام التشغيل Linux ومجدولة بواسطة النواة (kernel). ويتم عادةً بدؤها من خلال الرمز المُدار (باستخدام Thread.start())، ولكن يمكن أيضًا إنشاؤها في مكان آخر ثم إرفاقها بـ JavaVM. على سبيل المثال، يمكن إرفاق سلسلة محادثات تبدأ بـ pthread_create() أو std::thread باستخدام الدالتَين AttachCurrentThread() أو AttachCurrentThreadAsDaemon(). وإلى أن يتم إرفاق سلسلة التعليمات، لا تحتوي السلسلة على JNIEnv، ولا يمكنها إجراء طلبات JNI.

من الأفضل عادةً استخدام Thread.start() لإنشاء أي سلسلة محادثات تحتاج إلى استدعاء رمز Java. سيؤدي هذا الإجراء إلى التأكّد من توفّر مساحة كافية في تكديس، ومن أنك تستخدم ThreadGroup الصحيح، وأنك تستخدم ClassLoader نفسه مثل رمز Java. ومن الأسهل أيضًا ضبط اسم سلسلة المحادثات لتصحيح الأخطاء في Java بدلاً من ضبطها في الرموز البرمجية الأصلية (راجِع pthread_setname_np() إذا كان لديك pthread_t أو thread_t، وstd::thread::native_handle() إذا كان لديك std::thread وتريد pthread_t).

يؤدي إرفاق سلسلة تعليمات تم إنشاؤها محليًا إلى إنشاء كائن java.lang.Thread وإضافته إلى ThreadGroup "الرئيسي"، ما يجعله مرئيًا لبرنامج تصحيح الأخطاء. إنّ طلب الرقم AttachCurrentThread() على سلسلة محادثات سبق أن تم إرفاقه هو أمر غير مجدي.

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

سلاسل المحادثات المرفقة من خلال JNI يجب الاتصال بـ DetachCurrentThread() قبل الخروج. إذا كان الترميز مباشرةً أمرًا غير ملائم، في نظام Android 2.0 (Eclair) والإصدارات الأحدث، يمكنك استخدام pthread_key_create() لتحديد دالة تدميرية سيتم استدعاؤها قبل خروج سلسلة التعليمات، وطلب DetachCurrentThread() منها. (استخدِم هذا المفتاح مع pthread_setspecific() لتخزين JNIEnv في thread-local-storage، بهذه الطريقة سيتم تمريره إلى المُدمِّر كوسيطة.)

jclass وjmethodID وjfieldID

إذا كنت تريد الوصول إلى حقل كائن من خلال رمز برمجي أصلي، يمكنك إجراء ما يلي:

  • الحصول على مرجع عنصر الفئة للفئة باستخدام FindClass
  • الحصول على رقم تعريف الحقل من خلال GetFieldID
  • احصل على محتوى الحقل بعبارة مناسبة، مثل GetIntField

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

إذا كان الأداء مهمًا، فمن المفيد البحث عن القيم مرة واحدة وتخزين النتائج مؤقتًا في التعليمات البرمجية الأصلية. نظرًا لوجود حد واحد JavaVM لكل عملية، فمن المعقول تخزين هذه البيانات في هيكل محلي ثابت.

تكون مراجع الفئة ومعرّفات الحقول ومعرّفات الطُرق صالحة مضمونة إلى أن يتم إلغاء تحميل الفئة. ولا يتم إلغاء تحميل الصفوف إلا إذا كان من الممكن جمع بيانات غير مرغوب فيها في جميع الفئات المرتبطة بـ ClassLoader، وهو أمر نادر الحدوث ولكنه لن يكون مستحيلاً في Android. مع ذلك، يُرجى العِلم أنّ jclass هو مرجع فئة ويجب حمايته من خلال دعوة إلى NewGlobalRef (راجِع القسم التالي).

إذا كنت ترغب في تخزين المعرفات مؤقتًا عند تحميل فئة، وإعادة تخزينها مؤقتًا تلقائيًا إذا تم إلغاء تحميل الفئة وإعادة تحميلها، فالطريقة الصحيحة لتهيئة المعرفات هي إضافة جزء من التعليمة البرمجية يبدو مثل هذا إلى الفئة المناسبة:

Kotlin

companion object {
    /*
     * We use a static class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private external fun nativeInit()

    init {
        nativeInit()
    }
}

Java

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

أنشئ طريقة nativeClassInit في رمز C/C++ الذي يُجري عمليات البحث عن المعرّف. سيتم تنفيذ التعليمات البرمجية مرة واحدة، عند تهيئة الفئة. إذا تم إلغاء تحميل الفئة ثم إعادة تحميلها، فسيتم تنفيذها مرة أخرى.

المراجع المحلية والعالمية

كل وسيطة يتم تمريرها إلى طريقة أصلية، وكل كائن تقريبًا تعرضه دالة JNI يكون "مرجعًا محليًا". هذا يعني أنها صالحة لمدة الطريقة الأصلية الحالية في سلسلة التعليمات الحالية. حتى إذا استمر الكائن نفسه بعد عرض الطريقة الأصلية، يكون المرجع غير صالح.

ينطبق ذلك على جميع فئات jobject الفرعية، بما في ذلك jclass وjstring وjarray. (سيحذرك وقت التشغيل بشأن معظم حالات إساءة استخدام المراجع عند تفعيل عمليات فحص JNI الإضافية).

الطريقة الوحيدة للحصول على مراجع غير محلية هي من خلال الدالتَين NewGlobalRef وNewWeakGlobalRef.

إذا أردت الاحتفاظ بمرجع لفترة أطول، يجب استخدام مرجع "عالمي". تستخدم الدالة NewGlobalRef المرجع المحلي كوسيطة وتعرض وسيطة عمومية. من ضمان صلاحية المرجع العالمي إلى أن يتم طلب الرمز DeleteGlobalRef.

يشيع استخدام هذا النمط عند التخزين المؤقت لفئة jclass التي تم إرجاعها من FindClass، على سبيل المثال:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

تقبل جميع طرق JNI كلاً من المراجع المحلية والعامة كوسيطات. من الممكن أن يكون للمراجع التي تؤدي إلى الكائن نفسه قيم مختلفة. على سبيل المثال، قد تختلف القيم التي تعرضها الاستدعاءات المتتالية لـ NewGlobalRef في الكائن نفسه. لمعرفة ما إذا كان مرجعان يشيران إلى الكائن نفسه، عليك استخدام الدالة IsSameObject. لا تقارن أبدًا المراجع باستخدام == في الرموز البرمجية الأصلية.

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

يُطلَب من المبرمجين عدم توزيع المراجع المحلية بشكل مفرط. من الناحية العملية، يعني ذلك أنّه في حال إنشاء أعداد كبيرة من المراجع المحلية، ربما أثناء تشغيل مصفوفة من العناصر، يجب تحريرها يدويًا باستخدام DeleteLocalRef بدلاً من السماح لـ JNI بتنفيذ ذلك نيابةً عنك. لا يُطلب تنفيذ الإجراء إلا لحجز خانات لـ 16 مرجعًا محليًا، وإذا كنت بحاجة إلى أكثر من ذلك، عليك إمّا حذفه أثناء التنقّل أو استخدام EnsureLocalCapacity/PushLocalFrame لحجز المزيد.

يُرجى العِلم أنّ النوعَين jfieldID وjmethodID هما نوعان غير شفافين، وليسا مراجعين لكائنات، ويجب عدم تمريرها إلى NewGlobalRef. مؤشرات البيانات الأولية التي تعرضها دوال مثل GetStringUTFChars وGetByteArrayElements ليست كائنات أيضًا. (قد يتم تمريرها بين سلاسل المحادثات، وتكون صالحة حتى استدعاء الإصدار المطابق).

هناك حالة واحدة غير عادية تستحق الإشارة إليها بشكل منفصل. في حال إرفاق سلسلة محادثات أصلية باستخدام AttachCurrentThread، لن يؤدي الرمز الذي تستخدمه إلى إخلاء المراجع المحلية تلقائيًا إلى أن يتم فصل سلسلة المحادثات. يجب حذف أي مراجع محلية تنشئها يدويًا. بشكل عام، من المحتمل أن يحتاج أي رمز برمجي أصلي ينشئ مراجع محلية في حلقة التكرار إلى إجراء بعض الحذف اليدوي.

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

سلاسل UTF-8 وUTF-16

تستخدم لغة البرمجة Java UTF-16. لتسهيل الأمر، توفر JNI طرقًا تعمل أيضًا مع UTF-8 المعدَّل. ويكون الترميز المعدّل مفيدًا للرمز C لأنه يشفّر \u0000 بالشكل 0xc0 0x80 بدلاً من 0x00. الشيء الجميل في هذا هو أنه يمكنك الاعتماد على وجود سلاسل بدون إنهاء بنمط C، ومناسبة للاستخدام مع دوال سلسلة libc القياسية. من جهة أخرى، لا يمكنك تمرير بيانات UTF-8 عشوائية إلى JNI وتوقع أن تعمل بشكل صحيح.

للحصول على تمثيل UTF-16 للسمة String، استخدِم GetStringChars. تجدر الإشارة إلى أنّ سلاسل UTF-16 لا يتم إنهاؤها بشكلٍ صفري، ويُسمح باستخدام \u0000، لذا عليك التمسك بطول السلسلة ومؤشر jchar.

لا تنسَ Release السلاسل التي Get. تعرض دوال السلسلة jchar* أو jbyte*، وهي عبارة عن مؤشرات بالنمط C إلى البيانات الأساسية بدلاً من المراجع المحلية. وتكون هذه العناوين صالحة إلى أن يتم استدعاء Release، ما يعني أنّه لن يتم إصدارها عند عودة الطريقة الأصلية.

يجب أن تكون البيانات التي يتم تمريرها إلى NewStringUTF بتنسيق UTF-8 المعدَّل. وهناك خطأ شائع هو قراءة بيانات الأحرف من ملف أو مصدر بيانات على شبكة وتسليمها إلى NewStringUTF بدون فلترتها. يجب إزالة الأحرف غير الصالحة أو تحويلها إلى نموذج UTF-8 المعدَّل المناسب ما لم تكن البيانات صالحة بتنسيق MUTF-8 (أو ASCII 7 بت، وهو مجموعة فرعية متوافقة). وإذا لم تفعل ذلك، من المحتمل أن تقدم الإحالة الناجحة UTF-16 نتائج غير متوقعة. تفحص أداة CheckJNI المفعَّلة تلقائيًا للمحاكيات السلاسل وتُلغي الجهاز الافتراضي (VM) في حال تلقّي بيانات إدخال غير صالحة.

قبل استخدام Android 8، كان من الأسرع استخدام سلاسل UTF-16 لأنّ Android لم يكن يتطلب الحصول على نسخة منها باستخدام GetStringChars، في حين أنّ GetStringUTFChars يتطلب تخصيصًا وتحويلًا إلى UTF-8. غيَّر Android 8 تمثيل String لاستخدام 8 بت لكل حرف لسلاسل ASCII (لتوفير الذاكرة) وبدأ في استخدام أداة نقل البيانات المهملة. تقلل هذه الميزات بشكل كبير من عدد الحالات التي يمكن فيها لـ ART توفير مؤشر إلى بيانات String بدون إنشاء نسخة، حتى مع GetStringCritical. ومع ذلك، إذا كانت معظم السلاسل التي تمت معالجتها باستخدام الرمز قصيرة، من الممكن في معظم الحالات تجنُّب التخصيص وموضع الصفقة في معظم الحالات عن طريق استخدام مخزن احتياطي مخصّص للتكديس وGetStringRegion أو GetStringUTFRegion. مثلاً:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

الصفائف الأولية

توفر JNI دوال للوصول إلى محتويات كائنات الصفيفة. بينما يجب الوصول إلى صفائف الكائنات على إدخال واحد في كل مرة، يمكن قراءة صفائف العناصر الأولية وكتابتها مباشرةً كما لو تم تعريفها في C.

لجعل الواجهة فعّالة قدر الإمكان بدون فرض قيود على تنفيذ الجهاز الافتراضي، تسمح مجموعة طلبات Get<PrimitiveType>ArrayElements لوقت التشغيل بعرض مؤشر إلى العناصر الفعلية أو تخصيص بعض الذاكرة وإنشاء نسخة. وفي كلتا الحالتين، نضمن أن يكون المؤشر الأولي الذي يتم عرضه صالحًا إلى أن يتم إصدار استدعاء Release المقابل (ما يعني أنّه في حال عدم نسخ البيانات، سيتم تثبيت كائن الصفيف ولا يمكن تغيير موقعه كجزء من ضغط كومة الذاكرة المؤقتة). يجب Release كل مصفوفةGet. أيضًا، إذا تعذّر استدعاء Get، يجب التأكّد من أنّ الرمز الخاص بك لا يحاول Release كقيمة فارغة لاحقًا.

يمكنك تحديد ما إذا تم نسخ البيانات أم لا من خلال تمرير مؤشر قيمة غير فارغة للوسيطة isCopy. نادرًا ما يكون هذا مفيدًا.

يستخدم الاستدعاء Release الوسيطة mode التي يمكن أن تحتوي على واحدة من ثلاث قيم. تعتمد الإجراءات التي يتم تنفيذها من خلال وقت التشغيل على ما إذا كان قد تم عرض مؤشر إلى البيانات الفعلية أو نسخة منها:

  • 0
    • الفعلي: تم إلغاء تثبيت كائن الصفيف.
    • النسخ: يتم نسخ البيانات مرة أخرى. يتم تحرير المخزن المؤقت مع النسخة.
  • JNI_COMMIT
    • الفعلي: لا يفعل أي شيء.
    • النسخ: يتم نسخ البيانات مرة أخرى. لا يتم إخلاء مساحة التخزين المؤقت الذي يحتوي على النسخة.
  • JNI_ABORT
    • الفعلي: تم إلغاء تثبيت كائن الصفيف. لا يتم إلغاء عمليات الكتابة السابقة.
    • النسخ: يتم تحرير المخزن المؤقت الذي يحتوي على النسخة، وسيتم فقد أي تغييرات تم إجراؤها عليها.

أحد أسباب التحقّق من علامة isCopy هو معرفة ما إذا كنت بحاجة إلى استدعاء Release بـ JNI_COMMIT بعد إجراء تغييرات على صفيف. وإذا كنت تتناوب بين إجراء تغييرات وتنفيذ رمز يستخدم محتوى المصفوفة، قد تتمكن من تخطّي تنفيذ عدم التشغيل. هناك سبب آخر محتمل للتحقّق من العلامة هو المعالجة الفعّالة مع JNI_ABORT. على سبيل المثال، قد ترغب في الحصول على صفيف، وتعديله في مكانه، وتمرير القطع إلى دوال أخرى، ثم تجاهل التغييرات. إذا كنت تعلم أنّ مبادرة JNI تعمل على إنشاء نسخة جديدة لك، فلا داعي لإنشاء نسخة أخرى "قابلة للتعديل". إذا قدّمَت مؤسسة JNI الرابط الأصلي لك، عليك إنشاء نسختك الخاصة.

من الأخطاء الشائعة (تكراره في نموذج الرمز) افتراض أنّه يمكنك تخطّي استدعاء Release إذا كان *isCopy خطأ. ولكن الأمر ليس كذلك. إذا لم يتم تخصيص المخزن المؤقت للنسخ، فيجب تثبيت الذاكرة الأصلية لأسفل ولا يمكن نقلها من خلال مجمّع البيانات المهملة.

تجدر الإشارة أيضًا إلى أنّ العلامة JNI_COMMIT لا تؤدي إلى إطلاق الصفيف، وستحتاج في النهاية إلى استدعاء الدالة Release مرة أخرى باستخدام علامة مختلفة.

مكالمات المنطقة

هناك بديل للمكالمات مثل Get<Type>ArrayElements وGetStringChars قد يكون مفيدًا جدًا عندما يكون كل ما عليك فعله هو نسخ البيانات داخلها أو خارجها. فكِّر في النقاط التالية:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

يستخرج ذلك الصفيف، وينسخ عناصر len البايت الأولى منه، ثم يؤدي إلى إطلاق الصفيف. بناءً على طريقة التنفيذ، سيتم إما تثبيت محتوى المصفوفة أو نسخه من خلال طلب Get. ينسخ الرمز البيانات (لمرة ثانية تقريبًا)، ثم يستدعي Release. في هذه الحالة، JNI_ABORT يضمن عدم وجود فرصة للحصول على نسخة ثالثة.

يمكن للمرء إنجاز الشيء نفسه بشكل أكثر بساطة:

    env->GetByteArrayRegion(array, 0, len, buffer);

وهناك العديد من المزايا:

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

وبالمثل، يمكنك استخدام الاستدعاء Set<Type>ArrayRegion لنسخ البيانات إلى مصفوفة، وGetStringRegion أو GetStringUTFRegion لنسخ الأحرف من String.

الاستثناءات

يجب عدم استدعاء معظم دوال JNI عندما يكون هناك استثناء في انتظار المراجعة. من المتوقّع أن يلاحظ الرمز الاستثناء (من خلال القيمة المعروضة للدالة أو ExceptionCheck أو ExceptionOccurred) ثم يعرضه، أو يمحو الاستثناء والتعامل معه.

دوال JNI الوحيدة التي يُسمح لك بالاتصال بها أثناء وجود استثناء ما هي:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

يمكن أن تؤدي العديد من مكالمات JNI إلى ظهور استثناء، ولكن غالبًا ما توفر طريقة أسهل للتحقق من الإخفاق. على سبيل المثال، إذا كان NewString يعرض قيمة غير فارغة (NULL)، فلن تحتاج إلى التحقق من وجود استثناء. ومع ذلك، إذا استدعيت طريقة (باستخدام دالة مثل CallObjectMethod)، يجب عليك دائمًا البحث عن استثناء لأنّ القيمة المعروضة لن تكون صالحة إذا تم طرح استثناء.

تجدر الإشارة إلى أنّ الاستثناءات التي يتم إجراؤها من خلال التعليمات البرمجية المُدارة لا تساعد على تخفيف إطارات المكدس الأصلي. (واستثناءات C++ التي لا يُنصح باستخدامها بشكل عام على Android، يجب ألّا يتم تجاوز حدود انتقال JNI من رمز C++ إلى الرمز المُدار). تم ضبط مؤشر استثناء في سلسلة التعليمات الحالية من خلال تعليمات JNI Throw وThrowNew. وعند العودة إلى الإدارة من خلال الرموز البرمجية الأصلية، ستتم ملاحظة الاستثناء والتعامل معه بشكلٍ مناسب.

ويمكن للرمز الأصلي "التقاط" استثناء من خلال طلب ExceptionCheck أو ExceptionOccurred، ومحوه باستخدام ExceptionClear. كالعادة، يمكن أن يؤدي تجاهل الاستثناءات بدون التعامل معها إلى حدوث مشاكل.

لا تتوفّر دوال مضمّنة لمعالجة العنصر Throwable بنفسه، لذا إذا أردت (على سبيل المثال) الحصول على سلسلة الاستثناء، عليك العثور على الفئة Throwable والبحث عن رقم تعريف الطريقة getMessage "()Ljava/lang/String;" واستدعاءها. وإذا كانت النتيجة غير فارغة (NULL)، استخدِم GetStringUTFChars للحصول على عنصر يمكنك تقديمه إلى printf(3) أو ما يعادله.

فحص ممتد

تُجري JNI عملية تحقق قليلة من الأخطاء. تؤدي الأخطاء عادةً إلى حدوث أعطال. يوفّر Android أيضًا وضعًا يسمّى CheckJNI، حيث يتم تبديل مؤشرات جدول الدوالّ JavaVM وJNIEnv إلى جداول الدوال التي تنفِّذ سلسلة موسّعة من عمليات التحقّق قبل طلب طريقة التنفيذ العادية.

وتشمل عمليات التحقّق الإضافية ما يلي:

  • الصفائف: محاولة تخصيص صفيف بحجم سالب.
  • مؤشرات غير صالحة: تمرير jarray/jclass/jobject/jstring غير صالح إلى استدعاء JNI، أو تمرير مؤشر NULL إلى استدعاء JNI باستخدام وسيطة غير قابلة للقيم.
  • أسماء الفئات: تمرير أي شيء باستثناء النمط "java/lang/String" لاسم الفئة إلى استدعاء JNI.
  • المكالمات المهمة: إجراء اتصال JNI بين نقطة "حرجة" والإصدار المقابل لها.
  • Direct ByteBuffers: تمرير وسيطات غير صالحة إلى NewDirectByteBuffer
  • الاستثناءات: إجراء مكالمة JNI عندما يكون هناك استثناء في انتظار المراجعة.
  • JNIEnv*s: استخدام JNIEnv* من سلسلة التعليمات الخاطئة
  • jfieldIDs: استخدام NULL jfieldID، أو استخدام jfieldID لضبط قيمة من النوع الخطأ (محاولة تعيين StringBuilder إلى حقل سلسلة على سبيل المثال)، أو استخدام jfieldID لحقل ثابت لضبط حقل مثيل أو العكس، أو استخدام jfieldID من فئة معيّنة مع مثيلات من فئة أخرى.
  • jmethodIDs: استخدام النوع الخاطئ من jmethodID عند إجراء استدعاء JNI Call*Method: نوع عرض غير صحيح، أو عدم تطابق ثابت/غير ثابت، أو نوع خاطئ لـ "this" (للاستدعاءات غير الثابتة) أو فئة خاطئة (للاستدعاءات الثابتة).
  • المراجع: استخدام DeleteGlobalRef/DeleteLocalRef في نوع المراجع غير الصحيح
  • أوضاع الإصدار: تمرير وضع إصدار سيئ إلى مكالمة إصدار (شيء ما غير 0 أو JNI_ABORT أو JNI_COMMIT)
  • أمان الكتابة: عرض نوع غير متوافق من طريقتك الأصلية (عرض StringBuilder من طريقة تم تعريفها لعرض سلسلة على سبيل المثال).
  • UTF-8: تمرير تسلسل بايت UTF-8 المعدَّل غير صالح إلى استدعاء JNI.

(لم يتم وضع علامة في المربّع بجانب إمكانية الوصول إلى الأساليب والحقول: لا تسري قيود الوصول على الرموز البرمجية الأصلية).

هناك العديد من الطرق لتفعيل CheckJNI.

إذا كنت تستخدم المحاكي، يتم تشغيل CheckJNI افتراضيًا.

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

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

وفي أي من هاتين الحالتين، سيظهر لك شيء مثل هذا في إخراج Logcat عند بدء وقت التشغيل:

D AndroidRuntime: CheckJNI is ON

إذا كان لديك جهاز عادي، يمكنك استخدام الأمر التالي:

adb shell setprop debug.checkjni 1

لن يؤثر ذلك في التطبيقات قيد التشغيل، ولكن سيتم تفعيل CheckJNI في أي تطبيق يتم تشغيله من ذلك الوقت فصاعدًا. (تغيير الموقع إلى أي قيمة أخرى أو إعادة التشغيل ببساطة ستؤدي إلى إيقاف CheckJNI مرة أخرى). في هذه الحالة، سيظهر لك شيء مثل هذا في مخرجات Logcat في المرة التالية التي يبدأ فيها أحد التطبيقات:

D Late-enabling CheckJNI

يمكنك أيضًا ضبط السمة android:debuggable في ملف بيان التطبيق لتفعيل CheckJNI فقط لتطبيقك. يُرجى العلم أنّ أدوات إصدار Android ستنفّذ ذلك تلقائيًا لأنواع معيّنة من الإصدارات.

المكتبات الأصلية

يمكنك تحميل رموز برمجية أصلية من المكتبات المشتركة باستخدام الترميز System.loadLibrary العادي.

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

استدعِ System.loadLibrary (أو ReLinker.loadLibrary) من مهيئٍ لفئة ثابتة. الوسيطة هي اسم المكتبة "غير المزخرف"، لذلك لتحميل libfubar.so سيتم تمرير "fubar".

إذا كان لديك فئة واحدة فقط تتضمّن طُرقًا أصلية، من المنطقي أن يتم إدراج الطلب System.loadLibrary في مهيِّئ ثابت لتلك الفئة. بخلاف ذلك، قد يكون من الضروري إجراء المكالمة من Application حتى تعرف أنّه يتم دائمًا تحميل المكتبة وتحميلها في وقت مبكر.

هناك طريقتان يمكن لوقت التشغيل العثور من خلالهما على الطرق الأصلية. يمكنك تسجيلها بشكل صريح باستخدام RegisterNatives أو السماح بوقت التشغيل بالبحث عنها بشكل ديناميكي باستخدام dlsym. من مزايا RegisterNatives أنّه يمكنك التحقّق مسبقًا من توفّر الرموز، بالإضافة إلى إمكانية الحصول على مكتبات مشتركة أصغر وأسرع من خلال عدم تصدير أي بيانات باستثناء JNI_OnLoad. تتمثل ميزة السماح لوقت التشغيل باكتشاف الدوال هي أنها تعليمات برمجية أقل قليلاً في الكتابة.

لاستخدام RegisterNatives:

  • قدِّم دالة JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved).
  • في JNI_OnLoad، سجِّل جميع الطرق الأصلية باستخدام RegisterNatives.
  • يمكنك الإنشاء باستخدام -fvisibility=hidden بحيث يتم تصدير JNI_OnLoad فقط من مكتبتك. وينتج عن ذلك رمز أسرع وأصغر، ويجنِّب الاصطدامات المحتملة مع المكتبات الأخرى التي يتم تحميلها في تطبيقك (ولكنه يؤدي إلى إنشاء عمليات تتبُّع تسلسل استدعاء الدوال البرمجية أقل فائدة في حال تعطُّل التطبيق عند استخدام الرمز الأصلي).

يجب أن يبدو المهيئ الثابت على النحو التالي:

Kotlin

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Java

static {
    System.loadLibrary("fubar");
}

يجب أن تبدو الدالة JNI_OnLoad على هذا النحو إذا كانت مكتوبة بلغة C++:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

ولاستخدام "اكتشاف" الطرق الأصلية بدلاً من ذلك، عليك تسميتها بطريقة محددة (راجع مواصفات JNI للحصول على التفاصيل). وهذا يعني أنّه إذا كان توقيع الطريقة خاطئًا، لن تعرِف عنه إلا بعد استدعاء الطريقة لأول مرّة.

إنّ أي استدعاءات FindClass يتم إجراؤها من JNI_OnLoad ستحل الفئات في سياق أداة تحميل الفئة الذي تم استخدامه لتحميل المكتبة المشتركة. عند استدعاء هذه الدالة من سياقات أخرى، تستخدم FindClass أداة تحميل الفئة المرتبطة بالطريقة الموجودة أعلى حزمة Java، أو إذا لم تتوفر واحدة (لأن الاستدعاء مصدره سلسلة تعليمات أصلية تم إرفاقها للتو)، فهي تستخدم أداة تحميل الفئة "system" (نظام التشغيل). لا يعرف برنامج تحميل فئة النظام فئات تطبيقك، لذا لن تتمكن من البحث عن فئاتك الخاصة باستخدام FindClass في هذا السياق. هذا يجعل JNI_OnLoad مكانًا مناسبًا للبحث عن الفئات وتخزينها مؤقتًا: بمجرد أن يكون لديك مرجع عام صالح للjclass، يمكنك استخدامه من أي سلسلة محادثات مرفقة.

مكالمات مدمجة مع المحتوى بشكل أسرع باستخدام @FastNative و@CriticalNative

يمكن إضافة تعليقات توضيحية إلى الطُرق الأصلية باستخدام @FastNative أو @CriticalNative (وليس كليهما) لتسريع عمليات الانتقال بين الرموز البرمجية المُدارة والرموز الأصلية. ومع ذلك، تأتي هذه التعليقات التوضيحية مع بعض التغييرات في السلوك التي يجب مراعاتها بعناية قبل الاستخدام. بينما نذكر بإيجاز هذه التغييرات أدناه، يرجى الرجوع إلى الوثائق للحصول على التفاصيل.

لا يمكن تطبيق التعليق التوضيحي @CriticalNative إلا على الطرق الأصلية التي لا تستخدم العناصر المُدارة (في المعلَمات أو القيم المعروضة، أو باعتبارها this ضمنية)، وهذا التعليق التوضيحي يغيّر واجهة ABI الخاصة بانتقال JNI. يجب أن يستبعد التنفيذ الأصلي المَعلمتَين JNIEnv وjclass من توقيع الدالة.

أثناء تنفيذ طريقة @FastNative أو @CriticalNative، لا يمكن لمجموعة البيانات المهملة تعليق سلسلة التعليمات للعمل الأساسي وقد يتم حظرها. لا تستخدم هذه التعليقات التوضيحية للأساليب طويلة الأمد، بما في ذلك الطرق السريعة عادةً ولكنها غير محدودة بشكل عام. وعلى وجه الخصوص، يجب ألا يؤدي الرمز البرمجي عمليات إدخال/إخراج مهمة أو يكتسب أقفالاً أصلية يمكن الاحتفاظ بها لفترة طويلة.

تم تنفيذ هذه التعليقات التوضيحية لاستخدام النظام منذ Android 8 وأصبحت واجهة برمجة تطبيقات عامة تم اختبارها من خلال CTS في الإصدار Android 14. من المرجَّح أن تعمل هذه التحسينات أيضًا على الأجهزة التي تعمل بالإصدار 8 إلى 13 من نظام التشغيل Android (وبدون ضمانات CTS القوية)، إلا أنّ البحث الديناميكي عن الطرق الأصلية غير متاح إلا على الإصدار 12 من نظام Android والإصدارات الأحدث.RegisterNatives ويتم تجاهل هذه التعليقات التوضيحية على نظام التشغيل Android 7، ويؤدي عدم تطابق واجهة التطبيق الثنائية (ABI) مع @CriticalNative إلى تنظيم الوسيطات بشكل خاطئ واحتمالية حدوث أعطال.

بالنسبة إلى الطُرق المهمة بشأن الأداء التي تحتاج إلى هذه التعليقات التوضيحية، ننصح بشدة بالتسجيل السريع للطرق في مؤشر JNI RegisterNatives بدلاً من الاعتماد على أسلوب "الاكتشاف" المستنِد إلى الاسم للأساليب الأصلية. للحصول على أفضل أداء عند بدء تشغيل التطبيق، ننصح بتضمين المتصلين بالطرق @FastNative أو @CriticalNative في الملف الشخصي الأساسي. ومنذ الإصدار 12 من نظام التشغيل Android، تكون تكلفة الطلب إلى طريقة @CriticalNative أصلية من طريقة مجمّعة ومدارة رخيصة مثل أي طلب غير مضمَّن في لغة C/C++ ما دامت جميع الوسيطات مناسبة للسجلات (على سبيل المثال، حتى 8 وسيطات تكامل وما يصل إلى 8 وسيطات نقاط عائمة على Arm64).

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

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

اعتبارات إصدار 64 بت

لإتاحة البُنى الأساسية التي تستخدم مؤشرات الإصدار 64 بت، استخدِم حقل long بدلاً من int عند تخزين مؤشر إلى بنية أصلية في حقل Java.

الميزات غير المتوافقة/التوافق مع الأنظمة القديمة

يتم توفير جميع ميزات JNI 1.6، باستثناء ما يلي:

  • لم يتم تنفيذ DefineClass. ولا يستخدم Android رموز بايت Java أو ملفات الفئة، وبالتالي لن ينجح تمرير بيانات الفئة الثنائية.

للتوافق مع الأنظمة القديمة مع إصدارات Android القديمة، قد تحتاج إلى معرفة ما يلي:

  • البحث الديناميكي للدوال الأصلية

    حتى الإصدار 2.0 من نظام التشغيل Android (Eclair)، لم يتم تحويل الحرف '$' بشكل صحيح إلى " _00024" أثناء البحث عن أسماء الطرق. ولحلّ هذه المشكلة، يجب استخدام تسجيل صريح أو نقل الطرق الأصلية من الفئات الداخلية.

  • فصل سلاسل المحادثات

    حتى الإصدار Android 2.0 (Eclair)، لم يكن من الممكن استخدام دالة إزالة pthread_key_create لتجنُّب فصل سلسلة التعليمات قبل الخروج من التطبيق. (يستخدم وقت التشغيل أيضًا وظيفة تدمير مفتاح pthread، لذا سيكون هناك سباق لمعرفة الخيار الذي يتم استدعاؤه أولاً.)

  • المراجع العالمية الضعيفة

    لم يتم تنفيذ المراجع العامة الضعيفة حتى نظام التشغيل Android 2.2 (Froyo). وسترفض الإصدارات القديمة بشدّة محاولات استخدامها. يمكنك استخدام ثوابت إصدار نظام Android الأساسي لاختبار التوافق.

    قبل الإصدار 4.0 (إصدار آيس كريم ساندويتش)، كان من الممكن نقل المراجع العالمية الضعيفة فقط إلى NewLocalRef وNewGlobalRef وDeleteWeakGlobalRef. (تشجِّع هذه المواصفات بشدّة المبرمجين على إنشاء مراجع صعبة للشبكات العالمية الضعيفة قبل اتّخاذ أي إجراء، لذلك يجب ألّا يكون ذلك مقيدًا على الإطلاق).

    بدءًا من الإصدار 4.0 من نظام التشغيل Android (آيس كريم ساندويتش)، يمكن استخدام المراجع العامة الضعيفة مثل أي مراجع أخرى لمبادرة أخبار JNI.

  • المراجع المحلية

    قبل الإصدار 4.0 (إصدار آيس كريم ساندويتش)، كانت المراجع المحلية عبارة عن مؤشرات مباشرة. أضافت ميزة Ice Cream العناوين غير المباشرة المطلوبة لتوفير خدمات تجميع أفضل للبيانات كي يتم تجميع البيانات المهملة، غير أنّ هذا يعني أنّه لا يمكن رصد الكثير من أخطاء JNI في الإصدارات القديمة. يمكنك الاطّلاع على التغييرات في المراجع المحلية من JNI في ICS للحصول على مزيد من التفاصيل.

    في إصدارات Android التي تسبق Android 8.0، يتم وضع حدّ أقصى معيّن لعدد المراجع المحلية. بدءًا من Android 8.0، يتيح Android استخدام عدد غير محدود من المراجع المحلية.

  • تحديد نوع المرجع باستخدام GetObjectRefType

    حتى الإصدار 4.0 من نظام التشغيل Android (Ice Cream السندويشية)، كان من المستحيل تنفيذ GetObjectRefType بشكل صحيح بسبب استخدام المؤشرات المباشرة (انظر أعلاه). وبدلاً من ذلك، استخدمنا دليلاً إرشاديًا يستعرض في هذا الترتيب جدول المعلومات العامة الضعيف والوسيطات وجدول السكان المحليين. وعندما يعثر على المؤشر المباشر للمرة الأولى، يتم الإبلاغ عن أنّ المرجع الخاص بك كان من النوع الذي كان يفحصه. وهذا يعني على سبيل المثال، أنّك إذا طلبت GetObjectRefType على jclass عالمي كان مماثلاً لدالة jclass التي تم تمريرها كوسيطة ضمنية لطريقتك الأصلية الثابتة، ستحصل على JNILocalRefType بدلاً من JNIGlobalRefType.

  • @FastNative و@CriticalNative

    تم تجاهل هذه التعليقات التوضيحية للتحسين حتى الإصدار 7 من نظام التشغيل Android. سيؤدي عدم تطابق واجهة التطبيق الثنائية (ABI) في @CriticalNative إلى تنظيم الوسيطات بشكل خاطئ واحتمالية حدوث أعطال.

    لم يتم تنفيذ البحث الديناميكي للدوال الأصلية للطريقتَين @FastNative و@CriticalNative في الإصدار 8 إلى 10 من نظام Android ويحتوي على أخطاء معروفة في الإصدار 11. ومن المرجّح أن يؤدي استخدام هذه التحسينات بدون التسجيل الصريح في JNI RegisterNatives إلى حدوث أعطال على الأجهزة التي تعمل بالإصدار 8 إلى 11 من نظام التشغيل Android.

  • رمية "FindClass" لكرة ClassNotFoundException

    للتوافق مع الأنظمة القديمة، يعرض Android السمة ClassNotFoundException بدلاً من NoClassDefFoundError في حال عدم العثور على فئة معيّنة بحلول FindClass. يتوافق هذا السلوك مع واجهة برمجة تطبيقات انعكاس Java Class.forName(name).

سؤال شائع: لماذا أحصل على UnsatisfiedLinkError؟

عند العمل على الرموز البرمجية الأصلية، من الشائع حدوث فشل مثل هذا:

java.lang.UnsatisfiedLinkError: Library foo not found

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

الأسباب الشائعة لظهور استثناءات "لم يتم العثور على المكتبة":

  • المكتبة غير متاحة أو لا يمكن للتطبيق الوصول إليها. يمكنك استخدام adb shell ls -l <path> للتحقّق من توفّر المكتبة وأذوناتها.
  • لم يتم إنشاء المكتبة باستخدام NDK. وقد يؤدي ذلك إلى اعتماديات على وظائف أو مكتبات غير متاحة على الجهاز.

هناك فئة أخرى من حالات تعذُّر UnsatisfiedLinkError تبدو كما يلي:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

سيظهر لك ما يلي في أداة Logcat:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

هذا يعني أنّ وقت التشغيل حاول العثور على طريقة مطابقة ولكن لم تنجح وتشمل بعض الأسباب الشائعة لذلك ما يلي:

  • لا يتم تحميل المكتبة. تحقَّق من ناتج دالة Logcat للرسائل المتعلّقة بتحميل المكتبة.
  • تعذّر العثور على الطريقة بسبب عدم تطابق الاسم أو التوقيع. يحدث هذا عادةً بسبب:
    • بالنسبة إلى البحث الكسول عن الطريقة، تعذّر تعريف دوال C++ باستخدام extern "C" وإذن الوصول المناسب (JNIEXPORT). يُرجى العلم أنّه قبل إصدار Ice Cream، لم تكن وحدة الماكرو JNIEXPORT غير صحيحة، لذا لن يكون من المفيد استخدام وحدة تحكّم GCC جديدة مع jni.h قديم. يمكنك استخدام الترميز arm-eabi-nm للاطّلاع على الرموز كما تظهر في المكتبة، وإذا كانت تبدو مشوهة (أي بطريقة مثل _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass بدلاً من Java_Foo_myfunc)، أو إذا كان نوع الرمز حرف "t" صغير بدلاً من حرف T الكبير، يجب تعديل البيان.
    • بالنسبة إلى التسجيل الصريح، تظهر أخطاء بسيطة عند إدخال توقيع الطريقة. تأكَّد من أنّ ما ترسله إلى مكالمة التسجيل يتطابق مع التوقيع الوارد في ملف السجلّ. يُرجى تذكُّر أنّ الحرف "B" هو byte و"Z" هو boolean. تبدأ مكوّنات اسم الفئة في التوقيعات بالحرف "L"، وتنتهي بـ ";"، واستخدِم "/" لفصل أسماء الحزمة/الفئات، واستخدِم "$" لفصل أسماء الفئة الداخلية (مثل Ljava/util/Map$Entry;).

قد يساعد استخدام javah لإنشاء عناوين JNI تلقائيًا في تجنُّب بعض المشاكل.

الأسئلة الشائعة: لماذا لم يعثر "FindClass" على صفي؟

(تنطبق معظم هذه النصائح أيضًا على حالات تعذُّر العثور على طرق باستخدام GetMethodID أو GetStaticMethodID أو حقول تتضمّن GetFieldID أو GetStaticFieldID).

تأكَّد من أنّ سلسلة اسم الفئة لها التنسيق الصحيح. تبدأ أسماء فئات JNI باسم الحزمة وتفصل بينها بشرطات مائلة، مثل java/lang/String. إذا كنت تبحث عن فئة صفيفة، عليك أن تبدأ بالعدد المناسب من الأقواس المربّعة ويجب أيضًا لف الفئة بـ "L" و';'، وبالتالي تكون الصفيفة الأحادية البعد لـ String هي [Ljava/lang/String;. إذا كنت تبحث عن فئة داخلية، استخدِم "$" بدلاً من ".". بشكل عام، يُعد استخدام javap في ملف .class طريقة جيدة لمعرفة الاسم الداخلي للفئة.

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

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

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

الطريقة الأولى هي Foo.myfunc. يعثر FindClass على الكائن ClassLoader المرتبط بالفئة Foo ويستخدم ذلك.

يؤدي هذا عادةً ما تريد. قد تحدث مشكلة إذا أنشأت سلسلة محادثات بنفسك (ربما عن طريق طلب الرقم pthread_create ثم أرفقتها بـ AttachCurrentThread). والآن لا تتوفر إطارات تكديس من تطبيقك. فإذا استدعيت FindClass من سلسلة التعليمات هذه، فسوف يبدأ JavaVM في برنامج تحميل فئة "system" بدلاً من البرنامج المرتبط بتطبيقك، ولذلك لن تنجح محاولات العثور على فئات خاصة بالتطبيقات.

هناك بضعة طرق للتغلب على هذا الأمر:

  • نفِّذ عمليات بحث FindClass مرة واحدة في JNI_OnLoad، واحتفِظ بمراجع الفئات في ذاكرة التخزين المؤقت لاستخدامها لاحقًا. إنّ أي طلبات FindClass يتم إجراؤها كجزء من تنفيذ JNI_OnLoad ستستخدم أداة تحميل الفئة المرتبطة بالدالة System.loadLibrary (وهي قاعدة خاصة يتم توفيرها لتسهيل إعداد المكتبة). إذا كان رمز التطبيق يؤدي إلى تحميل المكتبة، سيستخدم FindClass أداة تحميل الفئة الصحيحة.
  • أدخِل مثيلاً من الفئة في الدوال التي تحتاج إليها، من خلال تعريف طريقتك الأصلية باستخدام وسيطة Class ثم تمرير Foo.class.
  • يمكنك تخزين مرجعٍ إلى العنصر ClassLoader في مكان يسهل الوصول إليه وإصدار طلبات loadClass مباشرةً. وهذا يتطلب بعض الجهد.

سؤال شائع: كيف يمكنني مشاركة البيانات الأولية باستخدام الرموز البرمجية الأصلية؟

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

يمكنك تخزين البيانات في "byte[]". يتيح ذلك الوصول السريع جدًا من التعليمات البرمجية المُدارة. على الجانب الأصلي، ومع ذلك، لا تضمن أن تتمكن من الوصول إلى البيانات دون الحاجة إلى نسخها. في بعض عمليات التنفيذ، يعرض الترميزان GetByteArrayElements وGetPrimitiveArrayCritical مؤشرات فعلية إلى البيانات الأولية في كومة الذاكرة المؤقتة المُدارة، ولكن في حالات أخرى، يتم تخصيص مورد احتياطي على كومة الذاكرة المؤقتة الأصلية ونسخ البيانات إليها.

البديل هو تخزين البيانات في مخزن بايت مؤقت. يمكن إنشاؤها باستخدام java.nio.ByteBuffer.allocateDirect، أو دالة NewDirectByteBuffer JNI. على عكس مخازن البايت الاحتياطية العادية، لا يتم تخصيص مساحة التخزين على كومة الذاكرة المؤقتة المُدارة، ويمكن الوصول إليها دائمًا مباشرةً من خلال رمز برمجي أصلي (احصل على العنوان من خلال GetDirectBufferAddress). واستنادًا إلى كيفية تنفيذ الوصول المباشر إلى المخزن المؤقت للبايت، يمكن أن يكون الوصول إلى البيانات من خلال الرمز المُدار بطيئًا جدًا.

يعتمد اختيار ما يجب استخدامه على عاملَين:

  1. هل ستتم معظم عمليات الوصول إلى البيانات من خلال رمز مكتوب بلغة Java أو بلغة C/C++ ؟
  2. إذا تم إرسال البيانات في النهاية إلى واجهة برمجة تطبيقات للنظام، بأي شكل يجب أن تكون؟ (على سبيل المثال، إذا تم تمرير البيانات في النهاية إلى دالة تأخذ بايت[]، قد يكون من غير الحكيم إجراء المعالجة في ByteBuffer مباشرة).

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