जेएनआई से जुड़ी सलाह

JNI, Java Native Interface है. यह एक ऐसा तरीका है जिससे Android, मैनेज किए गए कोड (Java या Kotlin प्रोग्रामिंग भाषाओं में लिखा गया कोड) से कंपाइल किए गए बाइटकोड को नेटिव कोड (C/C++ में लिखा गया कोड) के साथ इंटरैक्ट करने की अनुमति देता है. JNI, वेंडर-न्यूट्रल है. इसमें डाइनैमिक शेयर की गई लाइब्रेरी से कोड लोड करने की सुविधा है. साथ ही, यह कभी-कभी मुश्किल होने के बावजूद काफ़ी असरदार है.

ध्यान दें: Android, Kotlin को ART के साथ काम करने वाले बाइटकोड में कंपाइल करता है. यह Java प्रोग्रामिंग लैंग्वेज की तरह ही काम करता है. इसलिए, इस पेज पर दिए गए दिशा-निर्देशों को Kotlin और Java प्रोग्रामिंग लैंग्वेज, दोनों पर लागू किया जा सकता है. ये दिशा-निर्देश, JNI आर्किटेक्चर और इससे जुड़े खर्चों के बारे में हैं. ज़्यादा जानने के लिए, Kotlin और Android लेख पढ़ें.

अगर आपको इसके बारे में पहले से जानकारी नहीं है, तो Java Native Interface Specification पढ़ें. इससे आपको यह समझने में मदद मिलेगी कि JNI कैसे काम करता है और कौनसी सुविधाएं उपलब्ध हैं. इंटरफ़ेस के कुछ पहलुओं के बारे में पहली बार पढ़ने पर तुरंत पता नहीं चलता. इसलिए, आपको अगले कुछ सेक्शन काम के लग सकते हैं.

ग्लोबल JNI रेफ़रंस ब्राउज़ करने और यह देखने के लिए कि ग्लोबल JNI रेफ़रंस कहां बनाए और मिटाए जाते हैं, Android Studio 3.2 और इसके बाद के वर्शन में, Memory Profiler में JNI हीप व्यू का इस्तेमाल करें.

सामान्य सलाह

अपनी JNI लेयर के फ़ुटप्रिंट को कम करने की कोशिश करें. यहां कई डाइमेंशन पर विचार करना होता है. आपका जेएनआई समाधान, इन दिशा-निर्देशों के मुताबिक होना चाहिए. यहां दिए गए दिशा-निर्देशों को उनकी अहमियत के हिसाब से क्रम में लगाया गया है. सबसे अहम दिशा-निर्देश सबसे ऊपर दिया गया है:

  • JNI लेयर में संसाधनों की मार्शेलिंग को कम से कम करें. जेएनआई लेयर में मार्शेलिंग की लागत कम नहीं होती. ऐसा इंटरफ़ेस डिज़ाइन करें जिसमें आपको कम से कम डेटा को मार्शेल करना पड़े. साथ ही, डेटा को मार्शेल करने की फ़्रीक्वेंसी भी कम हो.
  • जब हो सके, तो मैनेज की गई प्रोग्रामिंग भाषा में लिखे गए कोड और C++ में लिखे गए कोड के बीच एसिंक्रोनस कम्यूनिकेशन से बचें. इससे आपके JNI इंटरफ़ेस को बनाए रखना आसान हो जाएगा. आम तौर पर, यूज़र इंटरफ़ेस (यूआई) में एसिंक्रोनस अपडेट को आसान बनाया जा सकता है. इसके लिए, एसिंक्रोनस अपडेट को यूज़र इंटरफ़ेस (यूआई) की भाषा में ही रखें. उदाहरण के लिए, JNI के ज़रिए Java कोड में यूज़र इंटरफ़ेस (यूआई) थ्रेड से C++ फ़ंक्शन को कॉल करने के बजाय, Java प्रोग्रामिंग भाषा में दो थ्रेड के बीच कॉलबैक करना बेहतर होता है. इनमें से एक थ्रेड, C++ कॉल को ब्लॉक करता है. इसके बाद, जब कॉल पूरा हो जाता है, तो यूआई थ्रेड को सूचना देता है.
  • उन थ्रेड की संख्या कम करें जिन्हें JNI से इंटरैक्ट करना है. अगर आपको Java और C++ दोनों भाषाओं में थ्रेड पूल का इस्तेमाल करना है, तो अलग-अलग वर्कर थ्रेड के बजाय, पूल के मालिकों के बीच JNI कम्यूनिकेशन को बनाए रखने की कोशिश करें.
  • अपने इंटरफ़ेस कोड को C++ और Java के कम सोर्स कोड में रखें, ताकि आने वाले समय में इसे आसानी से पहचाना जा सके. इससे कोड को फिर से व्यवस्थित करने में आसानी होगी. ज़रूरत के हिसाब से, JNI अपने-आप जनरेट होने वाली लाइब्रेरी का इस्तेमाल करें.

JavaVM और JNIEnv

JNI, दो मुख्य डेटा स्ट्रक्चर "JavaVM" और "JNIEnv" को तय करता है. ये दोनों फ़ंक्शन टेबल के पॉइंटर के पॉइंटर होते हैं. (C++ वर्शन में, ये ऐसी क्लास होती हैं जिनमें फ़ंक्शन टेबल का पॉइंटर होता है. साथ ही, हर JNI फ़ंक्शन के लिए एक मेंबर फ़ंक्शन होता है, जो टेबल के ज़रिए इनडायरेक्ट होता है.) JavaVM, "invocation interface" फ़ंक्शन उपलब्ध कराता है. इनकी मदद से, JavaVM बनाया और बंद किया जा सकता है. सैद्धांतिक तौर पर, हर प्रोसेस के लिए एक से ज़्यादा JavaVM हो सकते हैं. हालांकि, Android सिर्फ़ एक की अनुमति देता है.

