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++ للحظر ثم يتم إرسال إشعار إلى سلسلة محادثات واجهة المستخدم عند اكتمال استدعاء الحظر.
- يجب تقليل عدد سلاسل المحادثات التي تحتاج إلى لمس الجهاز أو لمسه من خلال JNI. إذا كنت بحاجة إلى استخدام مجموعات سلاسل المحادثات بلغتَي Java وC++ ، حاوِل إبقاء اتصال JNI بين مالكي المجموعات بدلاً من سلاسل المحادثات الفردية.
- يمكنك الاحتفاظ برمز الواجهة في عدد قليل من المواقع الجغرافية لمصادر لغة C++ وJava التي يسهل التعرُّف عليها، وذلك لتسهيل عمليات إعادة البناء في المستقبل. ننصحك باستخدام مكتبة الإنشاء التلقائي لمبادرة JNI حسب الاقتضاء.
JavaVM وJNIEnv
تُعرِّف JNI بنيتين رئيسيتين للبيانات، هما "JavaVM" و "JNIEnv". كلاهما يشير في الأساس إلى مؤشرات لدوال الجداول. (في إصدار C++، تكون فئات بها مؤشر إلى جدول دالة ودالة عضو لكل دالة JNI توجه من خلال الجدول). توفر لك JavaVM وظائف "واجهة الاستدعاء" التي تتيح لك إنشاء جهاز JavaVM وإتلافه. من الناحية النظرية، يمكن أن يتوفّر لديك العديد من أجهزة JavaScript في كل عملية، لكن نظام 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() } }
جافا
/* * 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 (أو ASCII 7 بت، وهي مجموعة فرعية متوافقة)، ستحتاج إلى إزالة الأحرف غير الصالحة أو تحويلها إلى نموذج 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_ptrheap_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
تعرض قيمة غير فارغة،
لن تحتاج إلى البحث عن استثناء. ومع ذلك، إذا تم استدعاء طريقة (باستخدام دالة مثل 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 إلى حقل سلسلة، على سبيل المثال)، أو استخدام jfieldID لحقل ثابت لتعيين حقل مثيل أو العكس بالعكس، أو استخدام jfieldID من فئة مع مثيلات من فئة أخرى.
- jmethodID: استخدام النوع الخاطئ من 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") } }
جافا
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. من المحتمل أن تعمل هذه التحسينات أيضًا على أجهزة Android من الإصدار 8 إلى 13 (وإن لم تكن
ضمانات CTS القوية)، ولكن البحث الديناميكي عن الطرق الأصلية لا يتوفر إلا على
Android 12 والإصدارات الأحدث، يلزم التسجيل الصريح مع JNI RegisterNatives
بشكل كبير
للتشغيل على إصدارات Android من 8 إلى 11. يتم تجاهل هذه التعليقات التوضيحية في نظام التشغيل Android 7، وقد يؤدي عدم تطابق واجهة التطبيق الثنائية (ABI) مع @CriticalNative
إلى تنظيم الوسيطات بشكل خاطئ واحتمالية تعطُّلها.
بالنسبة إلى الطُرق المهمة للأداء التي تحتاج إلى هذه التعليقات التوضيحية، ننصح بشدة بالتسجيل بوضوح في الطريقة 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)
جافا
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
عند تخزين المؤشر على بنية أصلية في حقل JavaScript.
الميزات غير المتوافقة/التوافق مع الأنظمة القديمة
تتوفّر جميع ميزات JNI 1.6 باستثناء ما يلي:
- لم يتم تنفيذ سياسة
DefineClass
. لا يستخدم نظام Android رموز بايت Java أو ملفات الفئة، لذلك لا يمكن تمرير بيانات الفئات الثنائية.
للتوافق مع الأنظمة القديمة مع إصدارات Android القديمة، قد تحتاج إلى الانتباه إلى ما يلي:
- البحث الديناميكي للدوال الأصلية
حتى الإصدار Android 2.0 (Eclair)، لم يكن الحرف '$' يتم تحويله بشكل صحيح إلى "_00024" أثناء البحث عن أسماء الطرق. وللتغلب على هذا الأمر، يتطلب الأمر استخدام تسجيل صريح أو نقل الطرق الأصلية خارج الصفوف الداخلية.
- فصل سلاسل المحادثات
قبل الإصدار 2.0 من نظام التشغيل Android (Eclair)، لم يكن من الممكن استخدام دالة إزالة
pthread_key_create
لتجنُّب مربّع الاختيار "يجب فصل سلسلة المحادثات قبل الخروج". (يستخدم وقت التشغيل أيضًا دالة تدمير مفتاح pthread، لذا سيكون من الأفضل تحديد الخيار الذي يتم استدعاؤه أولاً.) - المراجع العامة الضعيفة
قبل الإصدار Android 2.2 (Froyo)، لم يتم تنفيذ المراجع العامة الضعيفة. وسترفض الإصدارات القديمة بشدّة محاولات استخدامها. يمكنك استخدام ثوابت إصدار نظام Android الأساسي لاختبار التوافق.
كان من الممكن نقل المراجع العامة الضعيفة فقط إلى
NewLocalRef
وNewGlobalRef
وDeleteWeakGlobalRef
، وذلك حتى الإصدار 4.0 من نظام التشغيل Android 4.0 (Ice Cream Sandwich). (تشجع المواصفات بشدة المبرمجين على إنشاء مراجع ثابتة للعالم العالمي الضعيف قبل اتخاذ أي إجراء، لذلك لا يجب أن يكون هذا تقييدًا على الإطلاق.)بدءًا من الإصدار 4.0 من نظام التشغيل Android (Ice Cream Sandwich)، يمكن استخدام المراجع العالمية الضعيفة مثل أي مراجع أخرى لدليل JNI.
- المراجع المحلية
قبل الإصدار 4.0 من نظام التشغيل Android (Ice Cream Sandwich)، كانت الإشارات المحلية عبارة عن مؤشرات مباشرة. أضاف تطبيق Ice Cream Sandwich أي طريقة غير مباشرة، يمكنك الاطّلاع على التغييرات في المراجع المحلية ضمن "مبادرة أخبار Google" (JNI) في ICS للحصول على مزيد من التفاصيل.
في إصدارات Android التي تسبق Android 8.0، يتم وضع حدّ أقصى لعدد المراجع المحلية على إصدار معيّن. بدايةً من نظام التشغيل Android 8.0، يدعم Android عددًا غير محدود من المراجع المحلية.
- تحديد نوع المرجع باستخدام
GetObjectRefType
كان من المستحيل تطبيق السمة
GetObjectRefType
بشكل صحيح بسبب استخدام المؤشرات المباشرة (راجِع أعلاه) حتى الإصدار 4.0 من نظام التشغيل Android (Ice Cream Sandwich). بدلاً من ذلك، استخدمنا الدليل التوجيهي الذي يفحص جدول globals ضعيف، والوسيطات، وجدول السكان المحليين، وجدول عوارض العموم بهذا الترتيب. وفي المرة الأولى التي يعثر فيها هذا النظام على المؤشر المباشر، سيُعلمك بأنّ المرجع الخاص بك كان من النوع الذي كان يتم فحصه. هذا يعني على سبيل المثال أنّك إذا طلبتَGetObjectRefType
على مستوى jclass عام وكان هذا الحدث مماثلاً لدالة jclass التي تم تمريرها كوسيطة ضمنية لطريقتك الأصلية الثابتة، ستحصل علىJNILocalRefType
بدلاً منJNIGlobalRefType
. @FastNative
و@CriticalNative
حتى الإصدار 7 من نظام التشغيل Android، تم تجاهل التعليقات التوضيحية هذه المتعلّقة بالتحسين. سيؤدي عدم تطابق واجهة التطبيق الثنائية (ABI) في
@CriticalNative
إلى ضبط غير صحيح للوسيطات وحدوث أعطال على الأرجح.لم يتم تنفيذ البحث الديناميكي للدوال الأصلية للطريقة
@FastNative
و@CriticalNative
في الإصدار 8 إلى 10 من نظام Android ويحتوي على أخطاء معروفة في Android 11. ومن المرجّح أن يؤدي استخدام هذه التحسينات بدون تسجيل صريح في الإصدارRegisterNatives
من JNI إلى حدوث أعطال في الإصدارات من 8 إلى 11 من نظام التشغيل Android.
سؤال شائع: لماذا أحصل على 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" كبير، يجب تعديل البيان. - بالنسبة إلى التسجيل الصريح، تظهر أخطاء بسيطة عند إدخال
توقيع الطريقة. تأكَّد من أنّ ما ترسله إلى استدعاء التسجيل يتطابق مع التوقيع الوارد في ملف السجلّ.
تذكَّر أنّ الحرف "ب" هو
byte
والقيمة "Z" هي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 في أداة تحميل الفئة "system" بدلاً من تلك المرتبطة بتطبيقك،
لذلك لن يتم تنفيذ محاولات العثور على فئات خاصة بالتطبيق.
في ما يلي بعض الطرق للتغلب على هذا الأمر:
- نفِّذ عمليات بحث
FindClass
مرة واحدة فيJNI_OnLoad
، واحتفِظ بمراجع الصفوف مؤقتًا لاستخدامها في وقت لاحق. عند تنفيذ أي طلباتFindClass
كجزء من تنفيذJNI_OnLoad
، سيتم استخدام أداة تحميل الفئة المرتبطة بالدالة التي تُسمىSystem.loadLibrary
(وهي قاعدة خاصة يتم تقديمها لجعل إعداد المكتبة أكثر ملاءمة). إذا كان رمز تطبيقك يحمِّل المكتبة، سيستخدمFindClass
أداة تحميل الفئة الصحيحة. - أدخِل مثيل الفئة في الدوال التي تحتاج إليها، من خلال الإعلان عن طريقتك الأصلية للحصول على وسيطة الفئة ثم تمرير
Foo.class
. - يمكنك تخزين مرجع إلى الكائن
ClassLoader
في مكان يسهل الوصول إليه وإصدار استدعاءاتloadClass
مباشرةً. يتطلّب ذلك بعض الجهد.
الأسئلة الشائعة: كيف أشارك البيانات الأولية باستخدام الرمز البرمجي الأصلي؟
قد تجد نفسك في موقف تحتاج فيه إلى الوصول إلى مخزن احتياطي كبير للبيانات الأولية من كل من الرموز البرمجية المدارة والأصلية. وتشمل الأمثلة الشائعة معالجة الصور نقطية أو عيّنات صوتية. هناك نهجان أساسيان.
يمكنك تخزين البيانات في byte[]
. يتيح ذلك الوصول السريع جدًا
من التعليمات البرمجية المُدارة. على الجانب الأصلي، ومع ذلك، لا نضمن لك
أن تكون قادرًا على الوصول إلى البيانات دون الحاجة إلى نسخها. في بعض عمليات التنفيذ، سيعرض GetByteArrayElements
وGetPrimitiveArrayCritical
مؤشرات فعلية إلى البيانات الأولية في كومة الذاكرة المؤقتة المُدارة، وفي حالات أخرى، سيتم تخصيص مخزن مؤقت في كومة الذاكرة المؤقتة الأصلية ونسخ البيانات إليها.
يتمثل الخيار البديل في تخزين البيانات في مخزن بايت مباشر مباشر. ويمكن إنشاؤها باستخدام java.nio.ByteBuffer.allocateDirect
، أو دالة JNI NewDirectByteBuffer
. على عكس مخازن البايت الاحتياطية العادية، لا يتم تخصيص مساحة التخزين على كومة الذاكرة المؤقتة المُدارة، ويمكن الوصول دائمًا إلى البيانات من الرمز الأصلي مباشرةً (احصل على العنوان من خلال GetDirectBufferAddress
). واستنادًا إلى كيفية تنفيذ الوصول المباشر إلى المخزن المؤقت للبايت، يمكن أن يكون الوصول إلى البيانات من رمز مُدار بطيء جدًا.
ويعتمد اختيارهم على عاملَين:
- هل ستتم معظم عمليات الوصول إلى البيانات من خلال رمز مكتوب بلغة Java أو بلغة C/C++ ؟
- إذا كانت البيانات يتم نقلها في النهاية إلى واجهة برمجة تطبيقات خاصة بالنظام، فما هو النموذج الذي يجب أن تكون به؟ (على سبيل المثال، إذا تم تمرير البيانات في النهاية إلى دالة تأخذ بايت[])، قد يكون من غير الحكم إجراء المعالجة في
ByteBuffer
مباشر.)
وإذا لم يكن هناك نص فائز واضح، فاستخدم مخزنًا احتياطيًا مباشرًا للبايت. وسيكون دعمهم مضمّنًا مباشرةً في JNI، ومن المفترض أن يتحسَّن مستوى أدائهم في الإصدارات المستقبلية.