نکات JNI

JNI رابط بومی جاوا است. روشی را برای بایت کدی که اندروید از کد مدیریت شده (نوشته شده در زبان های برنامه نویسی جاوا یا کاتلین) کامپایل می کند تا با کد بومی (نوشته شده در C/C++) در تعامل باشد، تعریف می کند. JNI از نظر فروشنده خنثی است، از بارگذاری کد از کتابخانه های مشترک پویا پشتیبانی می کند، و در حالی که گاهی اوقات دست و پا گیر است، کارآمدی معقولی دارد.

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

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

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

نکات کلی

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

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

JavaVM و JNIEnv

JNI دو ساختار داده کلیدی، "JavaVM" و "JNIEnv" را تعریف می کند. هر دوی اینها اساساً نشانگرهای اشاره گر به جداول تابع هستند. (در نسخه ++C، آنها کلاس هایی هستند با یک اشاره گر به یک جدول تابع و یک تابع عضو برای هر تابع JNI که از طریق جدول غیرمستقیم می شود.) JavaVM توابع "Invocation Interface" را ارائه می دهد که به شما امکان می دهد یک را ایجاد و از بین ببرید. JavaVM. در تئوری شما می توانید چندین جاوا وی ام در هر فرآیند داشته باشید، اما اندروید فقط یک جاوای وی ام را اجازه می دهد.

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

JNIEnv برای ذخیره سازی thread-local استفاده می شود. به همین دلیل، شما نمی توانید یک JNIEnv را بین رشته ها به اشتراک بگذارید . اگر یک قطعه کد راه دیگری برای دریافت JNIEnv خود ندارد، باید JavaVM را به اشتراک بگذارید و GetEnv برای کشف JNIEnv رشته استفاده کنید. (با فرض اینکه یکی داشته باشد؛ AttachCurrentThread در زیر ببینید.)

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

موضوعات

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

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

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

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

رشته هایی که از طریق JNI متصل می شوند باید قبل از خروج DetachCurrentThread() را فراخوانی کنند . اگر کدنویسی مستقیماً دشوار است، در اندروید 2.0 (Eclair) و بالاتر می‌توانید از pthread_key_create() برای تعریف یک تابع تخریب‌کننده استفاده کنید که قبل از خروج رشته فراخوانی می‌شود و از آنجا DetachCurrentThread() را فراخوانی کنید. (از آن کلید با pthread_setspecific() برای ذخیره JNIEnv در thread-local-storage استفاده کنید؛ به این ترتیب به عنوان آرگومان به مخرب شما منتقل می شود.)

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

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

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

هر آرگومان به یک متد بومی ارسال می‌شود، و تقریباً هر شیئی که توسط یک تابع 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 به عنوان کلید استفاده نکنید.

برنامه نویسان موظفند مراجع محلی را "بیش از حد تخصیص ندهند". از نظر عملی، این بدان معناست که اگر تعداد زیادی مرجع محلی ایجاد می‌کنید، شاید در حین اجرای آرایه‌ای از اشیاء، به جای اینکه به JNI اجازه دهید این کار را برای شما انجام دهد، باید آنها را به صورت دستی با DeleteLocalRef آزاد کنید. این پیاده سازی فقط برای رزرو اسلات برای 16 مرجع محلی مورد نیاز است، بنابراین اگر به بیش از آن نیاز دارید، باید در حین حرکت آن را حذف کنید یا از EnsureLocalCapacity / PushLocalFrame برای رزرو بیشتر استفاده کنید.

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

یک مورد غیرعادی شایسته ذکر جداگانه است. اگر یک رشته بومی را با AttachCurrentThread ضمیمه کنید، کدی که اجرا می کنید هرگز به طور خودکار مراجع محلی را آزاد نمی کند تا زمانی که موضوع جدا شود. هر مرجع محلی که ایجاد می کنید باید به صورت دستی حذف شود. به طور کلی، هر کد بومی که ارجاعات محلی را در یک حلقه ایجاد می کند، احتمالاً نیاز به حذف دستی دارد.

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

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

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

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

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

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