JNIEnv, ज़्यादातर JNI फ़ंक्शन उपलब्ध कराता है. आपके सभी नेटिव फ़ंक्शन को पहले आर्ग्युमेंट के तौर पर JNIEnv मिलता है. हालांकि, @CriticalNative तरीकों को छोड़कर, तेज़ नेटिव कॉल देखें.

JNIEnv का इस्तेमाल थ्रेड-लोकल स्टोरेज के लिए किया जाता है. इस वजह से, थ्रेड के बीच JNIEnv शेयर नहीं किया जा सकता. अगर किसी कोड के पास अपना JNIEnv पाने का कोई और तरीका नहीं है, तो आपको JavaVM शेयर करना चाहिए. साथ ही, थ्रेड के JNIEnv का पता लगाने के लिए GetEnv का इस्तेमाल करना चाहिए. (मान लें कि इसमें एक है; नीचे AttachCurrentThread देखें.)

JNIEnv और JavaVM के C एलान, C++ एलान से अलग होते हैं. "jni.h" फ़ाइल में, C या C++ में शामिल किए जाने के आधार पर अलग-अलग typedefs दिए जाते हैं. इस वजह से, दोनों भाषाओं में शामिल की गई हेडर फ़ाइलों में JNIEnv आर्ग्युमेंट शामिल करना सही नहीं है. (दूसरे शब्दों में कहें, अगर आपकी हेडर फ़ाइल को #ifdef __cplusplus की ज़रूरत है, तो आपको कुछ अतिरिक्त काम करना पड़ सकता है. ऐसा तब होगा, जब हेडर में मौजूद कोई भी चीज़ JNIEnv को रेफ़र करती हो.)

थ्रेड

सभी थ्रेड, Linux थ्रेड होते हैं. इन्हें कर्नल शेड्यूल करता है. आम तौर पर, इन्हें मैनेज किए गए कोड (Thread.start() का इस्तेमाल करके) से शुरू किया जाता है. हालांकि, इन्हें किसी दूसरी जगह भी बनाया जा सकता है. इसके बाद, इन्हें Thread.start() से जोड़ा जा सकता है.JavaVM उदाहरण के लिए, pthread_create() या std::thread से शुरू हुई थ्रेड को AttachCurrentThread() या AttachCurrentThreadAsDaemon() फ़ंक्शन का इस्तेमाल करके अटैच किया जा सकता है. जब तक कोई थ्रेड अटैच नहीं हो जाता, तब तक उसके पास JNIEnv नहीं होता. साथ ही, वह JNI कॉल नहीं कर सकता.

आम तौर पर, Thread.start() का इस्तेमाल करके ऐसा थ्रेड बनाना सबसे अच्छा होता है जिसे Java कोड में कॉल करने की ज़रूरत होती है. ऐसा करने से, यह पक्का किया जा सकेगा कि आपके पास स्टैक स्पेस काफ़ी है, आप सही ThreadGroup में हैं, और आपने वही ClassLoader इस्तेमाल किया है जो आपके Java कोड में इस्तेमाल किया गया है. Java में डीबग करने के लिए थ्रेड का नाम सेट करना, नेटिव कोड की तुलना में ज़्यादा आसान है. अगर आपके पास pthread_t या thread_t है, तो pthread_setname_np() देखें. अगर आपके पास std::thread है और आपको pthread_t चाहिए, तो std::thread::native_handle() देखें.

नेटिव तरीके से बनाई गई थ्रेड को अटैच करने से, java.lang.Thread ऑब्जेक्ट बनता है और उसे "main" ThreadGroup में जोड़ा जाता है. इससे वह डीबगर को दिखने लगता है. पहले से अटैच किए गए थ्रेड पर AttachCurrentThread() को कॉल करने से कोई कार्रवाई नहीं होती.

Android, नेटिव कोड को चलाने वाली थ्रेड को निलंबित नहीं करता है. अगर गार्बेज कलेक्शन चल रहा है या डीबगर ने निलंबित करने का अनुरोध किया है, तो Android अगली बार JNI कॉल करने पर थ्रेड को रोक देगा.

जेएनआई के ज़रिए अटैच किए गए थ्रेड को DetachCurrentThread() को कॉल करना होगा. अगर इसे सीधे तौर पर कोड करना मुश्किल है, तो Android 2.0 (Eclair) और इसके बाद के वर्शन में, डिस्ट्रक्टर फ़ंक्शन को तय करने के लिए pthread_key_create() का इस्तेमाल किया जा सकता है. इस फ़ंक्शन को थ्रेड बंद होने से पहले कॉल किया जाएगा. इसके बाद, DetachCurrentThread() को कॉल करें. (थ्रेड-लोकल-स्टोरेज में JNIEnv को सेव करने के लिए, pthread_setspecific() के साथ उस कुंजी का इस्तेमाल करें. इस तरह, इसे आपके डिस्ट्रक्टर में आर्ग्युमेंट के तौर पर पास किया जाएगा.)

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();
    }

अपने C/C++ कोड में एक nativeClassInit तरीका बनाएं, जो आईडी लुकअप करता हो. क्लास शुरू होने पर, कोड एक बार एक्ज़ीक्यूट होगा. अगर क्लास को कभी अनलोड करके फिर से लोड किया जाता है, तो इसे फिर से लागू किया जाएगा.

स्थानीय और ग्लोबल रेफ़रंस

किसी नेटिव तरीके को पास किया गया हर आर्ग्युमेंट और JNI फ़ंक्शन से मिला लगभग हर ऑब्जेक्ट, "लोकल रेफ़रंस" होता है. इसका मतलब है कि यह मौजूदा थ्रेड में, मौजूदा नेटिव तरीके के दौरान मान्य है. अगर नेटिव तरीके से वैल्यू वापस मिलने के बाद भी ऑब्जेक्ट मौजूद रहता है, तो भी रेफ़रंस मान्य नहीं होता.

