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

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

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

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

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

نصائح عامة

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

  • يمكنك تقليل عملية تنظيم الموارد في طبقة JNI. تحطيم الكرة لطبقة JNI تكاليف غير بسيطة. حاوِل تصميم واجهة تقلِّل من كمية البيانات التي تحتاج إلى ترتيبها ومعدّل تكرار ترتيب البيانات.
  • تجنُّب الاتصال غير المتزامن بين الرمز البرمجي المكتوب في برمجة مُدارة واللغة والتعليمات البرمجية المكتوبة بلغة C++ إن أمكن. سيؤدي ذلك إلى تسهيل صيانة واجهة JNI. يمكنك عادةً تبسيط تعديلات واجهة المستخدم غير المتزامنة من خلال إبقاء التعديل غير المتزامن باللغة نفسها المستخدَمة في واجهة المستخدم. على سبيل المثال، بدلاً من استدعاء دالة C++ من سلسلة مهام واجهة المستخدم في رمز Java من خلال واجهة JNI، من الأفضل إجراء طلب استدعاء بين سلسلة مهام في لغة البرمجة Java، مع أحدهما يُجري طلبًا لحظر C++ ثم إرسال إشعار إلى سلسلة مهام واجهة المستخدم عند اكتمال طلب الحظر .
  • تقليل عدد سلاسل المحادثات التي تحتاج إلى اللمس أو التأثير من قِبل JNI إذا كنت بحاجة إلى استخدام مجموعات سلاسل المحادثات بكل من لغتَي 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 التي تحدّدها النواة. عادةً تم البدء من رمز مُدار (باستخدام 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" بدون فلترته. ما لم تكن متأكدًا من أنّ البيانات صالحة بترميز MUTF-8 (أو 7 بت ASCII، وهي مجموعة فرعية متوافقة)، عليك إزالة الأحرف غير الصالحة أو تحويلها إلى تنسيق UTF-8 المعدَّل المناسب. إذا لم تقُم بذلك، من المحتمل أن تحقِّق الإحالة الناجحة بتنسيق UTF-16 نتائج غير متوقعة. تفحص ميزة CheckJNI، التي يتم تفعيلها تلقائيًا لأدوات المحاكاة، السلاسل ويوقف الجهاز الافتراضي في حال استقبال إدخالات غير صالحة.

قبل الإصدار 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<jchar[]> 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 فارغة (NULL) المؤشر لاحقًا.

يمكنك تحديد ما إذا كان قد تم نسخ البيانات أم لا عن طريق تمرير مؤشر ليس NULL للوسيطة 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. قيمة غير فارغة، فإنك لا تحتاج إلى البحث عن استثناء. ومع ذلك، إذا يمكنك استدعاء طريقة (باستخدام دالة مثل CallObjectMethod)، يجب عليك دائمًا التحقق من وجود استثناء، لأن القيمة المعروضة صالحة في حالة حدوث استثناء.

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

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

ما مِن دوال مضمّنة لمعالجة عنصر Throwable. نفسها، لذلك إذا كنت تريد (قول) الحصول على سلسلة الاستثناء، فستحتاج إلى للعثور على فئة Throwable، والبحث عن رقم تعريف الطريقة getMessage "()Ljava/lang/String;"، قم باستدعاءها، وإذا كانت النتيجة هي قيمة غير فارغة، استخدم 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* من سلسلة المهام غير الصحيحة
  • معرّفات jfieldID: استخدام معرّف jfieldID فارغ أو استخدام معرّف jfieldID لضبط حقل على قيمة من النوع الخطأ (مثل محاولة ضبط StringBuilder على حقل String) أو استخدام معرّف jfieldID لحقل ثابت لضبط حقل مثيل أو العكس، أو استخدام معرّف jfieldID من فئة واحدة مع مثيلات فئة أخرى
  • jmethodIDs: استخدام النوع غير الصحيح من jmethodID عند إجراء استدعاء Call*Method JNI: نوع إرجاع غير صحيح أو عدم تطابق ثابت/غير ثابت أو نوع خطأ "this" (للطلبات غير الثابتة) أو فئة خاطئة (للطلبات الثابتة)
  • المراجع: استخدام DeleteGlobalRef/DeleteLocalRef على النوع غير الصحيح من المراجع
  • أوضاع الإصدار: تمرير وضع إصدار غير صالح إلى طلب إصدار (أي شيء غير 0 أو JNI_ABORT أو JNI_COMMIT)
  • اكتب Safety: عرض نوع غير متوافق من طريقتك الأصلية (عرض 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 في أنّه يتيح لك التحقّق من توفّر الرموز مسبقًا، بالإضافة إلى إمكانية الحصول على مكتبات مشترَكة أصغر حجمًا وأسرع من خلال عدم تصدير أي ملفّات إلّا RegisterNatives. تتمثل ميزة السماح لمحطة التشغيل باكتشاف الدوالّ في أنّه لا يلزم كتابة رمز برمجي كثير.

لاستخدام 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، أو في حال عدم توفّرها (لأنّ المكالمة واردة من سلسلة محادثات أصلية تم إرفاقها للتو) فإنها تستخدم "النظام" عامل تحميل الفئات. لا يعرف أداة تحميل فئات النظام عن صفوف تطبيقك، لذا لن تتمكّن من البحث عن صفوفك باستخدام FindClass في ذلك السياق. يجعل هذا "JNI_OnLoad" مكانًا مناسبًا للبحث عن الصفوف وحفظها في ذاكرة التخزين المؤقت: مرة واحدة لديك jclass مرجع عام صالح يمكنك استخدامه من أي سلسلة محادثات مرفقة.

مكالمات أسرع مع التطبيقات الأصلية باستخدام @FastNative و@CriticalNative

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

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

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

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

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

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

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، قد تحتاج إلى كن على دراية بما يلي:

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

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

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

    لم يكن من الممكن استخدام pthread_key_create حتى Android 2.0 (Eclair) لتجنب "يجب فصل سلسلة المحادثات قبل خروج" شيك. (يستخدم وقت التشغيل أيضًا دالة pthread key destructor، لذا سيكون هناك تنافس لمعرفة أيهما يتم استدعاؤه أولاً).

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

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

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

    بدءًا من Android 4.0 (Ice Cream Sandwich)، يمكن استخدام المراجع الشاملة الضعيفة مثل أيّ مراجع أخرى لـ JNI.

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

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

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

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

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

  • @FastNative و@CriticalNative

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

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

  • رمية "ClassNotFoundException" من قِبل "FindClass"

    للتوافق مع الإصدارات القديمة، يعرض 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). لاحظ أنه قبل استخدام الآيس كريم ساندويتش، ماكرو 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 طريقة جيدة لمعرفة الاسم الداخلي لفصلك.

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

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

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

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

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

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

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

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

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

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

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

ويعتمد اختيار الطريقة التي تريد استخدامها على عاملَين:

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

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