نکته‌های JNI

JNI مخفف Java Native Interface (میانای بومی جاوا) است. این میانای برنامه‌سازی کاربردی روشی را برای کدبایتی که Android از کد مدیریت‌شده (نوشته‌شده به زبان‌های برنامه‌نویسی Java یا Kotlin) کامپایل می‌کند تعریف می‌کند تا با کد بومی (نوشته‌شده به زبان C/C++‎) تعامل برقرار کند. JNI مستقل از فروشنده است، از بار کردن کد از کتابخانه‌های مشترک پویا پشتیبانی می‌کند، و اگرچه گاهی اوقات دست‌وپاگیر است، اما به‌طور معقولی کارآمد است.

توجه: ازآنجاکه Android،‏ Kotlin را به بایت‌کد سازگار با ART به روشی مشابه زبان برنامه‌نویسی Java کامپایل می‌کند، می‌توانید راهنمایی‌های این صفحه را ازنظر معماری JNI و هزینه‌های مرتبط با آن برای هر دو زبان برنامه‌نویسی Kotlin و Java اعمال کنید. برای کسب اطلاعات بیشتر، Kotlin و Android را ببینید.

اگر قبلاً با آن آشنا نشده‌اید، مشخصات میانای بومی جاوا را بخوانید تا با نحوه عملکرد JNI و ویژگی‌های دردسترس آشنا شوید. برخی‌از جنبه‌های میانای کاربری در اولین خواندن فوراً آشکار نمی‌شوند، بنابراین ممکن است چند بخش بعدی برایتان مفید باشد.

برای مرور ارجاع‌های JNI سراسری و دیدن اینکه ارجاع‌های JNI سراسری کجا ایجاد و حذف می‌شوند، از نمای انبوهه JNI در تحلیل‌گر حافظه در Android Studio نسخه ۳.۲ و بالاتر استفاده کنید.

نکات کلی

سعی کنید ردپای لایه JNI خود را به حداقل برسانید. در اینجا چند بعد وجود دارد که باید درنظر گرفته شود. ‫JNI شما باید سعی کند از این دستورالعمل‌ها پیروی کند (به‌ترتیب اهمیت در زیر فهرست شده‌اند، با مهم‌ترین شروع می‌شود):

  • مرتب‌سازی منابع را در لایه JNI به حداقل برسانید. مرتب‌سازی در لایه JNI هزینه‌های غیرجزئی دارد. سعی کنید واسطی طراحی کنید که مقدار داده‌هایی را که باید مرتب کنید و تعداد دفعاتی را که باید داده‌ها را مرتب کنید به حداقل برساند.
  • درصورت امکان، از ارتباط ناهمگام بین کد نوشته‌شده در زبان برنامه‌نویسی مدیریت‌شده و کد نوشته‌شده در C++‎ اجتناب کنید. این کار باعث می‌شود نگهداری از میانای JNI شما آسان‌تر شود. معمولاً می‌توانید به‌روزرسانی‌های ناهمگام واسط کاربر را با نگه داشتن به‌روزرسانی ناهمگام در همان زبان واسط کاربر ساده کنید. برای مثال، به‌جای فراخوانی تابع C++‎ از رشته میانای کاربر در کد Java ازطریق JNI، بهتر است فراخوانی برگشتی بین دو رشته در زبان برنامه‌نویسی Java انجام شود، به‌طوری‌که یکی از آن‌ها فراخوانی مسدودکننده C++‎ را انجام دهد و سپس وقتی فراخوانی مسدودکننده تکمیل شد، به رشته میانای کاربر اطلاع دهد.
  • تعداد رشته‌هایی را که باید با JNI تماس داشته باشند یا JNI با آن‌ها تماس داشته باشد به حداقل برسانید. اگر لازم است از استخرهای رشته در هر دو زبان Java و C++‎ استفاده کنید، سعی کنید ارتباط JNI را بین مالکان استخر حفظ کنید، نه بین رشته‌های عامل جداگانه.
  • کد میانای خود را در تعداد کمی از مکان‌های منبع C++‎ و Java که به‌راحتی قابل شناسایی هستند نگه دارید تا بازسازی کد در آینده تسهیل شود. درصورت لزوم، از کتابخانه تولید خودکار JNI استفاده کنید.

JavaVM و JNIEnv

JNI دو ساختار داده کلیدی را تعریف می‌کند: «JavaVM» و «JNIEnv». هر دوی این‌ها اساساً اشاره‌گرهایی به اشاره‌گرهای جدول‌های تابع هستند. (در نسخه C++‎، آن‌ها کلاس‌هایی با یک اشاره‌گر به جدول تابع و یک تابع عضو برای هر تابع JNI هستند که به‌طور غیرمستقیم ازطریق جدول عمل می‌کنند.) JavaVM عملکردهای «میانای فراخوانی» را ارائه می‌دهد، که به شما امکان می‌دهد JavaVM را ایجاد و نابود کنید. از لحاظ نظری می‌توانید چند JavaVM در هر فرایند داشته باشید، اما Android فقط یک مورد را مجاز می‌داند.

JNIEnv اکثر توابع JNI را ارائه می‌دهد. همه توابع بومی شما JNIEnv را به‌عنوان اولین آرگومان دریافت می‌کنند، به‌جز روش‌های @CriticalNative، فراخوانی‌های بومی سریع‌تر را ببینید.

از JNIEnv برای فضای ذخیره‌سازی محلی رشته استفاده می‌شود. به همین دلیل، نمی‌توانید JNIEnv را بین رشته‌ها هم‌رسانی کنید. If a piece of code has no other way to get its JNIEnv, you should share the JavaVM, and use GetEnv to discover the thread's JNIEnv. (با فرض اینکه داشته باشد؛ AttachCurrentThread را در زیر ببینید.)

