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

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

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

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

لتصفّح المراجع العالمية لـ 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 (لتوفير الذاكرة) وبدأت في استخدام الانتقال جهاز تجميع البيانات المهملة. تقلل هذه الميزات بشكل كبير من عدد الحالات التي يمكن أن يشير إلى بيانات 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 واحدة بدلاً من 2، مما يقلل من النفقات العامة.
  • لا تتطلّب تثبيت أو نسخ بيانات إضافية.
  • تقليل مخاطر حدوث خطأ المبرمج — بدون خطر النسيان للاتصال بـ 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 وThrowNew فقطThrow ضبط مؤشر استثناء في سلسلة المحادثات الحالية عند العودة إلى الحسابات المُدارة من التعليمات البرمجية الأصلية، سيتم ملاحظة الاستثناء والتعامل معه بشكل مناسب.

إمكانية "وصول" الرمز البرمجي الأصلي استثناء من خلال استدعاء 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* من سلسلة محادثات خاطئة.
  • jfieldIDs: استخدام NULL jfieldID أو استخدام jfieldID لضبط حقل على قيمة من النوع الخطأ (محاولة تعيين StringBuilder لحقل سلسلة، على سبيل المثال)، أو استخدام 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) من صف ثابت أداة تهيئة الإعدادات. الوسيطة هي "undecorated" واسم المكتبة لذا لتحميل 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 إلى Android 13 (وإن كان بدون ضمانات CTS القوية)، ولكن لا يتوافق البحث الديناميكي عن الطرق الأصلية إلا في الإصدار 12 من نظام التشغيل Android أو الإصدارات الأحدث، يجب التسجيل صراحةً في JNI RegisterNatives التي تعمل على إصدارات Android من 8 إلى 11 يتم تجاهل هذه التعليقات التوضيحية على الإصدار 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، لذا سيكون سباقًا لمعرفة أيهما يتم استدعاؤه أولاً).

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

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

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

    بدءًا من نظام التشغيل Android 4.0 (Iceكريم ساندويتش) فصاعدًا، يمكن أن تكون الإشارات العالمية الضعيفة واستخدامها مثل أي مراجع أخرى لمكتبة 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، ومن المفترض أن يتحسن الأداء في الإصدارات المستقبلية.