यह jobject की सभी सब-क्लास पर लागू होता है. जैसे, jclass, jstring, और jarray. (एक्सटेंड की गई JNI जांचें चालू होने पर, रनटाइम आपको ज़्यादातर गलत रेफ़रंस के बारे में चेतावनी देगा.)

गैर-स्थानीय रेफ़रंस पाने का एक ही तरीका है. इसके लिए, NewGlobalRef और NewWeakGlobalRef फ़ंक्शन का इस्तेमाल करें.

अगर आपको किसी रेफ़रंस को लंबे समय तक सेव रखना है, तो आपको "ग्लोबल" रेफ़रंस का इस्तेमाल करना होगा. NewGlobalRef फ़ंक्शन, स्थानीय रेफ़रंस को आर्ग्युमेंट के तौर पर लेता है और ग्लोबल रेफ़रंस दिखाता है. ग्लोबल रेफ़रंस, DeleteGlobalRef को कॉल करने तक मान्य रहेगा.

इस पैटर्न का इस्तेमाल आम तौर पर, FindClass से मिले jclass को कैश मेमोरी में सेव करते समय किया जाता है. उदाहरण के लिए:

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 को 0x00 के बजाय 0xc0 0x80 के तौर पर कोड में बदलती है. इसकी सबसे अच्छी बात यह है कि आपको C-स्टाइल वाली शून्य से खत्म होने वाली स्ट्रिंग मिलेंगी. इनका इस्तेमाल, libc के स्टैंडर्ड स्ट्रिंग फ़ंक्शन के साथ किया जा सकता है. हालांकि, इसमें एक समस्या यह है कि JNI को कोई भी UTF-8 डेटा पास नहीं किया जा सकता और यह उम्मीद नहीं की जा सकती कि वह सही तरीके से काम करेगा.

किसी String का UTF-16 वर्शन पाने के लिए, GetStringChars का इस्तेमाल करें. ध्यान दें कि UTF-16 स्ट्रिंग, शून्य पर खत्म नहीं होती हैं. साथ ही, \u0000 का इस्तेमाल किया जा सकता है. इसलिए, आपको स्ट्रिंग की लंबाई के साथ-साथ jchar पॉइंटर को भी बनाए रखना होगा.

उन स्ट्रिंग को Release करना न भूलें जिन्हें आपको Get. स्ट्रिंग फ़ंक्शन, jchar* या jbyte* दिखाते हैं. ये लोकल रेफ़रंस के बजाय, प्रिमिटिव डेटा के लिए C-स्टाइल पॉइंटर होते हैं. इनके मान्य होने की गारंटी Release को कॉल किए जाने तक होती है. इसका मतलब है कि नेटिव तरीके के वापस आने पर इन्हें रिलीज़ नहीं किया जाता है.

NewStringUTF को पास किया गया डेटा, Modified UTF-8 फ़ॉर्मैट में होना चाहिए. आम तौर पर, लोग किसी फ़ाइल या नेटवर्क स्ट्रीम से वर्ण डेटा पढ़ते हैं और उसे फ़िल्टर किए बिना NewStringUTF को दे देते हैं. यह एक सामान्य गलती है. अगर आपको नहीं पता कि डेटा मान्य MUTF-8 (या 7-बिट ASCII, जो कि एक कंपैटिबल सबसेट है) में है, तो आपको अमान्य वर्णों को हटाना होगा या उन्हें सही Modified UTF-8 फ़ॉर्म में बदलना होगा. अगर ऐसा नहीं किया जाता है, तो UTF-16 कन्वर्ज़न से अनचाहे नतीजे मिल सकते हैं. CheckJNI, इम्यूलेटर के लिए डिफ़ॉल्ट रूप से चालू होता है. यह स्ट्रिंग को स्कैन करता है और अमान्य इनपुट मिलने पर वीएम को बंद कर देता है.

Android 8 से पहले, UTF-16 स्ट्रिंग का इस्तेमाल करना आम तौर पर ज़्यादा तेज़ होता था, क्योंकि Android को GetStringChars में कॉपी की ज़रूरत नहीं होती थी. वहीं, GetStringUTFChars के लिए, मेमोरी में जगह और UTF-8 में कन्वर्ज़न की ज़रूरत होती थी. Android 8 में, String के प्रज़ेंटेशन को बदल दिया गया है. अब ASCII स्ट्रिंग के लिए, हर वर्ण के लिए 8 बिट का इस्तेमाल किया जाता है, ताकि मेमोरी को बचाया जा सके. साथ ही, मूविंग गार्बेज कलेक्टर का इस्तेमाल शुरू कर दिया गया है. इन सुविधाओं की मदद से, ऐसे मामलों की संख्या में काफ़ी कमी आती है जहां ART, कॉपी किए बिना String डेटा का पॉइंटर दे सकता है. ऐसा 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 पॉइंटर को ऐक्सेस करने की कोशिश न करे.

isCopy आर्ग्युमेंट के लिए, शून्य नहीं है ऐसा पॉइंटर पास करके यह तय किया जा सकता है कि डेटा कॉपी किया गया था या नहीं. यह जानकारी कभी-कभार ही काम की होती है.