قبل از Android 8، معمولاً کار با رشته‌های UTF-16 سریع‌تر بود، زیرا Android نیازی به کپی در GetStringChars نداشت، در حالی که GetStringUTFChars به ​​تخصیص و تبدیل به UTF-8 نیاز داشت. اندروید 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 تعریف شده‌اند.

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

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

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

  • 0
    • واقعی: شیء آرایه پین ​​نشده است.
    • کپی: داده ها کپی می شوند. بافر همراه با کپی آزاد می شود.
  • JNI_COMMIT
    • واقعی: هیچ کاری نمی کند.
    • کپی: داده ها کپی می شوند. بافر همراه با کپی آزاد نمی شود .
  • JNI_ABORT
    • واقعی: شیء آرایه پین ​​نشده است. نوشته های قبلی سقط نمی شوند.
    • کپی: بافر همراه با کپی آزاد می شود. هر تغییری در آن از بین می رود.

یکی از دلایل بررسی پرچم isCopy این است که بدانید آیا باید پس از ایجاد تغییرات در یک آرایه، Release با JNI_COMMIT فراخوانی کنید - اگر به طور متناوب بین ایجاد تغییرات و اجرای کدی که از محتویات آرایه استفاده می کند، می توانید از آن رد شوید. تعهد بدون عملیات یکی دیگر از دلایل احتمالی برای بررسی پرچم، مدیریت کارآمد 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 به جای 2، کاهش سربار.
  • نیازی به پین ​​کردن یا کپی اطلاعات اضافی ندارد.
  • خطر خطای برنامه نویس را کاهش می دهد - بدون خطر فراموشی تماس Release پس از خرابی چیزی.

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

استثنائات

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

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

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

بسیاری از تماس‌های JNI می‌توانند یک استثنا ایجاد کنند، اما اغلب راه ساده‌تری برای بررسی خرابی ارائه می‌دهند. به عنوان مثال، اگر NewString یک مقدار غیر NULL برمی گرداند، نیازی به بررسی استثنا نیست. با این حال، اگر متدی را فراخوانی می‌کنید (با استفاده از تابعی مانند CallObjectMethod )، همیشه باید یک استثنا را بررسی کنید، زیرا اگر یک استثنا پرتاب شود، مقدار بازگشتی معتبر نخواهد بود.

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

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

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

بررسی گسترده

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

بررسی های اضافی عبارتند از:

  • آرایه ها: تلاش برای تخصیص یک آرایه با اندازه منفی.
  • نشانگرهای بد: ارسال یک jarray/jclass/jobject/jstring بد به یک فراخوانی JNI، یا ارسال نشانگر NULL به فراخوانی JNI با آرگومان غیر قابل تهی.
  • نام کلاس: ارسال هر چیزی جز سبک "java/lang/string" نام کلاس به فراخوانی JNI.
  • تماس‌های بحرانی: برقراری تماس JNI بین دریافت «بحرانی» و انتشار متناظر آن.
  • Direct ByteBuffer: ارسال آرگومان های بد به NewDirectByteBuffer .
  • استثناها: برقراری تماس JNI در حالی که یک استثنا در انتظار وجود دارد.
  • JNIEnv*s: استفاده از JNIEnv* از رشته اشتباه.
  • jfieldIDs: استفاده از یک jfieldID NULL، یا استفاده از jfieldID برای تنظیم یک فیلد به مقداری از نوع اشتباه (مثلاً تلاش برای اختصاص یک StringBuilder به یک فیلد String)، یا استفاده از یک jfieldID برای یک فیلد ثابت برای تنظیم یک فیلد نمونه یا برعکس، یا با استفاده از jfieldID از یک کلاس با نمونه هایی از کلاس دیگر.
  • jmethodIDs: استفاده از نوع اشتباه jmethodID هنگام برقراری Call*Method JNI: نوع برگشتی نادرست، عدم تطابق استاتیک/غیر استاتیک، نوع اشتباه برای 'this' (برای تماس های غیر ایستا) یا کلاس اشتباه (برای تماس های ایستا).
  • منابع: استفاده از DeleteGlobalRef / DeleteLocalRef در نوع اشتباه مرجع.
  • حالت‌های انتشار: انتقال حالت انتشار بد به تماس انتشار (چیزی غیر از 0 ، JNI_ABORT ، یا JNI_COMMIT ).
  • نوع ایمنی: برگرداندن یک نوع ناسازگار از متد بومی شما (بازگرداندن StringBuilder از روشی که برای بازگرداندن یک رشته، مثلاً اعلام شده است).
  • UTF-8: ارسال یک دنباله بایت UTF-8 اصلاح شده نامعتبر به فراخوانی JNI.

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

راه های مختلفی برای فعال کردن CheckJNI وجود دارد.

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

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

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

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

D AndroidRuntime: CheckJNI is ON

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

adb shell setprop debug.checkjni 1

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

D Late-enabling CheckJNI

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

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

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

