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

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

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

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

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

نصائح عامة

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

  • تقليل تنظيم الموارد إلى أدنى حد في طبقة JNI إنّ تنظيم خوارزمية التنظيم على مستوى طبقة مبادرة أخبار Google (JNI) يؤدي إلى تكاليف غير بسيطة. حاوِل تصميم واجهة تقلِّل من مقدار البيانات التي تحتاج إلى تنظيمها ومعدّل تكرار تنظيم البيانات.
  • تجنَّب الاتصال غير المتزامن بين الرمز المكتوب بلغة برمجة مُدارة والرمز المكتوب بلغة C++ متى أمكن. سيؤدي هذا الإجراء إلى تسهيل صيانة واجهة JNI. يمكنك عادةً تبسيط التحديثات غير المتزامنة لواجهة المستخدم من خلال إبقاء التحديث غير المتزامن بلغة واجهة المستخدم نفسها. على سبيل المثال، بدلاً من استدعاء دالة C++ من سلسلة واجهة المستخدم في رمز JavaScript عبر 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، ومجدولة باستخدام النواة kernel. ويبدأ المستخدمون عادةً من الرمز المُدار (باستخدام Thread.start())، ولكن يمكن أيضًا إنشاؤها في مكان آخر ثم إرفاقها بـ JavaVM. على سبيل المثال، يمكن إرفاق سلسلة محادثات بدأت بـ pthread_create() أو std::thread باستخدام الدالتَين AttachCurrentThread() أو AttachCurrentThreadAsDaemon(). إلى أن يتم إرفاق سلسلة التعليمات، لا تحتوي سلسلة الرسائل على JNIEnv، ولا يمكنها إجراء طلبات JNI.

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

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

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

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr 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 للوسيطة isCopy. نادرًا ما يكون هذا مفيدًا.

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

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

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

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

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

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

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

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

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

ويمكن للمرء تحقيق الشيء نفسه ببساطة أكبر:

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

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

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

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

الاستثناءات

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

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

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

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

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

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

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

فحص ممتد

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

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

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

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

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

في حال استخدام المحاكي، يتم تفعيل CheckJNI تلقائيًا.

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

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

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

D AndroidRuntime: CheckJNI is ON

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

adb shell setprop debug.checkjni 1

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

D Late-enabling CheckJNI

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

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

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

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

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

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

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

لاستخدام RegisterNatives:

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

يجب أن يبدو المهيأ الثابت هكذا:

Kotlin

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

Java

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

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

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

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

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

    return JNI_VERSION_1_6;
}

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

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

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

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

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

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

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

بالنسبة إلى الطُرق المهمة المتعلّقة بالأداء والتي تحتاج إلى هذه التعليقات التوضيحية، ننصح بشدة بإجراء التسجيل الصريح للطرق في JNI RegisterNatives بدلاً من الاعتماد على "الاكتشاف" للأساليب الأصلية بناءً على الاسم. للحصول على أفضل أداء عند بدء تشغيل التطبيق، ننصح بإدراج المتصلين بطريقة @FastNative أو @CriticalNative في الملف الشخصي الأساسي. منذ نظام التشغيل Android 12، سعر استدعاء طريقة @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 عند تخزين مؤشر ما إلى بنية أصلية في حقل 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 الأساسي لاختبار التوافق.

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

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

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

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

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

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

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

  • @FastNative و@CriticalNative

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

    لم يتم تنفيذ البحث الديناميكي عن الدوال الأصلية للطُرق @FastNative و@CriticalNative في الإصدارين 8 و10 من نظام Android يحتوي على أخطاء معروفة في الإصدار 11 من نظام Android. من المرجّح أن يؤدي استخدام هذه التحسينات بدون التسجيل الصريح لدى JNI RegisterNatives إلى حدوث أعطال في الأجهزة التي تعمل بالإصدارات من 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). يُرجى ملاحظة أنّه قبل استخدام Icekin كانت وحدة ماكرو JNIEXEX غير صحيحة، لذا لن يعمل استخدام GCC جديد مع jni.h قديم. يمكنك استخدام arm-eabi-nm للاطّلاع على الرموز كما تظهر في المكتبة. إذا كانت الرموز تبدو مشوهة (مثل _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass بدلاً من Java_Foo_myfunc)، أو إذا كان نوع الرمز صغيرًا 't' وليس كبير 'T'، عليك تعديل البيان.
    • بالنسبة إلى التسجيل الصريح، ستظهر أخطاء بسيطة عند إدخال توقيع الطريقة. تأكَّد من أنّ الملف الذي تمرِّره إلى طلب التسجيل يتطابق مع التوقيع في ملف السجلّ. يُرجى تذكُّر أنّ الحرف "ب" هو byte والقيمة "ي" هي boolean. تبدأ مكونات اسم الفئة في التوقيعات بالحرفين "L" وتنتهي بـ ";"، وتستخدم "/" لفصل أسماء الحزمة/الفئة، وتستخدم "$" للفصل بين أسماء الفئة الداخلية (على سبيل المثال Ljava/util/Map$Entry;).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

يعتمد اختيار الإصدار المناسب على عاملَين:

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

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