Release कॉल में mode आर्ग्युमेंट होता है, जिसकी तीन वैल्यू हो सकती हैं. रनटाइम की कार्रवाइयां इस बात पर निर्भर करती हैं कि उसने असल डेटा का पॉइंटर दिखाया है या उसकी कॉपी:

  • 0
    • असल में: ऐरे ऑब्जेक्ट को अनपिन कर दिया गया है.
    • कॉपी करें: डेटा को वापस कॉपी किया जाता है. कॉपी किए गए बफ़र को खाली कर दिया जाता है.
  • JNI_COMMIT
    • असल में: कुछ नहीं करता.
    • कॉपी करें: डेटा को वापस कॉपी किया जाता है. कॉपी किए गए बफ़र को खाली नहीं किया गया है.
  • JNI_ABORT
    • असल में: ऐरे ऑब्जेक्ट को अनपिन कर दिया गया है. पहले के राइट ऑपरेशन नहीं रोके जाते हैं.
    • कॉपी: कॉपी वाला बफ़र खाली हो जाता है. इसमें किए गए सभी बदलाव मिट जाते हैं.

isCopy फ़्लैग की जांच करने का एक मकसद यह जानना है कि किसी ऐरे में बदलाव करने के बाद, आपको JNI_COMMIT के साथ isCopy को कॉल करने की ज़रूरत है या नहीं. अगर आपको ऐरे के कॉन्टेंट का इस्तेमाल करने वाले कोड को लागू करने और उसमें बदलाव करने के बीच बारी-बारी से स्विच करना है, तो हो सकता है कि आपको नो-ऑप कमिट को स्किप करने का विकल्प मिले.Release फ़्लैग की जांच करने की एक और वजह, JNI_ABORT को बेहतर तरीके से मैनेज करना है. उदाहरण के लिए, हो सकता है कि आपको एक ऐरे चाहिए हो, उसमें बदलाव करना हो, उसके कुछ हिस्सों को दूसरे फ़ंक्शन में पास करना हो, और फिर बदलावों को खारिज करना हो. अगर आपको पता है कि JNI आपके लिए नई कॉपी बना रहा है, तो "बदलाव किया जा सकने वाला" एक और वर्शन बनाने की कोई ज़रूरत नहीं है. अगर JNI आपको ओरिजनल पास कर रहा है, तो आपको अपनी कॉपी बनाने की ज़रूरत नहीं है.

यह एक सामान्य गलती है. उदाहरण के तौर पर दिए गए कोड में इसे दोहराया गया है. इसमें यह मान लिया जाता है कि अगर *isCopy की वैल्यू फ़ॉल्स है, तो Release कॉल को स्किप किया जा सकता है. हालांकि, ऐसा नहीं है. अगर कोई कॉपी बफ़र असाइन नहीं किया गया है, तो ओरिजनल मेमोरी को पिन किया जाना चाहिए. साथ ही, इसे कचरा इकट्ठा करने वाले प्रोग्राम से हटाया नहीं जा सकता.

यह भी ध्यान दें कि 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 कॉल का इस्तेमाल किया जा सकता है. साथ ही, String से वर्णों को कॉपी करने के लिए GetStringRegion या GetStringUTFRegion का इस्तेमाल किया जा सकता है.

अपवाद

जब कोई अपवाद लंबित हो, तब आपको ज़्यादातर JNI फ़ंक्शन कॉल नहीं करने चाहिए. आपके कोड को अपवाद (फ़ंक्शन की रिटर्न वैल्यू, ExceptionCheck या ExceptionOccurred के ज़रिए) का पता लगाना चाहिए और उसे रिटर्न करना चाहिए या अपवाद को हटाकर उसे हैंडल करना चाहिए.

अपवाद के लंबित होने पर, सिर्फ़ इन JNI फ़ंक्शन को कॉल करने की अनुमति है:

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

कई JNI कॉल से अपवाद मिल सकता है. हालांकि, अक्सर वे फ़ेल होने की जांच करने का आसान तरीका उपलब्ध कराते हैं. उदाहरण के लिए, अगर NewString शून्य के अलावा कोई वैल्यू दिखाता है, तो आपको अपवाद की जांच करने की ज़रूरत नहीं है. हालांकि, अगर आपको किसी तरीके को कॉल करना है (CallObjectMethod जैसे फ़ंक्शन का इस्तेमाल करके), तो आपको हमेशा अपवाद की जांच करनी चाहिए. ऐसा इसलिए, क्योंकि अगर कोई अपवाद थ्रो किया गया है, तो रिटर्न वैल्यू मान्य नहीं होगी.

ध्यान दें कि मैनेज किए गए कोड से थ्रो किए गए अपवाद, नेटिव स्टैक फ़्रेम को अनवाइंड नहीं करते हैं. (साथ ही, C++ के अपवादों को Android पर आम तौर पर इस्तेमाल करने का सुझाव नहीं दिया जाता. इसलिए, C++ कोड से मैनेज किए गए कोड में JNI ट्रांज़िशन बाउंड्री के पार अपवाद नहीं होने चाहिए.) JNI Throw और ThrowNew निर्देश, मौजूदा थ्रेड में सिर्फ़ एक अपवाद पॉइंटर सेट करते हैं. नेटिव कोड से मैनेज किए गए कोड पर वापस आने पर, अपवाद को नोट किया जाएगा और उसे ठीक से हैंडल किया जाएगा.

नेटिव कोड, ExceptionCheck या ExceptionOccurred को कॉल करके अपवाद को "कैच" कर सकता है. साथ ही, ExceptionClear की मदद से इसे हटा सकता है. हमेशा की तरह, अपवादों को हैंडल किए बिना खारिज करने से समस्याएं हो सकती हैं.

Throwable ऑब्जेक्ट में बदलाव करने के लिए, कोई भी बिल्ट-इन फ़ंक्शन उपलब्ध नहीं है. इसलिए, अगर आपको (मान लें कि) अपवाद स्ट्रिंग चाहिए, तो आपको Throwable क्लास ढूंढनी होगी, getMessage "()Ljava/lang/String;" के लिए तरीका आईडी ढूंढना होगा, उसे लागू करना होगा, और अगर नतीजा गैर-शून्य है, तो GetStringUTFChars का इस्तेमाल करके वह जानकारी हासिल करनी होगी जिसे printf(3) या उसके बराबर के फ़ंक्शन को दिया जा सकता है.

