JNI هي واجهة Java الأصلية. تحدّد هذه الواجهة طريقة تفاعل الرمز الثانوي الذي يجمّعه Android من الرمز المُدار (المكتوب بلغتَي البرمجة Java أو Kotlin) مع الرمز الأصلي (المكتوب بلغة C/C++). وتتسم واجهة JNI بأنّها مستقلة عن المورّدين، وتتيح إمكانية تحميل الرمز من المكتبات المشترَكة الديناميكية، كما أنّها تتسم بالكفاءة المعقولة على الرغم من أنّها قد تكون معقّدة في بعض الأحيان.
ملاحظة: بما أنّ نظام التشغيل Android يجمّع رموز Kotlin البرمجية إلى رموز بايت متوافقة مع ART بطريقة مشابهة للغة البرمجة Java، يمكنك تطبيق الإرشادات الواردة في هذه الصفحة على كل من لغتَي البرمجة Kotlin وJava من حيث بنية JNI والتكاليف المرتبطة بها. لمزيد من المعلومات، يمكنك الاطّلاع على مقالة Kotlin وAndroid.
إذا لم تكن على دراية به، يمكنك الاطّلاع على مواصفات Java Native Interface للتعرّف على طريقة عمل JNI والميزات المتاحة. بعض جوانب الواجهة ليست واضحة على الفور عند قراءتها للمرة الأولى، لذا قد تجد الأقسام القليلة التالية مفيدة.
لتصفُّح مراجع JNI العامة والاطّلاع على أماكن إنشاء مراجع JNI العامة وحذفها، استخدِم عرض ذاكرة التخزين المؤقت JNI في أداة فحص الذاكرة في Android Studio 3.2 والإصدارات الأحدث.
نصائح عامة
حاوِل تقليل حجم طبقة 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"
include أنواع typedefs مختلفة
اعتمادًا على ما إذا كان مضمّنًا في 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()
قبل الخروج.
إذا كان الترميز المباشر لهذا الإجراء صعبًا، يمكنك في الإصدار 2.0 من Android (Eclair) والإصدارات الأحدث استخدام pthread_key_create()
لتحديد دالة التدمير التي سيتم استدعاؤها قبل إنهاء سلسلة المحادثات، واستدعاء DetachCurrentThread()
من هناك. (استخدِم هذا المفتاح مع pthread_setspecific()
لتخزين JNIEnv في مساحة التخزين المحلية الخاصة بالعمليات، وبذلك سيتم تمريره إلى الدالة المدمرة كمعلَمة.)
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
ليست كائنات أيضًا. (قد يتم تمريرها بين سلاسل المحادثات، وتكون صالحة إلى أن يتم استدعاء الدالة البرمجية Release المطابقة).
هناك حالة غير عادية تستحقّ الذكر بشكل منفصل. إذا أرفقت سلسلة محادثات أصلية بـ 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 بتنسيق Modified UTF-8. من الأخطاء الشائعة قراءة بيانات الأحرف من ملف أو بث شبكة وتسليمها إلى NewStringUTF
بدون فلترتها.
ما لم تكن متأكدًا من أنّ البيانات صالحة بتنسيق MUTF-8 (أو 7-bit ASCII، وهي مجموعة فرعية متوافقة)، عليك إزالة الأحرف غير الصالحة أو تحويلها إلى تنسيق Modified UTF-8 المناسب.
إذا لم يكن الأمر كذلك، من المحتمل أن يؤدي التحويل إلى UTF-16 إلى نتائج غير متوقعة.
تفحص CheckJNI السلاسل، وهي مفعّلة تلقائيًا للمحاكيات،
وتوقف الجهاز الافتراضي إذا تلقّت إدخالاً غير صالح.
قبل الإصدار 8 من نظام التشغيل Android، كان من الأسرع عادةً التعامل مع سلاسل 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
هي false. ولكن ليست هذه هي القضية. إذا لم يتم تخصيص مخزن مؤقت للنسخ، يجب تثبيت الذاكرة الأصلية ولا يمكن نقلها بواسطة أداة جمع البيانات غير المرغوب فيها.
يُرجى أيضًا العِلم أنّ العلامة 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، عبر حدود الانتقال من C++ إلى الرمز البرمجي المُدار في واجهة JNI).
تعمل تعليمات 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 بين عملية "get" مهمة وعملية الإصدار المقابلة لها
- Direct ByteBuffers: تمرير وسيطات غير صالحة إلى
NewDirectByteBuffer
- الاستثناءات: إجراء مكالمة JNI أثناء وجود استثناء معلّق
- JNIEnv*s: استخدام JNIEnv* من سلسلة محادثات غير صحيحة
- jfieldIDs: استخدام jfieldID بقيمة NULL، أو استخدام jfieldID لضبط قيمة حقل بنوع غير صحيح (محاولة تعيين StringBuilder لحقل String، مثلاً)، أو استخدام jfieldID لحقل ثابت لضبط حقل مثيل أو العكس، أو استخدام jfieldID من فئة واحدة مع مثيلات فئة أخرى
- jmethodIDs: استخدام نوع خاطئ من jmethodID عند إجراء
Call*Method
استدعاء JNI: نوع الإرجاع غير صحيح، أو عدم تطابق بين الثابت وغير الثابت، أو نوع خاطئ لـ "this" (لعمليات الاستدعاء غير الثابتة) أو فئة خاطئة (لعمليات الاستدعاء الثابتة). - المراجع: استخدام
DeleteGlobalRef
/DeleteLocalRef
مع نوع المرجع غير الصحيح - أوضاع الإصدار: تمرير وضع إصدار غير صالح إلى طلب إصدار (أي قيمة أخرى غير
0
أوJNI_ABORT
أوJNI_COMMIT
) - أمان الأنواع: عرض نوع غير متوافق من طريقتك الأصلية (على سبيل المثال، عرض StringBuilder من طريقة تم تعريفها لعرض String).
- UTF-8: تمرير تسلسل بايت Modified 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، أو إذا لم تكن هناك أداة تحميل (لأنّ الاستدعاء يتم من سلسلة محادثات أصلية تم ربطها للتو)، فإنّها تستخدم أداة تحميل الفئات "النظام". لا يعرف برنامج تحميل فئات النظام فئات تطبيقك، لذا لن تتمكّن من البحث عن فئاتك الخاصة باستخدام FindClass
في هذا السياق. وهذا يجعل JNI_OnLoad
مكانًا مناسبًا للبحث عن الفئات وتخزينها مؤقتًا: فبمجرد حصولك على jclass
مرجع عام صالح، يمكنك استخدامه من أي سلسلة محادثات مرفقة.
إجراء مكالمات أصلية أسرع باستخدام @FastNative
و@CriticalNative
يمكن إضافة تعليقات توضيحية إلى الطرق الأصلية باستخدام
@FastNative
أو
@CriticalNative
(وليس كليهما) لتسريع عمليات الانتقال بين الرمز البرمجي المُدار والأصلي. ومع ذلك، تتضمّن هذه التعليقات التوضيحية بعض التغييرات في السلوك التي يجب أخذها في الاعتبار بعناية قبل استخدامها. مع أنّنا سنشير بإيجاز إلى هذه التغييرات أدناه، يُرجى الرجوع إلى المستندات للحصول على التفاصيل.
لا يمكن تطبيق التعليق التوضيحي @CriticalNative
إلا على الطرق الأصلية التي لا تستخدم كائنات مُدارة (في المَعلمات أو قيم الإرجاع أو كـ this
ضمنيًا)، ويغيّر هذا التعليق التوضيحي واجهة التطبيق الثنائية (ABI) الخاصة بانتقال JNI. يجب أن يستبعد التنفيذ الأصلي المَعلمتَين JNIEnv
وjclass
من توقيع الدالة.
أثناء تنفيذ طريقة @FastNative
أو @CriticalNative
، لا يمكن لعملية جمع البيانات غير المرغوب فيها تعليق سلسلة المحادثات للقيام بعمل أساسي، وقد يتم حظرها. لا تستخدِم هذه التعليقات التوضيحية مع الطرق التي تستغرق وقتًا طويلاً، بما في ذلك الطرق السريعة عادةً ولكنها غير محدودة بشكل عام.
وعلى وجه الخصوص، يجب ألا ينفّذ الرمز عمليات كبيرة متعلقة بوحدات الإدخال والإخراج أو يحصل على أقفال أصلية يمكن الاحتفاظ بها لفترة طويلة.
تم تنفيذ هذه التعليقات التوضيحية للاستخدام في النظام منذ Android 8، وأصبحت واجهة برمجة تطبيقات عامة تم اختبارها باستخدام مجموعة اختبار التوافق (CTS) في Android 14. من المحتمل أن تعمل عمليات التحسين هذه أيضًا على الأجهزة التي تعمل بالإصدارات من 8 إلى 13 من نظام التشغيل Android (على الرغم من عدم توفّر ضمانات قوية من خلال مجموعة أدوات اختبار التوافق)، ولكن لا تتوفّر عملية البحث الديناميكي عن الطرق الأصلية إلا على الإصدار 12 من نظام التشغيل Android والإصدارات الأحدث، ويُشترط التسجيل الصريح باستخدام JNI RegisterNatives
لتشغيل الإصدارات من 8 إلى 11 من نظام التشغيل Android. يتم تجاهل هذه التعليقات التوضيحية على الإصدارات 7 من نظام التشغيل Android أو الإصدارات الأقدم، وسيؤدي عدم تطابق 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" أثناء عمليات البحث عن أسماء الطرق. لحلّ هذه المشكلة، يجب استخدام التسجيل الصريح أو نقل الطرق الأصلية خارج الفئات الداخلية.
- فصل سلاسل المحادثات
حتى الإصدار 2.0 من Android (Eclair)، لم يكن من الممكن استخدام دالة
pthread_key_create
تدمير لتجنُّب عملية التحقّق من "يجب فصل سلسلة المحادثات قبل الخروج". (يستخدم وقت التشغيل أيضًا دالة تدمير مفتاح pthread، لذا سيكون هناك تسابق لمعرفة أيّ منهما سيتم استدعاؤه أولاً). - المَراجع العامة الضعيفة
لم يتم تنفيذ المراجع العامة الضعيفة حتى الإصدار 2.2 من نظام التشغيل Android (Froyo). وسترفض الإصدارات القديمة بشدة محاولات استخدامها. يمكنك استخدام ثوابت إصدار منصة Android لاختبار التوافق.
حتى الإصدار 4.0 من نظام التشغيل Android (Ice Cream Sandwich)، كان من الممكن تمرير المراجع العامة الضعيفة إلى
NewLocalRef
وNewGlobalRef
وDeleteWeakGlobalRef
فقط. (تشجّع المواصفات المبرمجين بشدة على إنشاء مراجع ثابتة إلى المتغيرات العامة الضعيفة قبل إجراء أي عمليات عليها، لذا لن يكون ذلك مقيّدًا على الإطلاق).بدءًا من الإصدار 4.0 من نظام التشغيل Android (Ice Cream Sandwich)، يمكن استخدام المراجع العامة الضعيفة مثل أي مراجع أخرى لواجهة JNI.
- المراجع المحلية
حتى الإصدار Android 4.0 (Ice Cream Sandwich)، كانت المراجع المحلية عبارة عن مؤشرات مباشرة. أضاف الإصدار Ice Cream Sandwich عملية التوجيه غير المباشر اللازمة لتوفير أدوات أفضل لجمع البيانات غير المرغوب فيها، ولكن هذا يعني أنّ الكثير من أخطاء JNI لا يمكن رصدها في الإصدارات القديمة. يمكنك الاطّلاع على تغييرات المراجع المحلية لواجهة JNI في نظام التشغيل ICS للحصول على مزيد من التفاصيل.
في إصدارات Android الأقدم من Android 8.0، يقتصر عدد المراجع المحلية على حدّ أقصى خاص بالإصدار. بدءًا من الإصدار 8.0 من نظام التشغيل Android، يتيح Android استخدام مراجع محلية غير محدودة.
- تحديد نوع المرجع باستخدام
GetObjectRefType
قبل الإصدار Android 4.0 (Ice Cream Sandwich)، كان من المستحيل تنفيذ
GetObjectRefType
بشكل صحيح، وذلك نتيجةً لاستخدام المؤشرات المباشرة (راجِع ما ورد أعلاه). بدلاً من ذلك، استخدمنا طريقة تجريبية تبحث في جدول المتغيرات العمومية الضعيفة، وفي الوسيطات، وفي جدول المتغيرات المحلية، وفي جدول المتغيرات العمومية بهذا الترتيب. في المرة الأولى التي يعثر فيها على المؤشر المباشر، سيُبلغ بأنّ المرجع كان من النوع الذي كان يفحصه. كان هذا يعني، على سبيل المثال، أنّه إذا استدعيتGetObjectRefType
على فئة jclass عامة تبيّن أنّها مطابقة لفئة jclass التي تم تمريرها كمعلَمة ضمنية إلى طريقتك الثابتة الأصلية، ستحصل علىJNILocalRefType
بدلاً منJNIGlobalRefType
. -
@FastNative
و@CriticalNative
وحتى الإصدار 7 من نظام التشغيل Android، كان يتم تجاهل تعليقات التحسين التوضيحية هذه. سيؤدي عدم تطابق ABI مع
@CriticalNative
إلى تنسيق وسيطة خاطئ ومن المحتمل حدوث أعطال.لم يتم تنفيذ البحث الديناميكي عن الدوال الأصلية لطريقتَي
@FastNative
و@CriticalNative
في نظام التشغيل Android 8 إلى 10، كما أنّه يتضمّن أخطاء معروفة في Android 11. من المحتمل أن يؤدي استخدام عمليات التحسين هذه بدون التسجيل الصريح في JNIRegisterNatives
إلى حدوث أعطال على الإصدارات 8 إلى 11 من نظام التشغيل Android. FindClass
يطرحClassNotFoundException
لضمان التوافق مع الإصدارات القديمة، يعرض Android الخطأ
ClassNotFoundException
بدلاً منNoClassDefFoundError
عندما لا يعثرFindClass
على فئة. يتوافق هذا السلوك مع واجهة برمجة التطبيقات Java Reflection APIClass.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 Sandwich، كان ماكرو JNIEXPORT غير صحيح، لذا لن ينجح استخدام GCC جديد معjni.h
قديم. يمكنك استخدامarm-eabi-nm
للاطّلاع على الرموز كما تظهر في المكتبة. إذا بدت الرموز مشوّهة (مثل_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
بدلاً منJava_Foo_myfunc
)، أو إذا كان نوع الرمز هو الحرف "t" الصغير بدلاً من الحرف "T" الكبير، عليك تعديل التعريف. - بالنسبة إلى التسجيل الواضح، تحدث أخطاء بسيطة عند إدخال توقيع الطريقة. تأكَّد من أنّ ما ترسله إلى طلب التسجيل يتطابق مع التوقيع في ملف السجلّ.
تذكَّر أنّ "ب" هي
byte
و"ز" هيboolean
. تبدأ مكوّنات اسم الفئة في التواقيع بالحرف "L" وتنتهي بالرمز ";"، ويتم استخدام الرمز "/" للفصل بين أسماء الحِزم/الفئات، والرمز "$" للفصل بين أسماء الفئات الداخلية (Ljava/util/Map$Entry;
مثلاً).
- بالنسبة إلى البحث الكسول عن الطُرق، يجب الإفصاح عن دوال C++ باستخدام
قد يساعد استخدام 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 في أداة تحميل الفئات "النظام" بدلاً من أداة التحميل المرتبطة بتطبيقك، وبالتالي ستفشل محاولات العثور على فئات خاصة بالتطبيق.
في ما يلي بعض الطرق لتجنُّب هذه المشكلة:
- يجب إجراء عمليات البحث عن
FindClass
مرة واحدة فقط، فيJNI_OnLoad
، وتخزين مراجع الفئات مؤقتًا لاستخدامها لاحقًا. أي طلباتFindClass
يتم إجراؤها كجزء من تنفيذJNI_OnLoad
ستستخدِم أداة تحميل الفئات المرتبطة بالدالة التي طلبتSystem.loadLibrary
(هذه قاعدة خاصة، يتم توفيرها لتسهيل عملية تهيئة المكتبة). إذا كان رمز تطبيقك يحمّل المكتبة، سيستخدمFindClass
محمّل الفئات الصحيح. - مرِّر مثيلاً للفئة إلى الدوال التي تحتاج إليه، وذلك من خلال تعريف طريقتك الأصلية لتلقّي وسيطة فئة، ثم مرِّر
Foo.class
. - يمكنك تخزين مرجع إلى الكائن
ClassLoader
في مكان مناسب، وإصدار طلباتloadClass
مباشرةً. ويتطلّب ذلك بعض الجهد.
أسئلة شائعة: كيف يمكنني مشاركة البيانات الأولية مع الرمز البرمجي الأصلي؟
قد تجد نفسك في موقف تحتاج فيه إلى الوصول إلى مخزن مؤقت كبير من البيانات الأولية من الرموز البرمجية المُدارة والأصلية. وتشمل الأمثلة الشائعة معالجة الصور النقطية أو عيّنات الصوت. هناك طريقتان أساسيتان.
يمكنك تخزين البيانات في byte[]
. ويتيح ذلك الوصول السريع جدًا من الرمز المُدار. في المقابل، لا يمكنك ضمان إمكانية الوصول إلى البيانات بدون الحاجة إلى نسخها. في بعض عمليات التنفيذ، ستعرض GetByteArrayElements
وGetPrimitiveArrayCritical
مؤشرات فعلية إلى البيانات الأولية في الذاكرة المدارة، ولكن في عمليات أخرى، سيتم تخصيص مخزن مؤقت في الذاكرة الأصلية ونسخ البيانات.
البديل هو تخزين البيانات في مخزن مؤقت مباشر للبايت. يمكن إنشاء هذه
القيم باستخدام java.nio.ByteBuffer.allocateDirect
أو
دالة NewDirectByteBuffer
في JNI. وعلى عكس مخازن البيانات المؤقتة العادية، لا يتم تخصيص مساحة التخزين في الذاكرة المدارة، ويمكن دائمًا الوصول إليها مباشرةً من الرمز البرمجي الأصلي (يمكنك الحصول على العنوان باستخدام GetDirectBufferAddress
). وبناءً على طريقة تنفيذ الوصول المباشر إلى مخزن البيانات المؤقتة، قد يكون الوصول إلى البيانات من الرمز البرمجي المُدار بطيئًا جدًا.
يعتمد اختيار أحدهما على عاملين:
- هل سيتم الوصول إلى معظم البيانات من خلال رموز مكتوبة بلغة Java أو C/C++؟
- إذا كان سيتم نقل البيانات في النهاية إلى واجهة برمجة تطبيقات خاصة بالنظام، ما هو الشكل الذي يجب أن تكون عليه؟ (على سبيل المثال، إذا تم تمرير البيانات في النهاية إلى دالة تأخذ byte[]، قد يكون من غير الحكمة إجراء المعالجة في
ByteBuffer
مباشرةً).
إذا لم يكن هناك فائز واضح، استخدِم مخزنًا مؤقتًا مباشرًا للبايت. ويتم توفير الدعم لها مباشرةً في JNI، ومن المفترض أن يتحسّن الأداء في الإصدارات المستقبلية.