در عمل، نسخه‌های قدیمی‌تر اندروید باگ‌هایی در 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 شما از کتابخانه شما صادر شود. این کد سریع‌تر و کوچک‌تر تولید می‌کند و از برخورد احتمالی با دیگر کتابخانه‌های بارگذاری‌شده در برنامه شما جلوگیری می‌کند (اما اگر برنامه شما در کد اصلی خراب شود، ردپای پشته‌های مفید کمتری ایجاد می‌کند).

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

کاتلین

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 از بارگیری کلاس مرتبط با متد در بالای پشته جاوا استفاده می کند، یا اگر یکی نباشد (چون فراخوانی از یک رشته بومی است که به تازگی ضمیمه شده است) از "سیستم" استفاده می کند. کلاس لودر بارگذار کلاس سیستم از کلاس های برنامه شما اطلاعی ندارد، بنابراین نمی توانید کلاس های خود را با FindClass در آن زمینه جستجو کنید. این امر JNI_OnLoad به مکانی مناسب برای جستجو و ذخیره کلاس‌ها تبدیل می‌کند: هنگامی که یک مرجع معتبر جهانی jclass دارید، می‌توانید آن را از هر رشته پیوست شده استفاده کنید.

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

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

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

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

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

برای روش‌های حیاتی عملکردی که به این حاشیه‌نویسی‌ها نیاز دارند، اکیداً توصیه می‌شود که به‌جای تکیه بر «کشف» روش‌های بومی مبتنی بر نام، روش(های) را با JNI RegisterNatives ثبت کنید. برای دریافت عملکرد بهینه راه‌اندازی برنامه، توصیه می‌شود تماس‌گیرندگان روش‌های @FastNative یا @CriticalNative را در نمایه پایه قرار دهید. از اندروید 12، فراخوانی به یک متد Native @CriticalNative از یک روش مدیریت شده کامپایل شده تقریباً به اندازه یک فراخوانی غیرحضوری در C/C++ ارزان است تا زمانی که همه آرگومان‌ها در رجیسترها قرار بگیرند (مثلا تا 8 انتگرال و تا 8 آرگومان های ممیز شناور در 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);

ملاحظات 64 بیتی

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

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

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

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

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

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

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

  • جدا کردن نخ ها

    تا قبل از Android 2.0 (Eclair)، استفاده از تابع تخریب کننده pthread_key_create برای جلوگیری از بررسی "رشته باید قبل از خروج جدا شود" امکان پذیر نبود. (زمان اجرا همچنین از یک تابع تخریب کننده کلید pthread استفاده می کند، بنابراین باید دید که کدام یک ابتدا فراخوانی می شود.)

  • مراجع جهانی ضعیف

    تا قبل از Android 2.2 (Froyo)، مراجع جهانی ضعیف اجرا نمی شدند. نسخه های قدیمی تر تلاش برای استفاده از آنها را به شدت رد می کنند. برای تست پشتیبانی می توانید از ثابت های نسخه پلتفرم اندروید استفاده کنید.

    تا قبل از 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 ، تعداد مراجع محلی محدود به یک محدودیت خاص نسخه است. با شروع اندروید 8.0، اندروید از مراجع محلی نامحدود پشتیبانی می کند.

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

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

  • @FastNative و @CriticalNative

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

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

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

    برای سازگاری به عقب، زمانی که یک کلاس توسط FindClass پیدا نمی شود، Android به جای NoClassDefFoundError ClassNotFoundException پرتاب می کند. این رفتار با بازتاب 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 ) ، یا اگر نوع نماد یک حروف کوچک است و نه یک حروف بزرگ ، پس باید اعلامیه را تنظیم کنید.
    • برای ثبت نام صریح ، خطاهای جزئی هنگام ورود به امضای روش. اطمینان حاصل کنید که آنچه شما به تماس ثبت نام منتقل می کنید با امضای موجود در پرونده log مطابقت دارد. به یاد داشته باشید که "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; خواهد بود. [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 از لودر کلاس صحیح استفاده می کند.
  • نمونه ای از کلاس را با اعلام روش مادری خود برای گرفتن یک آرگومان کلاس و سپس عبور از Foo.class در کارکردهایی که به آن نیاز دارند ، منتقل کنید.
  • حافظه را به شیء ClassLoader در جایی مفید ، ذخیره کنید و مستقیماً تماس های loadClass را صادر کنید. این نیاز به تلاش دارد.

سؤالات متداول: چگونه می توانم داده های خام را با کد بومی به اشتراک بگذارم؟

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

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

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

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

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

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