ज़्यादा समय तक जांच करना

JNI में गड़बड़ी की जांच बहुत कम की जाती है. गड़बड़ियों की वजह से आम तौर पर क्रैश हो जाता है. Android में CheckJNI नाम का एक मोड भी होता है. इसमें JavaVM और JNIEnv फ़ंक्शन टेबल पॉइंटर को फ़ंक्शन की उन टेबल पर स्विच किया जाता है जो स्टैंडर्ड तरीके को कॉल करने से पहले, कई तरह की जांच करती हैं.

इन अतिरिक्त जांचों में ये शामिल हैं:

  • ऐरे: नेगेटिव साइज़ का ऐरे असाइन करने की कोशिश की जा रही है.
  • खराब पॉइंटर: JNI कॉल को खराब jarray/jclass/jobject/jstring पास करना या JNI कॉल को NULL पॉइंटर पास करना, जिसमें शून्य नहीं हो सकने वाला आर्ग्युमेंट होता है.
  • क्लास के नाम: JNI कॉल में, क्लास के नाम के “java/lang/String” स्टाइल के अलावा किसी और स्टाइल का इस्तेमाल करना.
  • अहम कॉल: “अहम” गेट और उससे जुड़ी रिलीज़ के बीच JNI कॉल करना.
  • Direct ByteBuffers: NewDirectByteBuffer में गलत आर्ग्युमेंट पास किए जा रहे हैं.
  • अपवाद: कोई अपवाद लंबित होने पर, JNI कॉल करना.
  • JNIEnv*s: गलत थ्रेड से JNIEnv* का इस्तेमाल किया जा रहा है.
  • jfieldIDs: NULL jfieldID का इस्तेमाल करना, किसी फ़ील्ड को गलत टाइप की वैल्यू पर सेट करने के लिए jfieldID का इस्तेमाल करना (उदाहरण के लिए, किसी String फ़ील्ड को StringBuilder असाइन करने की कोशिश करना), किसी स्टैटिक फ़ील्ड के लिए jfieldID का इस्तेमाल करके किसी इंस्टेंस फ़ील्ड को सेट करना या इसके उलट करना या किसी दूसरी क्लास के इंस्टेंस के साथ किसी क्लास के jfieldID का इस्तेमाल करना.
  • jmethodIDs: Call*Method JNI कॉल करते समय गलत तरह के jmethodID का इस्तेमाल करना: गलत रिटर्न टाइप, स्टैटिक/नॉन-स्टैटिक मिसमैच, ‘this’ के लिए गलत टाइप (नॉन-स्टैटिक कॉल के लिए) या गलत क्लास (स्टैटिक कॉल के लिए).
  • रेफ़रंस: गलत तरह के रेफ़रंस पर DeleteGlobalRef/DeleteLocalRef का इस्तेमाल करना.
  • रिलीज़ मोड: रिलीज़ कॉल में गलत रिलीज़ मोड पास करना. जैसे, 0, JNI_ABORT या JNI_COMMIT के अलावा कोई और मोड.
  • टाइप की सुरक्षा: नेटिव तरीके से बनाए गए तरीके से, गलत टाइप की वैल्यू वापस पाना. उदाहरण के लिए, String वैल्यू वापस पाने के लिए बनाए गए तरीके से StringBuilder वैल्यू वापस पाना.
  • UTF-8: JNI कॉल को अमान्य बदला गया UTF-8 बाइट सीक्वेंस पास किया जा रहा है.

(तरीकों और फ़ील्ड के ऐक्सेस की जांच अब भी नहीं की गई है: ऐक्सेस से जुड़ी पाबंदियां, नेटिव कोड पर लागू नहीं होती हैं.)

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 से किए गए सभी FindClass कॉल, शेयर की गई लाइब्रेरी को लोड करने के लिए इस्तेमाल किए गए क्लास लोडर के कॉन्टेक्स्ट में क्लास को हल करेंगे.JNI_OnLoad अन्य कॉन्टेक्स्ट से कॉल किए जाने पर, FindClass Java स्टैक में सबसे ऊपर मौजूद तरीके से जुड़े क्लास लोडर का इस्तेमाल करता है. अगर कोई क्लास लोडर नहीं है (क्योंकि कॉल, अभी-अभी अटैच किए गए नेटिव थ्रेड से किया गया है), तो यह "सिस्टम" क्लास लोडर का इस्तेमाल करता है. सिस्टम क्लास लोडर को आपके ऐप्लिकेशन की क्लास के बारे में जानकारी नहीं होती. इसलिए, उस कॉन्टेक्स्ट में FindClass का इस्तेमाल करके, अपनी क्लास नहीं देखी जा सकतीं. इससे JNI_OnLoad क्लास को खोजने और कैश मेमोरी में सेव करने के लिए एक सुविधाजनक जगह बन जाती है: एक बार जब आपके पास मान्य jclass ग्लोबल रेफ़रंस हो जाता है, तो इसका इस्तेमाल किसी भी अटैच किए गए थ्रेड से किया जा सकता है.

@FastNative और @CriticalNative की मदद से, नेटिव कॉलिंग की सुविधा का तेज़ी से इस्तेमाल करना

मैनेज किए गए और नेटिव कोड के बीच ट्रांज़िशन को तेज़ करने के लिए, नेटिव तरीकों को @FastNative या @CriticalNative (दोनों नहीं) के साथ एनोटेट किया जा सकता है. हालांकि, इन एनोटेशन के साथ कुछ बदलाव भी होते हैं. इसलिए, इनका इस्तेमाल करने से पहले आपको इन बदलावों के बारे में ध्यान से सोचना होगा. हमने यहां इन बदलावों के बारे में कम शब्दों में बताया है. ज़्यादा जानकारी के लिए, कृपया दस्तावेज़ देखें.