تعاریف C برای JNIEnv و JavaVM با تعاریف C++‎ متفاوت است. فایل ضمیمه "jni.h" typedefهای مختلفی ارائه می‌دهد که به ضمیمه شدن آن در C یا C++‎ بستگی دارد. به همین دلیل، ضمیمه کردن آرگومان‌های JNIEnv در فایل‌های سرایند ضمیمه‌شده توسط هر دو زبان ایده خوبی نیست. (به عبارت دیگر: اگر فایل سرایند شما به #ifdef __cplusplus نیاز دارد، ممکن است مجبور شوید کارهای اضافی انجام دهید اگر چیزی در آن سرایند به JNIEnv اشاره دارد.)

رشته‌ها

همه رشته‌ها رشته‌های Linux هستند که توسط هسته زمان‌بندی شده‌اند. این رشته‌ها معمولاً از کد مدیریت‌شده (بااستفاده از Thread.start()) شروع می‌شوند، اما می‌توان آن‌ها را در جای دیگری نیز ایجاد کرد و سپس به JavaVM پیوست کرد. برای مثال، رشته‌ای که با pthread_create() یا std::thread شروع شده است را می‌توان بااستفاده از توابع AttachCurrentThread() یا AttachCurrentThreadAsDaemon() پیوست کرد. تا زمانی که رشته‌ای پیوست نشود، JNIEnv ندارد و نمی‌تواند تماس‌های JNI برقرار کند.

معمولاً بهتر است از Thread.start() برای ایجاد هر رشته‌ای که نیاز به فراخوانی کد جاوا دارد استفاده کنید. با این کار مطمئن می‌شوید که فضای پشته کافی دارید، در ThreadGroup صحیح هستید، و از همان ClassLoader کد Java خود استفاده می‌کنید. همچنین تنظیم نام رشته برای اشکال‌زدایی در Java آسان‌تر از کد بومی است (اگر pthread_t یا thread_t دارید، pthread_setname_np() را ببینید و اگر std::thread دارید و pthread_t می‌خواهید، std::thread::native_handle() را ببینید).

پیوست کردن رشته‌ای که به‌صورت بومی ایجاد شده است باعث می‌شود شیء java.lang.Thread ساخته شود و به ThreadGroup «اصلی» اضافه شود، که آن را برای اشکال‌زدا قابل‌مشاهده می‌کند. فراخوانی AttachCurrentThread() در رشته‌ای که ازقبل پیوست شده است، عملیاتی ندارد.

Android رشته‌های درحال اجرای کد بومی را تعلیق نمی‌کند. اگر جمع‌آوری زباله درحال انجام باشد یا اشکال‌زدا درخواست تعلیق صادر کرده باشد، Android دفعه بعد که رشته تماس JNI برقرار می‌کند، آن را موقتاً متوقف خواهد کرد.

رشته‌های پیوست‌شده ازطریق JNI باید قبل‌از خروج، DetachCurrentThread() را فراخوانی کنند. اگر کدنویسی مستقیم این مورد دشوار است، در Android 2.0 (Eclair) و بالاتر می‌توانید از pthread_key_create() برای تعریف تابع تخریب‌گر استفاده کنید که قبل‌از خروج رشته فراخوانی می‌شود، و DetachCurrentThread() را از آنجا فراخوانی کنید. (از آن کلید با pthread_setspecific() برای ذخیره JNIEnv در فضای ذخیره‌سازی محلی رشته استفاده کنید؛ به این ترتیب، به‌عنوان آرگومان به مخرب شما ارسال می‌شود.)

‫jclass،‏ jmethodID، و jfieldID

اگر می‌خواهید از کد بومی به فیلد شیء دسترسی پیدا کنید، باید کارهای زیر را انجام دهید:

  • دریافت مرجع شیء کلاس برای کلاس با FindClass
  • دریافت شناسه فیلد برای فیلد با GetFieldID
  • محتوای فیلد را با چیزی مناسب جایگزین کنید، مثلاً GetIntField

به‌طور مشابه، برای فراخوانی یک روش، ابتدا باید یک مرجع شیء کلاس و سپس یک شناسه روش دریافت کنید. شناسه‌ها اغلب فقط اشاره‌گرهایی به ساختارهای داده زمان اجرای داخلی هستند. جستجوی آن‌ها ممکن است به چندین مقایسه رشته نیاز داشته باشد، اما وقتی آن‌ها را داشته باشید، فراخوانی واقعی برای دریافت فیلد یا فراخوانی روش بسیار سریع است.

اگر عملکرد مهم است، بهتر است مقادیر را یک‌بار جستجو کنید و نتایج را در کد بومی‌تان ذخیره کنید. ازآنجایی‌که محدودیت یک JavaVM برای هر فرایند وجود دارد، منطقی است که این داده‌ها در یک ساختار محلی ایستا ذخیره شوند.

مرجع‌های کلاس، شناسه‌های فیلد، و شناسه‌های روش تا زمانی‌که کلاس بار نشده باشد تضمین می‌شود که معتبر باشند. کلاس‌ها فقط درصورتی بارگیری نمی‌شوند که همه کلاس‌های مرتبط با ClassLoader بتوانند زباله‌روبی شوند، که این امر نادر است اما در Android غیرممکن نیست. اما توجه داشته باشید که jclass یک مرجع کلاس است و باید با فراخوانی NewGlobalRef محافظت شود (بخش بعدی را ببینید).

اگر می‌خواهید شناسه‌ها را هنگام بار شدن کلاس در حافظه نهان ذخیره کنید و اگر کلاس بارگیری‌اش لغو شد و دوباره بار شد، آن‌ها را به‌طور خودکار دوباره در حافظه نهان ذخیره کنید، روش صحیح برای مقداردهی اولیه شناسه‌ها این است که قطعه کدی شبیه به این را به کلاس مناسب اضافه کنید:

کاتلین

companion object {
    /*
     * We use a static class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private external fun nativeInit()

    init {
        nativeInit()
    }
}

جاوا

    /*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

در کد C/C++‎ خود، روش nativeClassInit را ایجاد کنید که جستجوی شناسه را انجام می‌دهد. کد یک‌بار، هنگام مقداردهی اولیه کلاس، اجرا خواهد شد. اگر کلاس بارگیری‌زدایی شود و سپس مجدداً بارگیری شود، دوباره اجرا خواهد شد.

مراجع محلی و جهانی

هر آرگومانی که به یک روش بومی منتقل می‌شود و تقریباً هر شیء که توسط یک تابع 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 این کار را برای شما انجام دهد. پیاده‌سازی فقط برای رزرو کردن جایگاه برای ۱۶ مرجع محلی لازم است، بنابراین اگر به تعداد بیشتری نیاز دارید، باید یا درحین پیش رفتن حذف کنید یا از EnsureLocalCapacity/PushLocalFrame برای رزرو کردن تعداد بیشتر استفاده کنید.

توجه داشته باشید که jfieldID و jmethodID انواع مات هستند، نه ارجاع‌های شیء، و نباید به NewGlobalRef منتقل شوند. اشاره‌گرهای داده‌های خام که توسط توابعی مانند GetStringUTFChars و GetByteArrayElements برگردانده می‌شوند نیز شیء نیستند. (ممکن است بین رشته‌ها منتقل شوند و تا زمان فراخوانی Release منطبق معتبر هستند.)

یک مورد غیرعادی شایسته ذکر جداگانه است. اگر رشته‌ای بومی را با AttachCurrentThread پیوست کنید، کدی که اجرا می‌کنید هرگز به‌طور خودکار ارجاع‌های محلی را آزاد نمی‌کند تا زمانی که رشته جدا شود. هرگونه ارجاع محلی که ایجاد می‌کنید باید به‌صورت دستی حذف شود. In general, any native code that creates local references in a loop probably needs to do some manual deletion.

هنگام استفاده از مرجع‌های سراسری مراقب باشید. ارجاع‌های سراسری ممکن است اجتناب‌ناپذیر باشند، اما اشکال‌زدایی آن‌ها دشوار است و می‌توانند باعث (سوء)رفتارهای حافظه شوند که تشخیص آن‌ها دشوار است. اگر همه چیز برابر باشد، راه‌حلی که ارجاع‌های جهانی کمتری داشته باشد احتمالاً بهتر است.

رشته‌های UTF-8 و UTF-16

زبان برنامه‌نویسی جاوا از UTF-16 استفاده می‌کند. برای راحتی، JNI روش‌هایی را ارائه می‌دهد که با UTF-8 اصلاح‌شده نیز کار می‌کنند. کدبندی اصلاح‌شده برای کد C مفید است زیرا \u0000 را به‌جای 0x00 به‌صورت 0xc0 0x80 کدبندی می‌کند. نکته خوب این است که می‌توانید روی داشتن رشته‌های با پایان صفر به سبک C حساب کنید، که برای استفاده با توابع رشته libc استاندارد مناسب است. نکته منفی این است که نمی‌توانید داده‌های UTF-8 دلخواه را به JNI ارسال کنید و انتظار داشته باشید که به‌درستی کار کند.

برای دریافت نمایش UTF-16 از String، از GetStringChars استفاده کنید. توجه داشته باشید که رشته‌های UTF-16 به صفر ختم نمی‌شوند و \u0000 مجاز است، بنابراین باید طول رشته و همچنین اشاره‌گر jchar را حفظ کنید.

فراموش نکنید رشته‌هایی را که Get کرده‌اید Release کنید. توابع رشته‌ای jchar* یا jbyte* را برمی‌گردانند که اشاره‌گرهای سبک C به داده‌های اولیه هستند، نه ارجاع‌های محلی. تضمین می‌شود که این ارجاع‌ها تا زمانی که Release فراخوانی شود معتبر هستند، یعنی وقتی روش بومی برمی‌گردد، این ارجاع‌ها آزاد نمی‌شوند.

داده‌های ارسال‌شده به NewStringUTF باید در قالب Modified UTF-8 باشد. یک اشتباه رایج این است که داده‌های نویسه از یک فایل یا جریان شبکه خوانده شود و بدون فیلتر کردن به NewStringUTF تحویل داده شود. مگر اینکه بدانید داده‌ها MUTF-8 معتبر (یا ASCII‏ ۷ بیتی، که زیرمجموعه سازگاری است) هستند، باید نویسه‌های نامعتبر را حذف کنید یا آن‌ها را به فرم Modified UTF-8 مناسب تبدیل کنید. اگر این کار را نکنید، تبدیل UTF-16 احتمالاً نتایج غیرمنتظره‌ای ارائه خواهد داد. CheckJNI—که به‌طور پیش‌فرض برای شبیه‌سازها روشن است—رشته‌ها را اسکن می‌کند و اگر ورودی نامعتبر دریافت کند، VM را لغو می‌کند.

قبل‌از Android 8، معمولاً کار با رشته‌های UTF-16 سریع‌تر بود زیرا Android نیازی به کپی در GetStringChars نداشت، درحالی‌که GetStringUTFChars به تخصیص و تبدیل به UTF-8 نیاز داشت. Android 8 نمایش String را تغییر داد تا از ۸ بیت برای هر نویسه برای رشته‌های ASCII استفاده کند (برای صرفه‌جویی در حافظه) و شروع به استفاده از جمع‌آوری‌کننده زباله متحرک کرد. این ویژگی‌ها تعداد مواردی را که ART می‌تواند اشاره‌گری به داده‌های String بدون ایجاد کپی ارائه دهد، حتی برای GetStringCritical، به‌شدت کاهش می‌دهد. بااین‌حال، اگر اکثر رشته‌های پردازش‌شده توسط کد کوتاه باشند، می‌توان بااستفاده از میان‌گیر اختصاص‌یافته به پشته و GetStringRegion یا GetStringUTFRegion در اکثر موارد از اختصاص دادن و لغو اختصاص دادن اجتناب کرد. برای مثال:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr<jchar[]> heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

آرایه‌های اولیه

JNI توابعی برای دسترسی به محتوای اشیای آرایه ارائه می‌دهد. درحالی‌که آرایه‌های اشیاء باید هربار یک ورودی قابل‌دسترسی باشند، آرایه‌های اولیه را می‌توان مستقیماً خواند و نوشت، گویی در C اعلام شده‌اند.

برای اینکه میانای کاربری تا حد امکان کارآمد باشد و درعین‌حال پیاده‌سازی VM محدود نشود، خانواده تماس‌های Get<PrimitiveType>ArrayElements به زمان اجرا اجازه می‌دهد تا اشاره‌گری به عناصر واقعی برگرداند، یا مقداری حافظه اختصاص دهد و یک کپی ایجاد کند. در هر دو حالت، تضمین می‌شود که اشاره‌گر خام برگردانده‌شده تا زمانی که فراخوانی Release مربوطه صادر شود معتبر باشد (که به این معنی است که اگر داده‌ها کپی نشده باشند، شیء آرایه پین خواهد شد و نمی‌توان آن را به‌عنوان بخشی از فشرده‌سازی توده جابه‌جا کرد). باید هر آرایه‌ای را که Get می‌کنید Release کنید. همچنین، اگر تماس Get ناموفق بود، باید مطمئن شوید که کد شما بعداً سعی نمی‌کند اشاره‌گر NULL را Release کند.

می‌توانید با ارسال یک اشاره‌گر غیرتهی برای آرگومان isCopy، تعیین کنید که داده‌ها کپی شده‌اند یا نه. این مورد به‌ندرت مفید است.

فراخوانی Release آرگومان mode را می‌گیرد که می‌تواند یکی از سه مقدار را داشته باشد. کنش‌های انجام‌شده توسط زمان اجرا به این بستگی دارد که آیا اشاره‌گری به داده‌های واقعی برگردانده است یا نسخه‌ای از آن:

  • 0
    • واقعی: سنجاق شیء آرایه برداشته شده است.
    • کپی: داده‌ها کپی می‌شوند. میان‌گیر با نسخه کپی آزاد می‌شود.
  • JNI_COMMIT
    • واقعی: کاری انجام نمی‌دهد.
    • کپی: داده‌ها کپی می‌شوند. بافر با کپی آزاد نشده است.
  • JNI_ABORT
    • واقعی: سنجاق شیء آرایه برداشته شده است. نوشته‌های قبلی لغو نمی‌شوند.
    • کپی: میان‌گیر حاوی کپی آزاد می‌شود؛ هرگونه تغییر در آن ازدست می‌رود.

یکی از دلایل بررسی پرچم isCopy این است که بدانید آیا باید Release را با JNI_COMMIT پس‌از ایجاد تغییرات در آرایه فراخوانی کنید یا نه — اگر بین ایجاد تغییرات و اجرای کدی که از محتوای آرایه استفاده می‌کند جابه‌جا می‌شوید، ممکن است بتوانید از ثبت بدون عملیات صرف‌نظر کنید. دلیل احتمالی دیگر برای بررسی پرچم، رسیدگی کارآمد به JNI_ABORT است. برای مثال، ممکن است بخواهید آرایه‌ای را دریافت کنید، آن را در محل تغییر دهید، قطعاتی را به توابع دیگر منتقل کنید، و سپس تغییرات را کنار بگذارید. اگر می‌دانید که JNI نسخه جدیدی برای شما ایجاد می‌کند، نیازی به ایجاد نسخه «قابل‌ویرایش» دیگری نیست. اگر JNI نسخه اصلی را به شما می‌دهد، باید رونوشت خودتان را بسازید.

این یک اشتباه رایج است (که در کد نمونه تکرار شده است) که فرض کنید می‌توانید از فراخوانی Release صرف‌نظر کنید اگر *isCopy نادرست باشد. این‌طور نیست. اگر میان‌گیر کپی اختصاص داده نشده باشد، حافظه اصلی باید پین شود و نمی‌تواند توسط زباله‌روب جابه‌جا شود.

همچنین توجه داشته باشید که پرچم JNI_COMMIT آرایه را آزاد نمی‌کند، و درنهایت باید Release را دوباره با پرچم دیگری فراخوانی کنید.

تماس‌های منطقه

جایگزینی برای تماس‌هایی مثل Get<Type>ArrayElements و GetStringChars وجود دارد که وقتی فقط می‌خواهید داده‌ها را کپی کنید یا از آن‌ها کپی بگیرید، می‌تواند بسیار مفید باشد. موارد زیر را درنظر بگیرید:

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

این کار آرایه را می‌گیرد، اولین عناصر بایت len را از آن کپی می‌کند، و سپس آرایه را آزاد می‌کند. بسته به پیاده‌سازی، فراخوانی Get محتوای آرایه را سنجاق یا کپی می‌کند. کد داده‌ها را کپی می‌کند (شاید برای بار دوم)، سپس Release را فراخوانی می‌کند؛ در این مورد JNI_ABORT تضمین می‌کند که احتمال کپی سوم وجود نداشته باشد.

می‌توان همین کار را به‌صورت ساده‌تر انجام داد:

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

این کار چند مزیت دارد:

  • به یک فراخوانی JNI به‌جای ۲ فراخوانی نیاز دارد که سربار را کاهش می‌دهد.
  • نیازی به سنجاق کردن یا کپی‌های داده اضافی ندارد.
  • خطر خطای برنامه‌نویس را کاهش می‌دهد — خطر فراموش کردن فراخوانی Release پس‌از ناموفق بودن چیزی وجود ندارد.

به‌طور مشابه، می‌توانید از فراخوانی Set<Type>ArrayRegion برای کپی کردن داده‌ها در آرایه، و از GetStringRegion یا GetStringUTFRegion برای کپی کردن نویسه‌ها از String استفاده کنید.

استثناها

وقتی استثنایی درانتظار است، نباید اکثر توابع JNI را فراخوانی کنید. کد شما باید متوجه استثنا شود (ازطریق مقدار برگشتی تابع، ExceptionCheck، یا ExceptionOccurred) و برگردد، یا استثنا را پاک کند و آن را مدیریت کند.

تنها توابع JNI که مجاز هستید درحین معلق بودن استثنا فراخوانی کنید عبارت‌اند از:

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

Many JNI calls can throw an exception, but often provide a simpler way of checking for failure. برای مثال، اگر NewString مقدار غیرتهی برگرداند، نیازی نیست استثنا را بررسی کنید. بااین‌حال، اگر متدی را فراخوانی کنید (بااستفاده از تابعی مانند CallObjectMethod)، باید همیشه وجود استثنا را بررسی کنید، زیرا اگر استثنایی ایجاد شده باشد، مقدار برگشتی معتبر نخواهد بود.

توجه داشته باشید که استثناهای ایجادشده توسط کد مدیریت‌شده، چارچوب‌های پشته‌ای بومی را باز نمی‌کنند. (و استثناهای ++C، که معمولاً در Android توصیه نمی‌شوند، نباید در سراسر مرز انتقال JNI از کد ++C به کد مدیریت‌شده ایجاد شوند.) دستورالعمل‌های JNI Throw و ThrowNew فقط یک اشاره‌گر استثنا در رشته فعلی تنظیم می‌کنند. پس‌از بازگشت به کد مدیریت‌شده از کد بومی، استثنا ذکر می‌شود و به‌طور مناسب مدیریت می‌شود.

کد بومی می‌تواند با فراخوانی ExceptionCheck یا ExceptionOccurred استثنا را «بگیرد» و آن را با ExceptionClear پاک کند. طبق معمول، دور انداختن استثناها بدون رسیدگی به آن‌ها می‌تواند منجر به مشکلاتی شود.

هیچ تابع داخلی‌ای برای دستکاری خود شیء Throwable وجود ندارد، بنابراین اگر می‌خواهید (مثلاً) رشته استثنا را دریافت کنید، باید کلاس Throwable را پیدا کنید، شناسه روش را برای getMessage "()Ljava/lang/String;" جستجو کنید، آن را فراخوانی کنید، و اگر نتیجه غیرتهی بود، از GetStringUTFChars استفاده کنید تا چیزی را دریافت کنید که بتوانید به printf(3) یا معادل آن تحویل دهید.

بررسی گسترده

JNI خطایابی بسیار کمی انجام می‌دهد. خطاها معمولاً منجر به خرابی می‌شوند. ‫Android همچنین حالتی به‌نام CheckJNI ارائه می‌دهد که در آن اشاره‌گرهای جدول تابع JavaVM و JNIEnv به جدول‌هایی از توابع تغییر می‌کنند که قبل‌از فراخوانی پیاده‌سازی استاندارد، یک سری بررسی‌های گسترده انجام می‌دهند.

بررسی‌های اضافی شامل موارد زیر است:

  • آرایه‌ها: تلاش برای اختصاص دادن آرایه با اندازه منفی.
  • اشاره‌گرهای بد: ارسال jarray/jclass/jobject/jstring بد به فراخوانی JNI، یا ارسال اشاره‌گر NULL به فراخوانی JNI با آرگومان غیرقابل‌تهی.
  • نام کلاس‌ها: ارسال هر چیزی به‌جز سبک «java/lang/String» نام کلاس به فراخوانی JNI.
  • تماس‌های حیاتی: برقراری تماس JNI بین یک get «حیاتی» و انتشار مربوط به آن.
  • Direct ByteBuffers: passing bad arguments to NewDirectByteBuffer.
  • استثناها: برقراری تماس JNI درحالی‌که استثنایی درانتظار است.
  • JNIEnv*s: using a JNIEnv* from the wrong thread.
  • jfieldIDs: using a NULL jfieldID, or using a jfieldID to set a field to a value of the wrong type (trying to assign a StringBuilder to a String field, say), or using a jfieldID for a static field to set an instance field or vice versa, or using a jfieldID from one class with instances of another class.
  • jmethodIDs: استفاده از نوع اشتباه jmethodID هنگام برقراری تماس Call*Method JNI: نوع بازگشت نادرست، عدم تطابق ایستا/غیرایستا، نوع اشتباه برای «this» (برای تماس‌های غیرایستا) یا کلاس اشتباه (برای تماس‌های ایستا).
  • مراجع: استفاده از DeleteGlobalRef/DeleteLocalRef در نوع اشتباه مرجع.
  • حالت‌های انتشار: ارسال حالت انتشار نامناسب به تماس انتشار (چیزی غیراز 0، JNI_ABORT، یا JNI_COMMIT).
  • ایمنی تایپی: برگرداندن نوع ناسازگار از روش بومی (مثلاً برگرداندن StringBuilder از روشی که برای برگرداندن String اعلام شده است).
  • ‫UTF-8: ارسال توالی بایت Modified UTF-8 نامعتبر به فراخوانی JNI.

(دسترس‌پذیری روش‌ها و فیلدها هنوز بررسی نشده است: محدودیت‌های دسترسی برای کد بومی اعمال نمی‌شود.)

چندین روش برای فعال کردن CheckJNI وجود دارد.

اگر از شبیه‌ساز استفاده می‌کنید، CheckJNI به‌طور پیش‌فرض روشن است.

اگر دستگاه با دسترسی ریشه‌ای دارید، می‌توانید از توالی فرمان‌های زیر برای بازراه‌اندازی زمان اجرا با CheckJNI فعال استفاده کنید:

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

در هریک از این موارد، وقتی زمان اجرا شروع می‌شود، چیزی شبیه به این را در خروجی logcat خواهید دید:

D AndroidRuntime: CheckJNI is ON

اگر دستگاه معمولی دارید، می‌توانید از فرمان زیر استفاده کنید:

adb shell setprop debug.checkjni 1

این کار بر برنامه‌هایی که درحال‌اجرا هستند تأثیر نمی‌گذارد، اما CheckJNI برای هر برنامه‌ای که از آن نقطه به بعد راه‌اندازی شود فعال خواهد شد. (تغییر دادن ویژگی به هر مقدار دیگری یا راه‌اندازی مجدد ساده CheckJNI را دوباره غیرفعال می‌کند.) در این حالت، دفعه بعدی که برنامه‌ای شروع می‌شود، چیزی شبیه به این را در خروجی logcat خود خواهید دید:

D Late-enabling CheckJNI

همچنین می‌توانید مشخصه android:debuggable را در مانیفست برنامه‌تان تنظیم کنید تا CheckJNI را فقط برای برنامه‌تان روشن کنید. توجه داشته باشید که ابزارهای ساخت Android این کار را به‌طور خودکار برای انواع ساخت خاصی انجام خواهند داد.

کتابخانه‌های بومی

می‌توانید کد بومی را از کتابخانه‌های مشترک با System.loadLibrary استاندارد بار کنید.

در عمل، نسخه‌های قدیمی‌تر Android در PackageManager اشکالاتی داشتند که باعث می‌شد نصب و به‌روزرسانی کتابخانه‌های بومی غیرقابل‌اعتماد باشد. پروژه ReLinker راه‌حل‌هایی برای این مشکل و دیگر مشکلات بار کردن کتابخانه بومی ارائه می‌دهد.

‫System.loadLibrary (یا ReLinker.loadLibrary) را از مقداردهی اولیه کلاس ایستا فراخوانی کنید. متغیر مستقل نام کتابخانه «تزیین‌نشده» است، بنابراین برای بار کردن libfubar.so باید "fubar" را وارد کنید.

اگر فقط یک کلاس با روش‌های بومی دارید، منطقی است که فراخوانی System.loadLibrary در مقداردهی اولیه ایستا برای آن کلاس باشد. درغیراین‌صورت، ممکن است بخواهید تماس را از Application برقرار کنید تا مطمئن شوید کتابخانه همیشه بار می‌شود، و همیشه زود بار می‌شود.

دو روش وجود دارد که زمان اجرا می‌تواند روش‌های بومی شما را پیدا کند. می‌توانید آن‌ها را به‌طور صریح با RegisterNatives() ثبت کنید، یا می‌توانید به زمان اجرا اجازه دهید آن‌ها را به‌طور پویا با dlsym() جستجو کند. اکثر برنامه‌ها باید RegisterNatives() را از JNI_OnLoad() فراخوانی کنند. این کار ثبت پیشاپیش روش‌های بومی را با نگاشت صریح انجام می‌دهد. نوشتن این کد به کمی کار اولیه بیشتر نیاز دارد، اما مزایای زیر را دارد:

  1. اشتباهات در مراحل اولیه شناسایی می‌شوند. تابع نام‌گذاری‌شده نادرست باعث ایجاد خطا درطول بار کردن کتابخانه می‌شود.
  2. تنها نمادی که معمولاً باید صادر شود JNI_OnLoad() است. برخی‌از موارد استفاده ممکن است علاوه‌براین به ANativeActivity_OnCreate() یا موارد مشابه نیاز داشته باشند.
  3. Android Studio می‌تواند به شما هشدار دهد و اشتباهات رایج در نام‌های کلاس و امضاهای تابع را به‌طور خودکار اصلاح کند.

درمقابل، مزیت اجازه دادن به زمان اجرا برای کشف توابع شما ازطریق dlsym() این است که کد کمتری برای نوشتن وجود دارد و در مواردی که توابع JNI شما معمولاً هرگز فراخوانی نمی‌شوند، ارزان‌تر است.

برای استفاده از RegisterNatives:

  • تابع JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) را ارائه دهید.
  • در JNI_OnLoad، همه روش‌های بومی‌تان را بااستفاده از RegisterNatives ثبت کنید.
  • با متن نسخه (ترجیحاً) بسازید یا از -fvisibility=hidden استفاده کنید تا فقط JNI_OnLoad شما از کتابخانه‌تان صادر شود. این کار کد سریع‌تر و کوچک‌تری تولید می‌کند و از تداخل احتمالی با کتابخانه‌های دیگری که در برنامه‌تان بار شده‌اند جلوگیری می‌کند (اما اگر برنامه‌تان در کد بومی ازکار بیفتد، ردیابی‌های پشته‌ای با فایده کمتری ایجاد می‌کند).

مقداردهی اولیه ایستا باید به این شکل باشد:

کاتلین

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

جاوا

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

تابع JNI_OnLoad اگر به زبان C++‎ نوشته شده باشد، باید چیزی شبیه به این باشد:

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

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

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

    return JNI_VERSION_1_6;
}

برای استفاده از «اکتشاف» روش‌های بومی، باید آن‌ها را به‌روش خاصی نام‌گذاری کنید (برای جزئیات، مشخصات JNI را ببینید). این یعنی اگر امضای روشی اشتباه باشد، تا زمانی که روش برای اولین‌بار واقعاً فراخوانی نشود، از آن مطلع نخواهید شد.

هرگونه تماس FindClass برقرارشده از JNI_OnLoad کلاس‌ها را در زمینه بارکننده کلاسی که برای بار کردن کتابخانه مشترک استفاده شده است حل‌وفصل می‌کند. وقتی از زمینه‌های دیگر فراخوانی می‌شود، FindClass از بارکننده کلاس مرتبط با روش در بالای پشته Java استفاده می‌کند، یا اگر وجود نداشته باشد (چون فراخوانی از یک رشته بومی است که به‌تازگی متصل شده است)، از بارکننده کلاس «سیستم» استفاده می‌کند. بارکننده کلاس سیستم از کلاس‌های برنامه‌تان اطلاعی ندارد، بنابراین نمی‌توانید کلاس‌های خودتان را با FindClass در آن زمینه جستجو کنید. این امر JNI_OnLoad را به مکانی مناسب برای جستجو و ذخیره موقت کلاس‌ها تبدیل می‌کند: وقتی jclass مرجع سراسری معتبری داشته باشید می‌توانید از آن در هر رشته پیوست‌شده‌ای استفاده کنید.

تماس‌های داخلی سریع‌تر با @FastNative و @CriticalNative

روش‌های بومی را می‌توان با @FastNative یا @CriticalNative (اما نه هر دو) حاشیه‌نویسی کرد تا انتقال بین کد مدیریت‌شده و بومی تسریع شود. اما این حاشیه‌نویسی‌ها با تغییرات خاصی در رفتار همراه هستند که باید قبل‌از استفاده به‌دقت درنظر گرفته شوند. درحالی‌که ما به‌طور خلاصه به این تغییرات در زیر اشاره می‌کنیم، لطفاً برای جزئیات به اسناد مراجعه کنید.

حاشیه‌نویسی @CriticalNative فقط می‌تواند برای روش‌های بومی اعمال شود که از اشیای مدیریت‌شده استفاده نمی‌کنند (در پارامترها یا مقادیر برگشتی، یا به‌عنوان this ضمنی)، و این حاشیه‌نویسی ABI انتقال JNI را تغییر می‌دهد. پیاده‌سازی بومی باید پارامترهای JNIEnv و jclass را از امضای تابع خود حذف کند.

هنگام اجرای روش @FastNative یا @CriticalNative، جمع‌آوری زباله نمی‌تواند رشته را برای کار ضروری تعلیق کند و ممکن است مسدود شود. از این حاشیه‌نویسی‌ها برای روش‌های طولانی‌مدت، ازجمله روش‌های معمولاً سریع اما به‌طور کلی نامحدود، استفاده نکنید. به‌ویژه، کد نباید عملیات ورودی/خروجی قابل‌توجهی انجام دهد یا قفل‌های بومی را که می‌توانند برای مدت طولانی نگه داشته شوند، دریافت کند.

این حاشیه‌نویسی‌ها از Android 8 برای استفاده سیستم پیاده‌سازی شدند و در Android 14 به API عمومی آزمایش‌شده با CTS تبدیل شدند. این بهینه‌سازی‌ها احتمالاً در دستگاه‌های Android نسخه ۸ تا ۱۳ نیز کار می‌کنند (البته بدون ضمانت‌های قوی CTS)، اما جستجوی پویای روش‌های بومی فقط در Android نسخه ۱۲ و بالاتر پشتیبانی می‌شود، ثبت صریح با JNI RegisterNatives برای اجرا در Android نسخه ۸ تا ۱۱ اکیداً الزامی است. این حاشیه‌نویسی‌ها در Android 7- نادیده گرفته می‌شوند، عدم تطابق ABI برای @CriticalNative منجر به مرتب‌سازی اشتباه آرگومان و احتمالاً ازکارافتادن می‌شود.

برای روش‌های حیاتی عملکرد که به این حاشیه‌نویسی‌ها نیاز دارند، اکیداً توصیه می‌شود روش(ها) را به‌طور صریح با JNI RegisterNatives ثبت کنید، به‌جای اینکه به «کشف» روش‌های بومی مبتنی بر نام تکیه کنید. برای دریافت عملکرد بهینه راه‌اندازی برنامه، توصیه می‌شود فراخوانندگان روش‌های @FastNative یا @CriticalNative را در نمایه پایه اضافه کنید. از Android 12، فراخوانی روش بومی @CriticalNative از روش مدیریت‌شده کامپایل‌شده تقریباً به‌اندازه فراخوانی غیردرخط در C/C++‎ ارزان است، به‌شرطی که همه آرگومان‌ها در ثبات‌ها جا شوند (برای مثال تا ۸ آرگومان صحیح و تا ۸ آرگومان ممیز شناور در arm64).

گاهی اوقات بهتر است یک روش بومی را به دو روش تقسیم کنید: یک روش بسیار سریع که ممکن است ناموفق باشد و روش دیگری که موارد کند را مدیریت می‌کند. برای مثال:

کاتلین

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

جاوا

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

ملاحظات ۶۴ بیتی

برای پشتیبانی از معماری‌هایی که از اشاره‌گرهای ۶۴ بیتی استفاده می‌کنند، هنگام ذخیره کردن اشاره‌گر به ساختار بومی در فیلد Java، به‌جای int از فیلد long استفاده کنید.

ویژگی‌های پشتیبانی‌نشده/سازگاری با نسخه‌های قدیمی

همه ویژگی‌های JNI 1.6 پشتیبانی می‌شوند، به‌جز موارد زیر:

  • DefineClass پیاده‌سازی نشده است. Android از بایت‌کدهای Java یا فایل‌های کلاس استفاده نمی‌کند، بنابراین ارسال داده‌های کلاس دودویی کار نمی‌کند.

برای سازگاری با نسخه‌های قدیمی‌تر Android، ممکن است لازم باشد از موارد زیر مطلع باشید:

  • جستجوی پویای توابع بومی

    تا Android 2.0 (Eclair)، نویسه «$» درطول جستجو برای نام‌های روش به‌درستی به «_00024» تبدیل نمی‌شد. برای حل این مشکل، باید از ثبت صریح استفاده کنید یا روش‌های بومی را از کلاس‌های داخلی خارج کنید.

  • درحال جدا کردن رشته‌ها

    تا Android 2.0 (Eclair)، امکان استفاده از تابع تخریب‌کننده 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)، مرجع‌های محلی درواقع اشاره‌گرهای مستقیم بودند. Ice Cream Sandwich ارجاع غیرمستقیم لازم برای پشتیبانی از جمع‌آورنده‌های زباله بهتر را اضافه کرد، اما این یعنی بسیاری از اشکالات JNI در نسخه‌های قدیمی‌تر قابل‌شناسایی نیستند. برای جزئیات بیشتر، تغییرات مرجع محلی JNI در ICS را ببینید.

    در نسخه‌های Android قبل‌از Android 8.0، تعداد ارجاع‌های محلی به حد مجاز مختص نسخه محدود می‌شود. از Android 8.0 به بعد، Android از ارجاع‌های محلی نامحدود پشتیبانی می‌کند.

  • تعیین نوع مرجع با GetObjectRefType

    تا Android 4.0 (Ice Cream Sandwich)، درنتیجه استفاده از اشاره‌گرهای مستقیم (به بالا مراجعه کنید)، پیاده‌سازی صحیح GetObjectRefType غیرممکن بود. درعوض، از روشی اکتشافی استفاده کردیم که به‌ترتیب جدول سراسری ضعیف، آرگومان‌ها، جدول محلی، و جدول سراسری را بررسی می‌کرد. اولین باری که اشاره‌گر مستقیم شما را پیدا کرد، گزارش داد که مرجع شما از نوعی است که درحال بررسی آن است. این یعنی، برای مثال، اگر GetObjectRefType را در jclass جهانی فراخوانی می‌کردید که اتفاقاً همان jclass بود که به‌عنوان آرگومان ضمنی به روش بومی ایستای شما ارسال شده بود، به‌جای JNIGlobalRefType، JNILocalRefType را دریافت می‌کردید.

  • ‫@FastNative و @CriticalNative

    تا Android 7، این حاشیه‌نویسی‌های بهینه‌سازی نادیده گرفته می‌شدند. عدم تطابق ABI برای @CriticalNative منجر به مرتب‌سازی نادرست آرگومان و احتمالاً ازکارافتادگی می‌شود.

    جستجوی پویای توابع بومی برای روش‌های @FastNative و @CriticalNative در Android 8-10 پیاده‌سازی نشده است و در Android 11 دارای اشکال‌های شناخته‌شده است. استفاده از این بهینه‌سازی‌ها بدون ثبت صریح با JNI RegisterNatives احتمالاً منجر به ازکارافتادن در Android 8-11 می‌شود.

  • FindClass ClassNotFoundException پرتاب می‌کند

    برای سازگاری با نسخه قدیمی، Android به‌جای NoClassDefFoundError، ClassNotFoundException را زمانی که کلاسی توسط FindClass پیدا نمی‌شود، برمی‌گرداند. این رفتار با Java reflection API 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). توجه داشته باشید که قبل‌از Ice Cream Sandwich، ماکرو JNIEXPORT نادرست بود، بنابراین استفاده از GCC جدید با jni.h قدیمی کار نخواهد کرد. می‌توانید از arm-eabi-nm استفاده کنید تا نمادها را همان‌طور که در کتابخانه ظاهر می‌شوند ببینید؛ اگر درهم‌ریخته به‌نظر می‌رسند (چیزی شبیه _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass به‌جای Java_Foo_myfunc)، یا اگر نوع نماد حرف کوچک «t» به‌جای حرف بزرگ «T» است، باید اعلامیه را اصلاح کنید.
    • برای ثبت صریح، خطاهای جزئی هنگام وارد کردن امضای روش. مطمئن شوید آنچه به تماس ثبت ارسال می‌کنید با امضای موجود در فایل گزارش مطابقت داشته باشد. به‌خاطر داشته باش که «ب» byte است و «ز» boolean است. مؤلفه‌های نام کلاس در امضاها با «L» شروع می‌شود، با «;» پایان می‌یابد، از «/» برای جدا کردن نام‌های بسته/کلاس استفاده می‌کند، و از «$» برای جدا کردن نام‌های کلاس داخلی (مثلاً Ljava/util/Map$Entry;) استفاده می‌کند.

استفاده از javah برای تولید خودکار سرصفحه‌های JNI ممکن است به جلوگیری از برخی مشکلات کمک کند.

پرسشگان: چرا FindClass کلاس من را پیدا نکرد؟

(بیشتر این توصیه‌ها به‌طور یکسان برای عدم موفقیت در یافتن روش‌ها با GetMethodID یا GetStaticMethodID، یا فیلدها با GetFieldID یا GetStaticFieldID کاربرد دارد.)

مطمئن شوید رشته نام کلاس قالب درستی داشته باشد. نام‌های کلاس JNI با نام بسته شروع می‌شوند و با خط مورب از هم جدا می‌شوند، مثل java/lang/String. اگر به‌دنبال کلاس آرایه هستید، باید با تعداد مناسبی از کروشه شروع کنید و همچنین باید کلاس را با «L» و «;» بپوشانید، بنابراین آرایه یک‌بعدی String به‌صورت [Ljava/lang/String; خواهد بود. اگر به‌دنبال کلاس داخلی هستید، به‌جای «.» از «$» استفاده کنید. به‌طور کلی، استفاده از javap در فایل .class روش خوبی برای پیدا کردن نام داخلی کلاس شما است.

اگر کوچک کردن کد را فعال می‌کنید، حتماً پیکربندی کنید که کدام کد حفظ شود. پیکربندی قوانین نگهداری مناسب مهم است زیرا درغیراین‌صورت ممکن است کوچک‌کننده کد کلاس‌ها، روش‌ها، یا فیلدهایی را که فقط از JNI استفاده می‌شوند حذف کند.

اگر نام کلاس درست به‌نظر می‌رسد، ممکن است با مشکل بارکننده کلاس مواجه شده باشید. ‫FindClass می‌خواهد جستجوی کلاس را در بارکننده کلاس مرتبط با کد شما شروع کند. پشته فراخوانی را بررسی می‌کند، که چیزی شبیه این خواهد بود:

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

روش برتر Foo.myfunc است. FindClass شیء ClassLoader مرتبط با کلاس Foo را پیدا می‌کند و از آن استفاده می‌کند.

این گزینه معمولاً همان کاری را می‌کند که می‌خواهید. اگر خودتان رشته‌ای ایجاد کنید (مثلاً با فراخوانی pthread_create و سپس پیوست کردن آن با AttachCurrentThread)، ممکن است با مشکل مواجه شوید. اکنون هیچ چارچوب پشته‌ای از برنامه‌تان وجود ندارد. اگر FindClass را از این رشته فراخوانی کنید، JavaVM به‌جای بارکننده کلاس مرتبط با برنامه‌تان، در بارکننده کلاس «سیستم» شروع خواهد شد، بنابراین تلاش‌ها برای یافتن کلاس‌های مختص برنامه ناموفق خواهد بود.

چند راه برای حل این مشکل وجود دارد:

  • جستجوهای FindClass را یک‌بار در JNI_OnLoad انجام دهید و ارجاع‌های کلاس را برای استفاده‌های بعدی در حافظه پنهان ذخیره کنید. هرگونه فراخوانی FindClass که به‌عنوان بخشی از اجرای JNI_OnLoad انجام شود، از بارکننده کلاس مرتبط با تابعی که System.loadLibrary را فراخوانی کرده است استفاده خواهد کرد (این یک قانون ویژه است که برای راحت‌تر کردن مقداردهی اولیه کتابخانه ارائه شده است). اگر کد برنامه‌تان کتابخانه را بار می‌کند، FindClass از بارکننده کلاس صحیح استفاده خواهد کرد.
  • با اعلام روش بومی خود برای گرفتن آرگومان Class و سپس ارسال Foo.class، نمونه‌ای از کلاس را به توابعی که به آن نیاز دارند ارسال کنید.
  • مرجع شیء ClassLoader را در جایی دردسترس ذخیره کنید و مستقیماً تماس‌های loadClass را صادر کنید. این کار به مقداری تلاش نیاز دارد.

پرسشگان: چگونه داده‌های خام را با کد بومی هم‌رسانی کنم؟

ممکن است در شرایطی قرار بگیرید که لازم باشد به میان‌گیر بزرگی از داده‌های خام هم از کد مدیریت‌شده و هم از کد بومی دسترسی داشته باشید. نمونه‌های رایج شامل دستکاری بیت‌مپ‌ها یا نمونه‌های صدا است. دو رویکرد پایه وجود دارد.

می‌توانید داده‌ها را در byte[] ذخیره کنید. این کار دسترسی بسیار سریع از کد مدیریت‌شده را ممکن می‌کند. اما در بخش بومی، تضمینی وجود ندارد که بتوانید بدون کپی کردن داده‌ها به آن‌ها دسترسی داشته باشید. در برخی پیاده‌سازی‌ها، GetByteArrayElements و GetPrimitiveArrayCritical اشاره‌گرهای واقعی به داده‌های خام در پشته مدیریت‌شده را برمی‌گردانند، اما در برخی دیگر، میان‌گیری را در پشته بومی اختصاص می‌دهند و داده‌ها را کپی می‌کنند.

راه دیگر این است که داده‌ها را در میان‌گیر بایت مستقیم ذخیره کنید. این می‌تواند با java.nio.ByteBuffer.allocateDirect یا تابع JNI NewDirectByteBuffer ایجاد شود. برخلاف میان‌گیرهای بایت معمولی، فضای ذخیره‌سازی در توده مدیریت‌شده اختصاص داده نمی‌شود و همیشه می‌توان مستقیماً از کد بومی به آن دسترسی داشت (نشانی را با GetDirectBufferAddress دریافت کنید). بسته به نحوه پیاده‌سازی دسترسی مستقیم به میان‌گیر بایت، دسترسی به داده‌ها از کد مدیریت‌شده می‌تواند بسیار کند باشد.

انتخاب اینکه از کدام‌یک استفاده کنید به دو عامل بستگی دارد:

  1. آیا بیشتر دسترسی‌ها به داده ازطریق کدهای نوشته‌شده در Java یا C/C++‎ انجام می‌شود؟
  2. اگر داده‌ها درنهایت به میانای برنامه‌سازی کاربردی سیستم ارسال می‌شوند، باید به چه شکلی باشند؟ (برای مثال، اگر داده‌ها درنهایت به تابعی منتقل شود که byte[]‎ می‌گیرد، پردازش در ByteBuffer مستقیم ممکن است عاقلانه نباشد.)

اگر برنده مشخصی وجود ندارد، از میان‌گیر بایت مستقیم استفاده کنید. پشتیبانی از آن‌ها مستقیماً در JNI تعبیه شده است و عملکرد باید در نسخه‌های بعدی بهبود یابد.