@CriticalNative एनोटेशन सिर्फ़ उन नेटिव तरीकों पर लागू किया जा सकता है जो मैनेज किए गए ऑब्जेक्ट का इस्तेमाल नहीं करते हैं. जैसे, पैरामीटर या रिटर्न वैल्यू में या इंप्लिसिट this के तौर पर. यह एनोटेशन, JNI ट्रांज़िशन ABI को बदलता है. नेटिव तरीके से लागू करने के लिए, फ़ंक्शन के सिग्नेचर से JNIEnv और jclass पैरामीटर को हटाना होगा.

@FastNative या @CriticalNative तरीके को लागू करते समय, गार्बेज कलेक्शन ज़रूरी काम के लिए थ्रेड को निलंबित नहीं कर सकता. साथ ही, यह ब्लॉक हो सकता है. लंबे समय तक चलने वाले तरीकों के लिए, इन एनोटेशन का इस्तेमाल न करें. इनमें आम तौर पर तेज़ी से काम करने वाले तरीके शामिल हैं, लेकिन आम तौर पर इनकी कोई सीमा नहीं होती. खास तौर पर, कोड को अहम I/O कार्रवाइयां नहीं करनी चाहिए. साथ ही, ऐसे नेटिव लॉक हासिल नहीं करने चाहिए जिन्हें लंबे समय तक रखा जा सकता है.

इन एनोटेशन को सिस्टम के इस्तेमाल के लिए, Android 8 से लागू किया गया था. साथ ही, Android 14 में ये CTS-टेस्ट किए गए सार्वजनिक एपीआई बन गए. ये ऑप्टिमाइज़ेशन, Android 8 से 13 वाले डिवाइसों पर भी काम कर सकते हैं. हालांकि, इनमें सीटीएस की गारंटी नहीं मिलती. नेटिव तरीकों का डाइनैमिक लुकअप, सिर्फ़ Android 12 और इसके बाद के वर्शन पर काम करता है. Android 8 से 11 वाले वर्शन पर चलाने के लिए, JNI RegisterNatives के साथ साफ़ तौर पर रजिस्ट्रेशन करना ज़रूरी है. Android 7 और इससे पहले के वर्शन पर इन एनोटेशन को अनदेखा किया जाता है. साथ ही, @CriticalNative के लिए ABI के मेल न खाने से, आर्ग्युमेंट को गलत तरीके से मार्शल किया जाएगा और ऐप्लिकेशन क्रैश हो सकते हैं.

परफ़ॉर्मेंस के लिए ज़रूरी उन तरीकों के लिए जिनमें इन एनोटेशन की ज़रूरत होती है, यह सुझाव दिया जाता है कि नेटिव तरीकों के नाम के आधार पर "डिस्कवरी" पर भरोसा करने के बजाय, JNI RegisterNatives के साथ तरीकों को साफ़ तौर पर रजिस्टर करें. ऐप्लिकेशन के स्टार्टअप की परफ़ॉर्मेंस को बेहतर बनाने के लिए, यह सुझाव दिया जाता है कि @FastNative या @CriticalNative तरीकों के कॉलर को बेसलाइन प्रोफ़ाइल में शामिल करें. Android 12 से, कंपाइल किए गए मैनेज किए गए किसी तरीके से @CriticalNative नेटिव तरीके को कॉल करना, C/C++ में नॉन-इनलाइन कॉल करने जितना ही आसान है. हालांकि, ऐसा तब तक होता है, जब तक सभी आर्ग्युमेंट रजिस्टर में फ़िट हो जाते हैं. उदाहरण के लिए, 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-बिट पॉइंटर का इस्तेमाल करने वाले आर्किटेक्चर के साथ काम करने के लिए, Java फ़ील्ड में नेटिव स्ट्रक्चर का पॉइंटर सेव करते समय, int के बजाय long फ़ील्ड का इस्तेमाल करें.

इस्तेमाल न की जा सकने वाली सुविधाएं/पिछले वर्शन के साथ काम न करने की सुविधा

JNI 1.6 की सभी सुविधाएँ काम करती हैं. हालाँकि, इसमें यह अपवाद है:

  • DefineClass लागू नहीं किया गया है. Android, Java बाइटकोड या क्लास फ़ाइलों का इस्तेमाल नहीं करता. इसलिए, बाइनरी क्लास डेटा पास करने से काम नहीं चलता.

Android के पुराने वर्शन के साथ काम करने की सुविधा के लिए, आपको इन बातों का ध्यान रखना होगा:

  • नेटिव फ़ंक्शन का डाइनैमिक लुकअप

    Android 2.0 (Eclair) तक, तरीके के नामों को खोजते समय '$' वर्ण को "_00024" में ठीक से नहीं बदला जाता था. इस समस्या को हल करने के लिए, साफ़ तौर पर रजिस्ट्रेशन करना होगा या नेटिव तरीकों को इनर क्लास से बाहर ले जाना होगा.

  • थ्रेड अलग करना

    Android 2.0 (Eclair) तक, "thread must be detached before exit" चेक से बचने के लिए, pthread_key_create डिस्ट्रक्टर फ़ंक्शन का इस्तेमाल नहीं किया जा सकता था. (रनटाइम, pthread कुंजी डिस्ट्रक्टर फ़ंक्शन का भी इस्तेमाल करता है, इसलिए यह देखना होगा कि कौन सा फ़ंक्शन पहले कॉल किया जाता है.)

  • कमज़ोर ग्लोबल रेफ़रंस

    Android 2.2 (Froyo) तक, वीक ग्लोबल रेफ़रंस लागू नहीं किए गए थे. पुराने वर्शन का इस्तेमाल करने की कोशिशों को अस्वीकार कर दिया जाएगा. Android प्लैटफ़ॉर्म के वर्शन के कॉन्स्टेंट का इस्तेमाल करके, यह जांच की जा सकती है कि आपके डिवाइस पर यह सुविधा काम करती है या नहीं.

    Android 4.0 (Ice Cream Sandwich) तक, सिर्फ़ NewLocalRef, NewGlobalRef, और DeleteWeakGlobalRef को कमज़ोर ग्लोबल रेफ़रंस पास किए जा सकते थे. (स्पेसिफ़िकेशन में प्रोग्रामर को यह सुझाव दिया गया है कि वे कमज़ोर ग्लोबल के साथ कुछ भी करने से पहले, उनके लिए हार्ड रेफ़रंस बनाएं. इसलिए, इससे कोई समस्या नहीं होनी चाहिए.)

    Android 4.0 (Ice Cream Sandwich) और इसके बाद के वर्शन में, वीक ग्लोबल रेफ़रंस का इस्तेमाल किसी भी अन्य JNI रेफ़रंस की तरह किया जा सकता है.

  • स्थानीय रेफ़रंस

    Android 4.0 (आइसक्रीम सैंडविच) तक, लोकल रेफ़रंस सीधे पॉइंटर होते थे. Ice Cream Sandwich में, बेहतर गार्बेज कलेक्टर की सुविधा के लिए ज़रूरी इंडायरेक्शन जोड़ा गया था. हालांकि, इसका मतलब यह है कि पुरानी रिलीज़ में कई JNI गड़बड़ियों का पता नहीं लगाया जा सकता. ज़्यादा जानकारी के लिए, ICS में JNI लोकल रेफ़रंस में हुए बदलाव देखें.

    Android 8.0 से पहले के Android वर्शन में, लोकल रेफ़रंस की संख्या को वर्शन के हिसाब से तय की गई सीमा तक सीमित किया जाता है. Android 8.0 से, Android में अनलिमिटेड लोकल रेफ़रंस इस्तेमाल किए जा सकते हैं.

  • GetObjectRefType की मदद से रेफ़रंस टाइप तय करना

    Android 4.0 (Ice Cream Sandwich) तक, डायरेक्ट पॉइंटर (ऊपर देखें) का इस्तेमाल करने की वजह से, GetObjectRefType को सही तरीके से लागू नहीं किया जा सका. इसके बजाय, हमने एक ह्यूरिस्टिक का इस्तेमाल किया. यह ह्यूरिस्टिक, इस क्रम में वीक ग्लोबल टेबल, आर्ग्युमेंट, लोकल टेबल, और ग्लोबल टेबल को देखती है. पहली बार डायरेक्ट पॉइंटर मिलने पर, यह रिपोर्ट करेगा कि आपका रेफ़रंस उस टाइप का है जिसकी वह जांच कर रहा है. उदाहरण के लिए, इसका मतलब यह था कि अगर आपने किसी ग्लोबल jclass पर GetObjectRefType कॉल किया है और वह jclass, आपके स्टैटिक नेटिव तरीके को इंप्लिसिट आर्ग्युमेंट के तौर पर पास किए गए jclass के जैसा ही है, तो आपको JNIGlobalRefType के बजाय JNILocalRefType मिलेगा.

  • @FastNative और @CriticalNative

    Android 7 तक, इन ऑप्टिमाइज़ेशन एनोटेशन को अनदेखा किया जाता था. @CriticalNative के लिए ABI के मेल न खाने की वजह से, आर्ग्युमेंट को गलत तरीके से मार्श किया जाएगा और ऐप्लिकेशन क्रैश हो सकता है.

    Android 8 से 10 में, @FastNative और @CriticalNative तरीकों के लिए नेटिव फ़ंक्शन के डाइनैमिक लुकअप को लागू नहीं किया गया था. साथ ही, Android 11 में कुछ ज्ञात बग मौजूद हैं. जेएनआई RegisterNatives के साथ साफ़ तौर पर रजिस्टर किए बिना इन ऑप्टिमाइज़ेशन का इस्तेमाल करने से, Android 8 से 11 पर क्रैश होने की संभावना है.

  • FindClass ने ClassNotFoundException को हराया

    पुराने सिस्टम के साथ काम करने की सुविधा के लिए, जब FindClass को कोई क्लास नहीं मिलती है, तो Android NoClassDefFoundError के बजाय ClassNotFoundException दिखाता है. यह 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 मैक्रो गलत था. इसलिए, पुराने jni.h के साथ नए GCC का इस्तेमाल करने से काम नहीं चलेगा. लाइब्रेरी में मौजूद सिंबल देखने के लिए, arm-eabi-nm का इस्तेमाल किया जा सकता है. अगर वे सही तरीके से नहीं दिख रहे हैं (जैसे, Java_Foo_myfunc के बजाय _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass दिख रहे हैं) या सिंबल का टाइप, कैपिटल '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; होगा. अगर आपको किसी इनर क्लास को ढूंढना है, तो '.' के बजाय '$' का इस्तेमाल करें. आम तौर पर, .class फ़ाइल पर javap का इस्तेमाल करना, अपनी क्लास का इंटरनल नाम पता करने का एक अच्छा तरीका है.

अगर आपने कोड का साइज़ कम करने की सुविधा चालू की है, तो पक्का करें कि आपने यह कॉन्फ़िगर किया हो कि कौनसे कोड को रखना है. सही कीप नियमों को कॉन्फ़िगर करना ज़रूरी है. ऐसा इसलिए, क्योंकि कोड श्रिंक करने वाला टूल उन क्लास, तरीकों या फ़ील्ड को हटा सकता है जिनका इस्तेमाल सिर्फ़ JNI से किया जाता है.

अगर क्लास का नाम सही है, तो हो सकता है कि आपको क्लास लोडर से जुड़ी समस्या आ रही हो. FindClass को आपके कोड से जुड़े क्लास लोडर में क्लास सर्च शुरू करनी है. यह कॉल स्टैक की जांच करता है, जो कुछ इस तरह दिखेगा:

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

सबसे ऊपर दिया गया तरीका Foo.myfunc है. FindClass, Foo क्लास से जुड़े ClassLoader ऑब्जेक्ट को ढूंढता है और उसका इस्तेमाल करता है.

आम तौर पर, इससे आपको मनमुताबिक नतीजे मिलते हैं. अगर आपने खुद ही थ्रेड बनाया है, तो आपको समस्या हो सकती है. ऐसा हो सकता है कि आपने pthread_create को कॉल करके, उसे AttachCurrentThread से अटैच किया हो. अब आपके ऐप्लिकेशन से कोई स्टैक फ़्रेम नहीं है. अगर इस थ्रेड से FindClass को कॉल किया जाता है, तो JavaVM, आपके ऐप्लिकेशन से जुड़े क्लास लोडर के बजाय "system" क्लास लोडर में शुरू होगा. इसलिए, ऐप्लिकेशन से जुड़ी क्लास ढूंढने की कोशिशें पूरी नहीं हो पाएंगी.

इस समस्या को हल करने के लिए यहां कुछ तरीके दिए गए हैं:

  • FindClass लुकअप एक बार करें. इसके लिए, JNI_OnLoad का इस्तेमाल करें. साथ ही, क्लास के रेफ़रंस को बाद में इस्तेमाल करने के लिए कैश मेमोरी में सेव करें. JNI_OnLoad को लागू करने के दौरान किए गए सभी FindClass कॉल, System.loadLibrary को कॉल करने वाले फ़ंक्शन से जुड़े क्लास लोडर का इस्तेमाल करेंगे. यह एक खास नियम है, जिसे लाइब्रेरी को शुरू करने की प्रोसेस को ज़्यादा आसान बनाने के लिए उपलब्ध कराया गया है. अगर आपका ऐप्लिकेशन कोड लाइब्रेरी लोड कर रहा है, तो FindClass सही क्लास लोडर का इस्तेमाल करेगा.
  • क्लास के इंस्टेंस को उन फ़ंक्शन में पास करें जिन्हें इसकी ज़रूरत है. इसके लिए, अपने नेटिव तरीके का एलान करें, ताकि वह क्लास आर्ग्युमेंट ले सके. इसके बाद, Foo.class पास करें.
  • ClassLoader ऑब्जेक्ट के रेफ़रंस को किसी ऐसी जगह पर कैश मेमोरी में सेव करें जहां से उसे आसानी से ऐक्सेस किया जा सके. इसके बाद, सीधे loadClass कॉल करें. इसके लिए, आपको कुछ मेहनत करनी होगी.

अक्सर पूछे जाने वाले सवाल: मैं नेटिव कोड के साथ रॉ डेटा कैसे शेयर करूं?

ऐसा हो सकता है कि आपको मैनेज किए गए और नेटिव कोड, दोनों से रॉ डेटा के बड़े बफ़र को ऐक्सेस करना पड़े. इसके सामान्य उदाहरणों में, बिटमैप या साउंड सैंपल में बदलाव करना शामिल है. इसके दो बुनियादी तरीके हैं.

डेटा को byte[] में सेव किया जा सकता है. इससे मैनेज किए गए कोड से बहुत तेज़ी से ऐक्सेस किया जा सकता है. हालांकि, नेटिव साइड पर, आपको डेटा को कॉपी किए बिना ऐक्सेस करने की गारंटी नहीं दी जाती. कुछ मामलों में, GetByteArrayElements और GetPrimitiveArrayCritical, मैनेज किए गए हीप में मौजूद रॉ डेटा के असली पॉइंटर दिखाएंगे. हालांकि, अन्य मामलों में यह नेटिव हीप पर एक बफ़र असाइन करेगा और डेटा को कॉपी करेगा.

इसके अलावा, डेटा को सीधे तौर पर बाइट बफ़र में भी सेव किया जा सकता है. इन्हें java.nio.ByteBuffer.allocateDirect या JNI NewDirectByteBuffer फ़ंक्शन का इस्तेमाल करके बनाया जा सकता है. सामान्य बाइट बफ़र के उलट, स्टोरेज को मैनेज किए गए हीप पर असाइन नहीं किया जाता. साथ ही, इसे हमेशा नेटिव कोड से सीधे तौर पर ऐक्सेस किया जा सकता है. इसके लिए, GetDirectBufferAddress का इस्तेमाल करके पता पाएं. डाइरेक्ट बाइट बफ़र ऐक्सेस को लागू करने के तरीके के आधार पर, मैनेज किए गए कोड से डेटा को ऐक्सेस करने में बहुत समय लग सकता है.

इनमें से किसका इस्तेमाल करना है, यह दो बातों पर निर्भर करता है:

  1. क्या ज़्यादातर डेटा ऐक्सेस, Java या C/C++ में लिखे गए कोड से होगा?
  2. अगर डेटा को सिस्टम एपीआई को भेजा जा रहा है, तो वह किस फ़ॉर्म में होना चाहिए? (उदाहरण के लिए, अगर डेटा को आखिर में ऐसे फ़ंक्शन में पास किया जाता है जो byte[] लेता है, तो सीधे तौर पर ByteBuffer में प्रोसेसिंग करना सही नहीं हो सकता.)

अगर सभी ने एक जैसा परफ़ॉर्म किया होगा, तो डायरेक्ट बाइट बफ़र का इस्तेमाल करें. इनके लिए सहायता सीधे तौर पर JNI में शामिल की गई है. साथ ही, आने वाली रिलीज़ में परफ़ॉर्मेंस बेहतर होनी चाहिए.