เคล็ดลับเกี่ยวกับ JNI

JNI คือ Java Native Interface ซึ่งกำหนดวิธีให้ไบต์โค้ดที่ Android คอมไพล์จากโค้ดที่มีการจัดการ (เขียนในภาษาโปรแกรม Java หรือ Kotlin) โต้ตอบกับโค้ดที่มาพร้อมเครื่อง (เขียนใน C/C++) ได้ JNI ไม่ขึ้นอยู่กับผู้ให้บริการ รองรับการโหลดโค้ดจากไลบรารีที่ใช้ร่วมกันแบบไดนามิก และแม้ว่าบางครั้งจะซับซ้อน แต่ก็มีประสิทธิภาพพอสมควร

หมายเหตุ: เนื่องจาก Android คอมไพล์ Kotlin เป็นไบต์โค้ดที่ใช้กับ ART ได้ในลักษณะเดียวกับภาษาโปรแกรม Java คุณจึงใช้คำแนะนำในหน้านี้กับทั้งภาษาโปรแกรม Kotlin และ Java ได้ในแง่ของสถาปัตยกรรม JNI และค่าใช้จ่ายที่เกี่ยวข้อง ดูข้อมูลเพิ่มเติมได้ที่ Kotlin และ Android

หากยังไม่คุ้นเคย โปรดอ่านข้อกำหนดของ Java Native Interface เพื่อทำความเข้าใจวิธีการทำงานของ JNI และฟีเจอร์ที่พร้อมใช้งาน คุณอาจไม่เข้าใจ บางส่วนของอินเทอร์เฟซในทันทีที่อ่าน ครั้งแรก ดังนั้นส่วนถัดไป 2-3 ส่วนอาจมีประโยชน์สำหรับคุณ

หากต้องการเรียกดูการอ้างอิง JNI ทั่วโลกและดูว่ามีการสร้างและลบการอ้างอิง JNI ทั่วโลกที่ใด ให้ใช้มุมมองฮีป JNI ใน Memory Profiler ใน Android Studio 3.2 ขึ้นไป

เคล็ดลับทั่วไป

พยายามลดร่องรอยของเลเยอร์ JNI มีหลายมิติที่ต้องพิจารณาในที่นี้ โซลูชัน JNI ควรพยายามปฏิบัติตามหลักเกณฑ์ต่อไปนี้ (เรียงตามลำดับความสำคัญจากมากไปน้อย โดยเริ่มจากหลักเกณฑ์ที่สำคัญที่สุด)

  • ลดการจัดเรียงทรัพยากรในเลเยอร์ JNI การส่งผ่านข้อมูลในเลเยอร์ JNI มีค่าใช้จ่ายที่สำคัญ พยายามออกแบบอินเทอร์เฟซที่ลดปริมาณ ข้อมูลที่คุณต้องจัดเรียงและความถี่ที่คุณต้องจัดเรียงข้อมูล
  • หลีกเลี่ยงการสื่อสารแบบไม่พร้อมกันระหว่างโค้ดที่เขียนในภาษาโปรแกรมที่มีการจัดการ และโค้ดที่เขียนใน C++ เมื่อเป็นไปได้ ซึ่งจะช่วยให้ดูแลรักษาอินเทอร์เฟซ JNI ได้ง่ายขึ้น โดยปกติแล้ว คุณจะทำให้การอัปเดต UI แบบไม่พร้อมกันง่ายขึ้นได้โดยเก็บการอัปเดตแบบไม่พร้อมกันไว้ในภาษาเดียวกับ UI ตัวอย่างเช่น แทนที่จะ เรียกใช้ฟังก์ชัน C++ จากเทรด UI ในโค้ด Java ผ่าน JNI คุณควร ใช้ Callback ระหว่าง 2 เทรดในภาษาโปรแกรม Java โดยให้เทรดใดเทรดหนึ่ง ทำการเรียก C++ ที่บล็อก แล้วแจ้งเทรด UI เมื่อการเรียกที่บล็อก เสร็จสมบูรณ์
  • ลดจำนวนเธรดที่ JNI ต้องเข้าถึงหรือเข้าถึง หากจำเป็นต้องใช้ Thread Pool ทั้งในภาษา Java และ C++ ให้พยายามรักษาการสื่อสาร JNI ระหว่างเจ้าของ Pool แทนที่จะเป็นระหว่าง Worker Thread แต่ละรายการ
  • เก็บโค้ดอินเทอร์เฟซไว้ในแหล่งที่มาของ C++ และ Java ที่ระบุได้ง่ายและมีจำนวนน้อย เพื่ออำนวยความสะดวกในการปรับโครงสร้างโค้ดในอนาคต พิจารณาใช้ไลบรารีการสร้าง JNI อัตโนมัติ ตามความเหมาะสม

JavaVM และ JNIEnv

JNI กำหนดโครงสร้างข้อมูลหลัก 2 อย่าง ได้แก่ "JavaVM" และ "JNIEnv" ทั้ง 2 อย่างนี้เป็นตัวชี้ไปยังตัวชี้ไปยังตารางฟังก์ชัน (ในเวอร์ชัน C++ จะเป็นคลาสที่มี พอยน์เตอร์ไปยังตารางฟังก์ชันและฟังก์ชันสมาชิกสำหรับแต่ละฟังก์ชัน JNI ที่อ้อมผ่าน ตาราง) JavaVM มีฟังก์ชัน "invocation interface" ซึ่งช่วยให้คุณสร้างและทำลาย JavaVM ได้ ในทางทฤษฎี คุณสามารถมี JavaVM หลายรายการต่อกระบวนการได้ แต่ Android อนุญาตเพียงรายการเดียว

JNIEnv มีฟังก์ชัน JNI ส่วนใหญ่ ฟังก์ชันเนทีฟทั้งหมดจะรับ JNIEnv เป็นอาร์กิวเมนต์แรก ยกเว้นเมธอด @CriticalNative ดูการเรียกเนทีฟที่เร็วขึ้น

JNIEnv ใช้สำหรับที่เก็บข้อมูลเฉพาะเธรด ด้วยเหตุนี้ คุณจึงแชร์ JNIEnv ระหว่างเธรดไม่ได้ หากโค้ดไม่มีวิธีอื่นในการรับ JNIEnv คุณควรแชร์ JavaVM และใช้ GetEnv เพื่อค้นหา JNIEnv ของเธรด (หากมี ดูAttachCurrentThreadด้านล่าง)

การประกาศ JNIEnv และ JavaVM ใน C จะแตกต่างจากการประกาศใน C++ "jni.h" include file มี typedefs ที่แตกต่างกัน ขึ้นอยู่กับว่ารวมอยู่ใน C หรือ C++ ด้วยเหตุนี้จึงไม่ควร รวมอาร์กิวเมนต์ JNIEnv ในไฟล์ส่วนหัวที่รวมอยู่ในทั้ง 2 ภาษา (กล่าวอีกนัยหนึ่งคือ หากไฟล์ส่วนหัวต้องใช้ #ifdef __cplusplus คุณอาจต้องดำเนินการเพิ่มเติมหากมีสิ่งใดในส่วนหัวนั้นอ้างอิงถึง JNIEnv)

ชุดข้อความ

ชุดข้อความทั้งหมดเป็นชุดข้อความ Linux ซึ่งเคอร์เนลเป็นผู้กำหนดเวลา โดยปกติแล้วจะเริ่มจากโค้ดที่มีการจัดการ (ใช้ Thread.start()) แต่ก็สามารถสร้างที่อื่นแล้วแนบไปกับ JavaVM ได้เช่นกัน ตัวอย่างเช่น คุณสามารถแนบเธรดที่เริ่มต้นด้วย pthread_create() หรือ std::thread ได้โดยใช้ฟังก์ชัน AttachCurrentThread() หรือ AttachCurrentThreadAsDaemon() เธรดจะไม่มี JNIEnv และโทรหา JNI ไม่ได้จนกว่าจะแนบเธรด

โดยปกติแล้ว การใช้ Thread.start() เพื่อสร้างเธรดที่ต้องเรียกใช้โค้ด Java เป็นวิธีที่ดีที่สุด การทำเช่นนี้จะช่วยให้คุณมีพื้นที่สแต็กเพียงพอ อยู่ใน ThreadGroup ที่ถูกต้อง และใช้ ClassLoader เดียวกันกับโค้ด Java นอกจากนี้ การตั้งชื่อเธรดเพื่อการแก้ไขข้อบกพร่องใน Java ยังทำได้ง่ายกว่าจากโค้ดเนทีฟ (ดู pthread_setname_np() หากคุณมี pthread_t หรือ thread_t และดู std::thread::native_handle() หากคุณมี std::thread และต้องการ pthread_t)

การแนบเธรดที่สร้างขึ้นโดยเนทีฟจะทำให้มีการสร้างออบเจ็กต์ java.lang.Thread และเพิ่มลงใน ThreadGroup "main" ซึ่งจะทำให้ดีบักเกอร์มองเห็น การเรียกใช้ AttachCurrentThread() ในเธรดที่แนบอยู่แล้วจะไม่มีผล

Android จะไม่ระงับเธรดที่เรียกใช้โค้ดที่มาพร้อมเครื่อง หาก การเก็บขยะกำลังดำเนินการอยู่ หรือดีบักเกอร์ได้ออกคำขอระงับ Android จะหยุดชั่วคราวในครั้งถัดไปที่ทำการเรียก JNI

เธรดที่แนบผ่าน JNI ต้องเรียกใช้ DetachCurrentThread() ก่อนที่จะออก หากการเขียนโค้ดโดยตรงเป็นเรื่องยาก ใน Android 2.0 (Eclair) ขึ้นไป คุณ สามารถใช้ pthread_key_create() เพื่อกำหนดฟังก์ชัน ทำลายล้างที่จะเรียกใช้ก่อนที่เธรดจะออก และ เรียกใช้ DetachCurrentThread() จากที่นั่น (ใช้คีย์นั้นกับ pthread_setspecific() เพื่อจัดเก็บ JNIEnv ใน ที่เก็บข้อมูลเฉพาะเธรด วิธีนี้จะส่งไปยังตัวทำลายของคุณเป็น อาร์กิวเมนต์)

jclass, jmethodID และ jfieldID

หากต้องการเข้าถึงฟิลด์ของออบเจ็กต์จากโค้ดดั้งเดิม ให้ทำดังนี้

  • รับการอ้างอิงออบเจ็กต์คลาสสำหรับคลาสที่มี FindClass
  • รับรหัสฟิลด์สำหรับฟิลด์ที่มี GetFieldID
  • รับเนื้อหาของฟิลด์ด้วยสิ่งที่เหมาะสม เช่น GetIntField

ในทำนองเดียวกัน หากต้องการเรียกใช้เมธอด คุณต้องรับการอ้างอิงออบเจ็กต์คลาสก่อน แล้วจึงรับรหัสเมธอด โดยปกติแล้ว รหัสจะเป็นเพียง ตัวชี้ไปยังโครงสร้างข้อมูลรันไทม์ภายใน การค้นหาอาจต้องมีการเปรียบเทียบสตริงหลายรายการ แต่เมื่อพบแล้ว การเรียกจริงเพื่อรับฟิลด์หรือเรียกใช้เมธอด จะรวดเร็วมาก

หากประสิทธิภาพเป็นสิ่งสำคัญ คุณควรค้นหาค่าเพียงครั้งเดียวและแคชผลลัพธ์ ในโค้ดดั้งเดิม เนื่องจากแต่ละกระบวนการมี JavaVM ได้เพียง 1 รายการ จึงควรเก็บข้อมูลนี้ไว้ในโครงสร้างแบบคงที่ในเครื่อง

การอ้างอิงคลาส รหัสฟิลด์ และรหัสเมธอดจะรับประกันว่าถูกต้องจนกว่าจะมีการยกเลิกการโหลดคลาส ระบบจะยกเลิกการโหลดคลาสก็ต่อเมื่อคลาสทั้งหมดที่เชื่อมโยงกับ ClassLoader สามารถรวบรวมขยะได้เท่านั้น ซึ่งเกิดขึ้นได้ยากแต่ก็ไม่ใช่ว่าจะเกิดขึ้นไม่ได้ใน Android อย่างไรก็ตาม โปรดทราบว่า jclass เป็นข้อมูลอ้างอิงของคลาสและต้องได้รับการปกป้องด้วยการเรียกใช้ NewGlobalRef (ดูส่วนถัดไป)

หากต้องการแคชรหัสเมื่อโหลดชั้นเรียน และแคชรหัสอีกครั้งโดยอัตโนมัติ หากมีการยกเลิกการโหลดชั้นเรียนและโหลดซ้ำ วิธีที่ถูกต้องในการเริ่มต้น รหัสคือการเพิ่มโค้ดที่มีลักษณะดังนี้ลงในชั้นเรียนที่เหมาะสม

Kotlin

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

    init {
        nativeInit()
    }
}

Java

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

    static {
        nativeInit();
    }

สร้างnativeClassInitเมธอดในโค้ด C/C++ ที่ทำการค้นหารหัส โค้ด จะทำงานครั้งเดียวเมื่อมีการเริ่มต้นคลาส หากคลาสถูกยกเลิกการโหลดและ โหลดซ้ำ ระบบจะเรียกใช้ฟังก์ชันอีกครั้ง

การอ้างอิงในเครื่องและทั่วโลก

อาร์กิวเมนต์ทุกรายการที่ส่งไปยังเมธอดเนทีฟและออบเจ็กต์เกือบทุกรายการที่ฟังก์ชัน JNI แสดงผลคือ "การอ้างอิงภายใน" ซึ่งหมายความว่าตัวแปรนี้จะใช้ได้ตลอด ระยะเวลาของเมธอดดั้งเดิมปัจจุบันในเธรดปัจจุบัน แม้ว่าออบเจ็กต์จะยังคงอยู่ต่อไปหลังจากที่เมธอดดั้งเดิม ส่งคืนค่าแล้ว แต่การอ้างอิงก็ไม่ถูกต้อง

ซึ่งมีผลกับคลาสย่อยทั้งหมดของ jobject รวมถึง jclass, jstring และ jarray (รันไทม์จะเตือนคุณเกี่ยวกับการใช้การอ้างอิงส่วนใหญ่ที่ไม่ถูกต้องเมื่อเปิดใช้การตรวจสอบ JNI แบบขยาย)

วิธีเดียวที่จะรับการอ้างอิงที่ไม่ใช่ในเครื่องคือผ่านฟังก์ชัน NewGlobalRef และ NewWeakGlobalRef

หากต้องการเก็บข้อมูลอ้างอิงไว้นานขึ้น คุณต้องใช้ข้อมูลอ้างอิง "ส่วนกลาง" ฟังก์ชัน NewGlobalRef ใช้การอ้างอิงในเครื่องเป็นอาร์กิวเมนต์และแสดงผลการอ้างอิงส่วนกลาง เรารับประกันว่าการอ้างอิงทั่วโลกจะใช้ได้จนกว่าคุณจะโทรหา DeleteGlobalRef

โดยทั่วไปมักใช้รูปแบบนี้เมื่อแคช jclass ที่ส่งคืนจาก FindClass เช่น

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

เมธอด JNI ทั้งหมดรับทั้งการอ้างอิงในเครื่องและการอ้างอิงส่วนกลางเป็นอาร์กิวเมนต์ การอ้างอิงไปยังออบเจ็กต์เดียวกันอาจมีค่าแตกต่างกันได้ เช่น ค่าที่ส่งคืนจากการเรียกใช้ NewGlobalRef ในออบเจ็กต์เดียวกันติดต่อกันอาจแตกต่างกัน หากต้องการดูว่าการอ้างอิง 2 รายการอ้างอิงถึงออบเจ็กต์เดียวกันหรือไม่ คุณต้องใช้ฟังก์ชัน IsSameObject อย่าเปรียบเทียบ การอ้างอิงกับ == ในโค้ดเนทีฟ

ผลที่ตามมาอย่างหนึ่งคือคุณต้องไม่ถือว่าการอ้างอิงออบเจ็กต์นั้นคงที่หรือมีลักษณะเฉพาะ ในโค้ดเนทีฟ ค่าที่แสดงออบเจ็กต์อาจแตกต่างกัน จากการเรียกใช้เมธอดครั้งหนึ่งไปยังครั้งถัดไป และเป็นไปได้ที่ออบเจ็กต์ 2 รายการ ที่แตกต่างกันอาจมีค่าเดียวกันในการเรียกใช้ที่ต่อเนื่องกัน อย่าใช้ค่า jobject เป็นคีย์

โปรแกรมเมอร์ต้อง "ไม่จัดสรรมากเกินไป" สำหรับการอ้างอิงในเครื่อง ในทางปฏิบัติ หมายความว่าหากคุณสร้างการอ้างอิงในเครื่องจำนวนมาก เช่น ขณะเรียกใช้ผ่านอาร์เรย์ของออบเจ็กต์ คุณควรปล่อยการอ้างอิงเหล่านั้นด้วยตนเองโดยใช้ DeleteLocalRef แทนที่จะปล่อยให้ JNI ทำแทน การติดตั้งใช้งานจำเป็นต้องจองช่องสำหรับ การอ้างอิงในเครื่อง 16 รายการเท่านั้น ดังนั้นหากต้องการมากกว่านั้น คุณควรลบรายการที่ไม่ต้องการออกไปเรื่อยๆ หรือใช้ EnsureLocalCapacity/PushLocalFrame เพื่อจองเพิ่มเติม

โปรดทราบว่า jfieldID และ jmethodID เป็นประเภทที่ทึบแสง ไม่ใช่การอ้างอิงออบเจ็กต์ และไม่ควรส่งไปยัง NewGlobalRef พอยน์เตอร์ข้อมูลดิบ ที่ฟังก์ชันอย่าง GetStringUTFChars และ GetByteArrayElements แสดงผลก็ไม่ใช่ออบเจ็กต์เช่นกัน (อาจส่งผ่านระหว่างเธรดและจะใช้ได้จนกว่าจะมีการเรียกใช้ฟังก์ชัน Release ที่ตรงกัน)

มีกรณีที่ผิดปกติกรณีหนึ่งที่ควรกล่าวถึงแยกกัน หากแนบเธรดดั้งเดิมด้วย AttachCurrentThread โค้ดที่คุณเรียกใช้จะไม่ปล่อยการอ้างอิงในเครื่องโดยอัตโนมัติจนกว่าเธรดจะแยกออก คุณจะต้องลบการอ้างอิงในเครื่อง ที่สร้างขึ้นด้วยตนเอง โดยทั่วไปแล้ว โค้ดเนทีฟใดๆ ที่สร้างการอ้างอิงในเครื่องในลูปอาจต้องมีการลบด้วยตนเอง

โปรดระมัดระวังในการใช้การอ้างอิงส่วนกลาง การอ้างอิงส่วนกลางอาจหลีกเลี่ยงไม่ได้ แต่ก็แก้ไขข้อบกพร่องได้ยาก และอาจทำให้เกิดลักษณะการทำงานของหน่วยความจำที่วินิจฉัยได้ยาก เมื่อค่าอื่นๆ เท่ากันหมด โซลูชันที่มีการอ้างอิงส่วนกลางน้อยกว่าน่าจะดีกว่า

สตริง UTF-8 และ UTF-16

ภาษาโปรแกรม Java ใช้ UTF-16 JNI มีเมธอดที่ใช้ได้กับ Modified UTF-8 เพื่อความสะดวก การเข้ารหัสที่แก้ไขแล้วมีประโยชน์สำหรับโค้ด C เนื่องจากจะเข้ารหัส \u0000 เป็น 0xc0 0x80 แทน 0x00 ข้อดีของวิธีนี้คือคุณสามารถใช้สตริงที่ลงท้ายด้วย 0 ในรูปแบบ C ได้ ซึ่งเหมาะสำหรับใช้กับฟังก์ชันสตริง libc มาตรฐาน ข้อเสียคือคุณไม่สามารถส่งข้อมูล UTF-8 ที่กำหนดเองไปยัง JNI และคาดหวังให้ทำงานได้อย่างถูกต้อง

หากต้องการรับการแสดง UTF-16 ของ String ให้ใช้ GetStringChars โปรดทราบว่าสตริง UTF-16 ไม่ได้ลงท้ายด้วย 0 และอนุญาตให้ใช้ \u0000 ได้ ดังนั้นคุณจึงต้องเก็บความยาวของสตริงไว้พร้อมกับตัวชี้ jchar

อย่าลืมReleaseสตริงที่คุณGet ฟังก์ชันสตริงจะแสดงผล 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 Android 8 เปลี่ยนการแสดง String ให้ใช้ 8 บิตต่ออักขระ สำหรับสตริง 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 มีฟังก์ชันสำหรับการเข้าถึงเนื้อหาของออบเจ็กต์อาร์เรย์ แม้ว่าอาร์เรย์ของออบเจ็กต์จะต้องเข้าถึงทีละรายการ แต่อาร์เรย์ของ Primitive สามารถอ่านและเขียนได้โดยตรงราวกับว่ามีการประกาศใน C

เพื่อให้อินเทอร์เฟซมีประสิทธิภาพมากที่สุดโดยไม่จำกัด การใช้งาน VM Get<PrimitiveType>ArrayElements ตระกูลการเรียกใช้ช่วยให้รันไทม์สามารถส่งคืนพอยน์เตอร์ไปยังองค์ประกอบจริง หรือ จัดสรรหน่วยความจำและทำสำเนาได้ ไม่ว่าจะด้วยวิธีใดก็ตาม ระบบจะรับประกันว่าพอยน์เตอร์ดิบที่แสดงผล จะถูกต้องจนกว่าจะมีการเรียกใช้ Release ที่เกี่ยวข้อง (ซึ่งหมายความว่าหากไม่ได้คัดลอกข้อมูล ระบบจะปักหมุดออบเจ็กต์อาร์เรย์ และย้ายตำแหน่งไม่ได้ซึ่งเป็นส่วนหนึ่งของการกระชับฮีป) คุณต้อง Release อาร์เรย์ทุกรายการที่คุณ Get นอกจากนี้ หากGet การเรียกใช้ล้มเหลว คุณต้องตรวจสอบว่าโค้ดไม่ได้พยายามReleaseพอยน์เตอร์ NULL ในภายหลัง

คุณตรวจสอบได้ว่ามีการคัดลอกข้อมูลหรือไม่โดยส่งพอยน์เตอร์ที่ไม่ใช่ NULL สำหรับอาร์กิวเมนต์ isCopy ซึ่งมักไม่ค่อยมีประโยชน์

Release การเรียกใช้จะใช้อาร์กิวเมนต์ mode ที่มีค่าใดค่าหนึ่งใน 3 ค่าต่อไปนี้ การดำเนินการที่รันไทม์ดำเนินการจะขึ้นอยู่กับว่ารันไทม์แสดงผลพอยน์เตอร์ไปยังข้อมูลจริงหรือสำเนาของข้อมูลนั้นหรือไม่

  • 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 จะปักหมุดหรือคัดลอกเนื้อหาอาร์เรย์ ทั้งนี้ขึ้นอยู่กับการ ติดตั้งใช้งาน โค้ดจะคัดลอกข้อมูล (อาจเป็นครั้งที่ 2) จากนั้นเรียกใช้ Release ในกรณีนี้ JNI_ABORT จะช่วยให้มั่นใจได้ว่าจะไม่มีการคัดลอกครั้งที่ 3

คุณสามารถทำสิ่งเดียวกันนี้ได้ง่ายขึ้นโดยทำดังนี้

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

ซึ่งมีข้อดีหลายประการ ดังนี้

  • ต้องใช้การเรียก JNI เพียงครั้งเดียวแทนที่จะเป็น 2 ครั้ง ซึ่งช่วยลดค่าใช้จ่าย
  • ไม่จำเป็นต้องปักหมุดหรือทำสำเนาข้อมูลเพิ่มเติม
  • ลดความเสี่ยงจากข้อผิดพลาดของโปรแกรมเมอร์ - ไม่มีความเสี่ยงที่จะลืม เรียกใช้ Release หลังจากที่บางอย่างล้มเหลว

ในทำนองเดียวกัน คุณสามารถใช้ Set<Type>ArrayRegion call เพื่อคัดลอกข้อมูลลงในอาร์เรย์ และ GetStringRegion หรือ GetStringUTFRegion เพื่อคัดลอกอักขระออกจาก String

ข้อยกเว้น

คุณต้องไม่เรียกใช้ฟังก์ชัน JNI ส่วนใหญ่ขณะที่กำลังรอดำเนินการข้อยกเว้น โค้ดของคุณควรสังเกตข้อยกเว้น (ผ่านค่าที่ฟังก์ชันแสดงผล ExceptionCheck หรือ ExceptionOccurred) และแสดงผล หรือล้างข้อยกเว้นและจัดการ

ฟังก์ชัน JNI เดียวที่คุณได้รับอนุญาตให้เรียกใช้ขณะที่ข้อยกเว้น รอดำเนินการอยู่คือ

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

การเรียก JNI หลายรายการอาจทำให้เกิดข้อยกเว้น แต่ก็มักจะมีวิธีที่ง่ายกว่า ในการตรวจสอบว่าการดำเนินการล้มเหลวหรือไม่ เช่น หาก NewString แสดงผลค่าที่ไม่ใช่ NULL คุณก็ไม่จำเป็นต้องตรวจสอบข้อยกเว้น อย่างไรก็ตาม หากเรียกใช้เมธอด (โดยใช้ฟังก์ชันเช่น CallObjectMethod) คุณต้องตรวจสอบข้อยกเว้นเสมอ เนื่องจากค่าที่ส่งคืนจะใช้ไม่ได้หากมีการส่งข้อยกเว้น

โปรดทราบว่าข้อยกเว้นที่โค้ดที่มีการจัดการส่งคืนจะไม่คลายเฟรมสแต็กเนทีฟ (และไม่ควรส่งข้อยกเว้น C++ ใน Android โดยทั่วไป และต้องไม่ส่งข้อยกเว้น ข้ามขอบเขตการเปลี่ยน JNI จากโค้ด C++ ไปยังโค้ดที่มีการจัดการ) คำสั่ง JNI Throw และ ThrowNew จะตั้งค่าพอยน์เตอร์ข้อยกเว้นในเธรดปัจจุบันเท่านั้น เมื่อกลับไปใช้โค้ดที่มีการจัดการจากโค้ดดั้งเดิม ระบบจะบันทึกข้อยกเว้นและจัดการอย่างเหมาะสม

โค้ดเนทีฟสามารถ "ดัก" ข้อยกเว้นได้โดยการเรียกใช้ ExceptionCheck หรือ ExceptionOccurred และล้างข้อยกเว้นด้วย ExceptionClear เช่นเคย การทิ้งข้อยกเว้นโดยไม่จัดการอาจทำให้เกิดปัญหาได้

ไม่มีฟังก์ชันในตัวสำหรับการจัดการออบเจ็กต์ Throwable เอง ดังนั้นหากต้องการ (เช่น) รับสตริงข้อยกเว้น คุณจะต้องค้นหาคลาส Throwable ค้นหารหัสเมธอดสำหรับ getMessage "()Ljava/lang/String;" เรียกใช้ และหากผลลัพธ์ไม่ใช่ NULL ให้ใช้ GetStringUTFChars เพื่อรับสิ่งที่คุณส่งไปยัง printf(3) หรือเทียบเท่าได้

การตรวจสอบเพิ่มเติม

JNI ไม่ค่อยตรวจสอบข้อผิดพลาด โดยปกติแล้วข้อผิดพลาดจะทำให้เกิดข้อขัดข้อง นอกจากนี้ Android ยังมีโหมดที่เรียกว่า CheckJNI ซึ่งจะสลับตัวชี้ตารางฟังก์ชัน JavaVM และ JNIEnv ไปยังตารางฟังก์ชันที่ทำการตรวจสอบเพิ่มเติมหลายรายการก่อนเรียกใช้การติดตั้งใช้งานมาตรฐาน

การตรวจสอบเพิ่มเติมมีดังนี้

  • อาร์เรย์: พยายามจัดสรรอาร์เรย์ที่มีขนาดเป็นลบ
  • พอยน์เตอร์ที่ไม่ถูกต้อง: การส่ง jarray/jclass/jobject/jstring ที่ไม่ถูกต้องไปยังการเรียก JNI หรือการส่งพอยน์เตอร์ NULL ไปยังการเรียก JNI ที่มีอาร์กิวเมนต์ที่ไม่ใช่ NULL
  • ชื่อคลาส: ส่งสิ่งอื่นที่ไม่ใช่รูปแบบชื่อคลาส "java/lang/String" ไปยังการเรียก JNI
  • การเรียกที่สำคัญ: การเรียก JNI ระหว่างการรับที่ "สำคัญ" กับการเผยแพร่ที่เกี่ยวข้อง
  • Direct ByteBuffers: ส่งอาร์กิวเมนต์ที่ไม่ถูกต้องไปยัง NewDirectByteBuffer
  • ข้อยกเว้น: การโทร JNI ขณะที่รอดำเนินการข้อยกเว้น
  • JNIEnv*s: การใช้ JNIEnv* จากเธรดที่ไม่ถูกต้อง
  • jfieldIDs: การใช้ jfieldID ที่เป็น NULL หรือการใช้ jfieldID เพื่อตั้งค่าช่องเป็นค่าที่มีประเภทไม่ถูกต้อง (เช่น พยายามกำหนด StringBuilder ให้กับช่อง String) หรือการใช้ jfieldID สำหรับช่องแบบคงที่เพื่อตั้งค่าช่องอินสแตนซ์หรือในทางกลับกัน หรือการใช้ jfieldID จากคลาสหนึ่งกับอินสแตนซ์ของอีกคลาสหนึ่ง
  • jmethodIDs: using the wrong kind of jmethodID when making a Call*Method JNI call: incorrect return type, static/non-static mismatch, wrong type for ‘this’ (for non-static calls) or wrong class (for static calls).
  • การอ้างอิง: การใช้ 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 ในไฟล์ Manifest ของแอปพลิเคชันเพื่อ เปิด CheckJNI สำหรับแอปของคุณเท่านั้นได้ด้วย โปรดทราบว่าเครื่องมือบิลด์ของ Android จะดำเนินการนี้โดยอัตโนมัติสำหรับ บิลด์บางประเภท

ไลบรารีที่มาพร้อมเครื่อง

คุณโหลดโค้ดเนทีฟจากไลบรารีที่ใช้ร่วมกันได้ด้วย System.loadLibrary มาตรฐาน

ในทางปฏิบัติ Android เวอร์ชันเก่ามีข้อบกพร่องใน PackageManager ซึ่งทำให้การติดตั้งและการอัปเดตไลบรารีเนทีฟไม่น่าเชื่อถือ โปรเจ็กต์ ReLinker มีวิธีแก้ปัญหานี้และปัญหาอื่นๆ เกี่ยวกับการโหลดไลบรารีแบบเนทีฟ

เรียกใช้ System.loadLibrary (หรือ ReLinker.loadLibrary) จากตัวเริ่มต้นคลาสแบบคงที่ อาร์กิวเมนต์คือชื่อไลบรารีที่ "ไม่มีการตกแต่ง" ดังนั้นหากต้องการโหลด libfubar.so คุณจะต้องส่ง "fubar"

หากคุณมีคลาสที่มีเมธอดเนทีฟเพียงคลาสเดียว การเรียกใช้ System.loadLibrary ควรอยู่ในตัวเริ่มต้นแบบคงที่ของคลาสนั้น มิเช่นนั้น คุณอาจต้องเรียกใช้จาก Application เพื่อให้มั่นใจว่าระบบจะโหลดไลบรารีเสมอ และโหลดตั้งแต่เนิ่นๆ

รันไทม์จะค้นหาเมธอดดั้งเดิมได้ 2 วิธี คุณจะลงทะเบียนอย่างชัดเจนกับ RegisterNatives หรือจะให้รันไทม์ค้นหาแบบไดนามิกด้วย dlsym ก็ได้ ข้อดีของ RegisterNatives คือคุณจะได้รับการตรวจสอบล่วงหน้าว่ามีสัญลักษณ์อยู่จริง นอกจากนี้ คุณยังสามารถมีไลบรารีที่ใช้ร่วมกันซึ่งมีขนาดเล็กลงและเร็วขึ้นได้โดยไม่ต้องส่งออกสิ่งอื่นใดนอกจาก JNI_OnLoad ข้อดีของการปล่อยให้รันไทม์ค้นหาฟังก์ชัน คือคุณจะเขียนโค้ดน้อยลงเล็กน้อย

วิธีใช้ "RegisterNatives"

  • ระบุJNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)ฟังก์ชัน
  • ใน JNI_OnLoad ให้ลงทะเบียนวิธีการเนทีฟทั้งหมดโดยใช้ RegisterNatives
  • สร้างโดยใช้สคริปต์เวอร์ชัน (แนะนำ) หรือใช้ -fvisibility=hidden เพื่อให้ระบบส่งออกเฉพาะ JNI_OnLoad จากคลัง ซึ่งจะทำให้โค้ดทำงานได้เร็วขึ้นและมีขนาดเล็กลง รวมถึงหลีกเลี่ยงการชนกันที่อาจเกิดขึ้นกับไลบรารีอื่นๆ ที่โหลดลงในแอป (แต่จะสร้าง Stack Trace ที่มีประโยชน์น้อยกว่าหากแอปขัดข้องในโค้ดแบบเนทีฟ)

ตัวเริ่มต้นแบบคงที่ควรมีลักษณะดังนี้

Kotlin

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

Java

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

ฟังก์ชัน JNI_OnLoad ควรมีลักษณะดังนี้หากเขียนใน C++

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

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

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

    return JNI_VERSION_1_6;
}

หากต้องการใช้ "การค้นพบ" เมธอดเนทีฟแทน คุณต้องตั้งชื่อเมธอดในลักษณะที่เฉพาะเจาะจง (ดูรายละเอียดได้ที่ข้อกำหนด JNI) ซึ่งหมายความว่าหากลายเซ็นของเมธอดไม่ถูกต้อง คุณจะไม่ทราบจนกว่าจะมีการเรียกใช้เมธอดเป็นครั้งแรก

FindClassการเรียกJNI_OnLoadใดๆ ที่ทำจาก JNI_OnLoad จะแก้ไขคลาสใน บริบทของตัวโหลดคลาสที่ใช้โหลดไลบรารีที่ใช้ร่วมกัน เมื่อเรียกใช้จากบริบทอื่นๆ FindClass จะใช้โปรแกรมโหลดคลาสที่เชื่อมโยงกับเมธอดที่ด้านบนของสแต็ก Java หรือหากไม่มี (เนื่องจากมีการเรียกใช้จากเธรดดั้งเดิมที่เพิ่งแนบ) ก็จะใช้โปรแกรมโหลดคลาส "ระบบ" Class Loader ของระบบไม่รู้จักคลาสของแอปพลิเคชัน คุณจึงค้นหาคลาสของคุณเองด้วย FindClass ในบริบทนั้นไม่ได้ ซึ่งทำให้ JNI_OnLoad เป็นที่ที่สะดวกในการค้นหาและแคชคลาส เมื่อคุณมีjclass การอ้างอิงส่วนกลางที่ถูกต้อง คุณจะใช้การอ้างอิงนั้นจากเธรดที่แนบมาได้

การเรียกใช้เนทีฟที่เร็วขึ้นด้วย @FastNative และ @CriticalNative

คุณใส่คำอธิบายประกอบในเมธอดเนทีฟได้ด้วย @FastNative หรือ @CriticalNative (แต่จะใช้ทั้ง 2 อย่างไม่ได้) เพื่อเร่งการเปลี่ยนผ่านระหว่างโค้ดที่จัดการและโค้ดเนทีฟ อย่างไรก็ตาม คำอธิบายประกอบเหล่านี้ มาพร้อมกับการเปลี่ยนแปลงลักษณะการทำงานบางอย่างที่ต้องพิจารณาอย่างรอบคอบก่อนใช้งาน แม้ว่าเราจะกล่าวถึงการเปลี่ยนแปลงเหล่านี้โดยย่อด้านล่าง แต่โปรดดูรายละเอียดในเอกสารประกอบ

@CriticalNative ใช้ได้เฉพาะกับเมธอดดั้งเดิมที่ไม่ได้ ใช้ออบเจ็กต์ที่มีการจัดการ (ในพารามิเตอร์หรือค่าที่ส่งคืน หรือเป็น this โดยนัย) และคำอธิบายประกอบนี้ จะเปลี่ยน ABI การเปลี่ยน JNI การติดตั้งใช้งานเนทีฟต้องยกเว้นพารามิเตอร์ JNIEnvและjclassออกจากลายเซ็นฟังก์ชัน

ขณะเรียกใช้เมธอด @FastNative หรือ @CriticalNative การเก็บขยะ จะไม่สามารถระงับเธรดสำหรับงานที่จำเป็นและอาจถูกบล็อกได้ อย่าใช้คำอธิบายประกอบเหล่านี้กับเมธอดที่ทำงานเป็นเวลานาน รวมถึงเมธอดที่มักจะทำงานอย่างรวดเร็วแต่โดยทั่วไปแล้วไม่มีขอบเขต โดยเฉพาะอย่างยิ่ง โค้ดไม่ควรดําเนินการ I/O ที่สําคัญหรือรับการล็อกดั้งเดิมที่ อาจถือครองเป็นเวลานาน

คำอธิบายประกอบเหล่านี้ได้รับการติดตั้งใช้งานเพื่อใช้ในระบบตั้งแต่ Android 8 และกลายเป็น API สาธารณะที่ผ่านการทดสอบ CTS ใน Android 14 การเพิ่มประสิทธิภาพเหล่านี้อาจใช้ได้กับอุปกรณ์ Android 8-13 ด้วย (แม้ว่าจะไม่มีการรับประกัน CTS ที่เข้มงวด) แต่การค้นหาแบบไดนามิกของเมธอดดั้งเดิมจะรองรับเฉพาะใน Android 12 ขึ้นไป และต้องมีการลงทะเบียนอย่างชัดเจนกับ JNI RegisterNatives อย่างเคร่งครัดสำหรับการเรียกใช้ใน Android เวอร์ชัน 8-11 ระบบจะละเว้นคำอธิบายประกอบเหล่านี้ใน Android 7 ลงไป เนื่องจากความไม่ตรงกันของ ABI สำหรับ @CriticalNative จะทำให้การจัดรูปแบบอาร์กิวเมนต์ไม่ถูกต้องและอาจทำให้เกิดข้อขัดข้อง

สำหรับเมธอดที่สำคัญต่อประสิทธิภาพซึ่งจำเป็นต้องมีคำอธิบายประกอบเหล่านี้ เราขอแนะนำให้ลงทะเบียนเมธอดอย่างชัดเจนกับ JNI RegisterNatives แทนที่จะอาศัย "การค้นหา" เมธอดเนทีฟตามชื่อ ขอแนะนำให้ใส่ผู้เรียกใช้เมธอด @FastNative หรือ @CriticalNative ไว้ในโปรไฟล์พื้นฐานเพื่อให้แอปเริ่มต้นทำงานได้อย่างมีประสิทธิภาพสูงสุด ตั้งแต่ Android 12 เป็นต้นมา การเรียกใช้@CriticalNativeเมธอดเนทีฟจากเมธอดที่มีการจัดการที่คอมไพล์แล้วนั้นแทบจะ ไม่ต่างจากการเรียกใช้แบบไม่อินไลน์ใน C/C++ ตราบใดที่อาร์กิวเมนต์ทั้งหมดพอดีกับรีจิสเตอร์ (เช่น อาร์กิวเมนต์จำนวนเต็มสูงสุด 8 รายการและอาร์กิวเมนต์แบบจุดลอยสูงสุด 8 รายการใน arm64)

บางครั้งการแยกวิธีการดั้งเดิมออกเป็น 2 วิธีอาจเป็นทางเลือกที่ดีกว่า โดยวิธีหนึ่งเป็นวิธีที่รวดเร็วมากซึ่งอาจล้มเหลว และอีกวิธีหนึ่งจะจัดการกับกรณีที่ช้า เช่น

Kotlin

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

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

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

Java

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

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

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

ข้อควรพิจารณาเกี่ยวกับ 64 บิต

หากต้องการรองรับสถาปัตยกรรมที่ใช้พอยน์เตอร์ 64 บิต ให้ใช้ฟิลด์ long แทน int เมื่อจัดเก็บพอยน์เตอร์ไปยังโครงสร้างเนทีฟในฟิลด์ Java

ฟีเจอร์ที่ไม่รองรับ/ความเข้ากันได้กับรุ่นก่อนหน้า

ระบบรองรับฟีเจอร์ทั้งหมดของ JNI 1.6 ยกเว้นฟีเจอร์ต่อไปนี้

  • DefineClass ไม่ได้ใช้ Android ไม่ได้ใช้ไบต์โค้ด Java หรือไฟล์คลาส ดังนั้นการส่งข้อมูลคลาสแบบไบนารีจึงใช้ไม่ได้

คุณอาจต้องทราบข้อมูลต่อไปนี้เพื่อความเข้ากันได้แบบย้อนหลังกับ Android เวอร์ชันเก่า

  • การค้นหาฟังก์ชันเนทีฟแบบไดนามิก

    จนถึง Android 2.0 (Eclair) ระบบไม่ได้แปลงอักขระ "$" เป็น "_00024" อย่างถูกต้อง ในระหว่างการค้นหาชื่อเมธอด การหลีกเลี่ยงข้อจำกัดนี้ต้องใช้การลงทะเบียนที่ชัดเจนหรือย้ายวิธีการเนทีฟออกจากคลาสภายใน

  • แยกชุดข้อความ

    จนถึง Android 2.0 (Eclair) คุณไม่สามารถใช้ฟังก์ชันpthread_key_create Destructor เพื่อหลีกเลี่ยงการตรวจสอบ "ต้องแยกเธรดก่อน ออก" (รันไทม์ยังใช้ฟังก์ชันทำลายคีย์ 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 รองรับการอ้างอิงในเครื่องแบบไม่จำกัด

  • Determining reference type with GetObjectRefType

    จนถึง Android 4.0 (Ice Cream Sandwich) การใช้ ตัวชี้โดยตรง (ดูด้านบน) ทำให้ไม่สามารถใช้งาน GetObjectRefTypeได้อย่างถูกต้อง แต่เราใช้ฮิวริสติก ที่ดูผ่านตารางส่วนกลางที่อ่อนแอ อาร์กิวเมนต์ ตารางตัวแปรเฉพาะ และตารางส่วนกลางตามลำดับ ครั้งแรกที่พบตัวชี้โดยตรง ระบบจะรายงานว่าการอ้างอิงของคุณเป็นประเภทที่ ระบบกำลังตรวจสอบ ซึ่งหมายความว่า เช่น หาก คุณเรียกใช้ GetObjectRefType ใน jclass ทั่วโลกที่ เป็น jclass เดียวกันกับที่ส่งเป็นอาร์กิวเมนต์โดยนัยไปยังเมธอดเนทีฟแบบคงที่ คุณจะได้รับ JNILocalRefType แทนที่จะเป็น JNIGlobalRefType

  • @FastNative และ @CriticalNative

    ใน Android 7 ลงไป ระบบจะไม่สนใจคำอธิบายประกอบการเพิ่มประสิทธิภาพเหล่านี้ ABI ที่ไม่ตรงกันสำหรับ @CriticalNative จะทําให้เกิดการจัดรูปแบบอาร์กิวเมนต์ที่ไม่ถูกต้อง และอาจทําให้เกิดข้อขัดข้อง

    การค้นหาแบบไดนามิกของฟังก์ชันเนทีฟสำหรับเมธอด @FastNative และ @CriticalNative ยังไม่ได้ใช้งานใน Android 8-10 และ มีข้อบกพร่องที่ทราบใน Android 11 การใช้การเพิ่มประสิทธิภาพเหล่านี้โดยไม่มีการลงทะเบียนกับ JNI อย่างชัดเจนRegisterNatives อาจทําให้เกิดข้อขัดข้องใน Android 8-11

  • FindClass การขว้าง ClassNotFoundException

    Android จะส่ง ClassNotFoundException แทน NoClassDefFoundError เพื่อให้เข้ากันได้กับเวอร์ชันก่อนหน้า เมื่อไม่พบคลาสโดย FindClass ลักษณะการทำงานนี้สอดคล้องกับ Java Reflection API Class.forName(name)

คำถามที่พบบ่อย: เหตุใดฉันจึงได้รับ UnsatisfiedLinkError

เมื่อทำงานกับโค้ดเนทีฟ คุณอาจเห็นข้อผิดพลาดเช่นนี้

java.lang.UnsatisfiedLinkError: Library foo not found

ในบางกรณี ข้อความนี้หมายความตามที่ระบุไว้ นั่นคือไม่พบไลบรารี ในกรณีอื่นๆ ไลบรารีมีอยู่แต่ dlopen(3) เปิดไม่ได้ และ รายละเอียดของความล้มเหลวจะอยู่ในข้อความรายละเอียดของข้อยกเว้น

สาเหตุที่พบบ่อยซึ่งอาจทำให้คุณพบข้อยกเว้น "ไม่พบไลบรารี" มีดังนี้

  • ไม่มีไลบรารีหรือแอปเข้าถึงไลบรารีไม่ได้ ใช้ adb shell ls -l <path> เพื่อตรวจสอบว่ามีไลบรารีอยู่หรือไม่ และตรวจสอบสิทธิ์
  • ไม่ได้สร้างไลบรารีด้วย NDK ซึ่งอาจส่งผลให้เกิด การอ้างอิงฟังก์ชันหรือไลบรารีที่ไม่มีในอุปกรณ์

UnsatisfiedLinkErrorความล้มเหลว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 เพื่อดู ข้อความเกี่ยวกับการโหลดไลบรารี
  • ไม่พบเมธอดเนื่องจากชื่อหรือลายเซ็นไม่ตรงกัน โดยทั่วไปปัญหานี้ เกิดจากสาเหตุต่อไปนี้
    • สำหรับการค้นหาเมธอดแบบ Lazy การไม่ประกาศฟังก์ชัน C++ ด้วย extern "C" และการมองเห็นที่เหมาะสม (JNIEXPORT) โปรดทราบว่าก่อน Ice Cream Sandwich มาโคร JNIEXPORT ไม่ถูกต้อง ดังนั้นการใช้ GCC ใหม่กับ jni.h เก่าจึงใช้ไม่ได้ คุณใช้ arm-eabi-nm เพื่อดูสัญลักษณ์ตามที่ปรากฏในไลบรารีได้ หากสัญลักษณ์ดู ผิดเพี้ยน (เช่น _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass แทนที่จะเป็น Java_Foo_myfunc) หรือหากประเภทสัญลักษณ์เป็น ตัวพิมพ์เล็ก "t" แทนที่จะเป็นตัวพิมพ์ใหญ่ "T" คุณจะต้อง ปรับการประกาศ
    • สำหรับการลงทะเบียนที่ชัดเจน ข้อผิดพลาดเล็กน้อยเมื่อป้อน ลายเซ็นของเมธอด ตรวจสอบว่าสิ่งที่คุณส่งไปยัง การเรียกการลงทะเบียนตรงกับลายเซ็นในไฟล์บันทึก โปรดทราบว่า "B" คือ byte และ "Z" คือ boolean คอมโพเนนต์ชื่อคลาสในลายเซ็นจะขึ้นต้นด้วย "L" ลงท้ายด้วย ";" ใช้ "/" เพื่อแยกชื่อแพ็กเกจ/คลาส และใช้ "$" เพื่อแยก ชื่อคลาสภายใน (Ljava/util/Map$Entry; เช่น)

การใช้ javah เพื่อสร้างส่วนหัว JNI โดยอัตโนมัติอาจช่วย หลีกเลี่ยงปัญหาบางอย่างได้

คำถามที่พบบ่อย: เหตุใด FindClass จึงไม่พบชั้นเรียนของฉัน

(คำแนะนำส่วนใหญ่นี้ใช้ได้ดีพอๆ กันกับการค้นหาวิธีการที่มี GetMethodID หรือ GetStaticMethodID หรือฟิลด์ที่มี GetFieldID หรือ GetStaticFieldID ไม่สำเร็จ)

ตรวจสอบว่าสตริงชื่อคลาสมีรูปแบบที่ถูกต้อง ชื่อคลาส JNI จะขึ้นต้นด้วยชื่อแพ็กเกจและคั่นด้วยเครื่องหมายทับ เช่น java/lang/String หากคุณกำลังค้นหาคลาสอาร์เรย์ คุณต้องเริ่มต้นด้วยวงเล็บเหลี่ยมจำนวนที่เหมาะสมและ ต้องครอบคลาสด้วย "L" และ ";" ด้วย ดังนั้นอาร์เรย์หนึ่งมิติของ String จะเป็น [Ljava/lang/String; หากคุณค้นหาคลาสภายใน ให้ใช้ "$" แทน "." โดยทั่วไปแล้ว การใช้ javap ในไฟล์ .class เป็นวิธีที่ดีในการค้นหา ชื่อภายในของคลาส

หากเปิดใช้การย่อโค้ด โปรดตรวจสอบว่าคุณได้ กำหนดค่าโค้ดที่จะเก็บไว้ การกำหนดค่ากฎ Keep ที่เหมาะสมเป็นสิ่งสำคัญเนื่องจากเครื่องมือลดขนาดโค้ดอาจนำคลาส เมธอด หรือฟิลด์ที่ใช้จาก JNI เท่านั้นออก

หากชื่อชั้นเรียนถูกต้อง คุณอาจพบปัญหาเกี่ยวกับตัวโหลดคลาส FindClass ต้องการเริ่มการค้นหาคลาสใน เครื่องมือโหลดคลาสที่เชื่อมโยงกับโค้ดของคุณ โดยจะตรวจสอบสแต็กการเรียก ซึ่งจะมีลักษณะดังนี้

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

วิธีที่อยู่ด้านบนสุดคือ Foo.myfunc FindClass จะค้นหาออบเจ็กต์ ClassLoader ที่เชื่อมโยงกับคลาส Foo และใช้ออบเจ็กต์นั้น

ซึ่งโดยปกติแล้วจะทำในสิ่งที่คุณต้องการ คุณอาจพบปัญหาหาก สร้างเธรดด้วยตนเอง (อาจโดยการเรียกใช้ pthread_create แล้วแนบด้วย AttachCurrentThread) ตอนนี้ไม่มีเฟรมสแต็กจากแอปพลิเคชันของคุณแล้ว หากคุณเรียกใช้ FindClass จากเธรดนี้ JavaVM จะเริ่มต้นใน Class Loader ของ "system" แทนที่จะเป็น Class Loader ที่เชื่อมโยงกับแอปพลิเคชันของคุณ ดังนั้นการพยายามค้นหาคลาสเฉพาะของแอปจะล้มเหลว

คุณแก้ไขปัญหานี้ได้ด้วยวิธีต่อไปนี้

  • ทำการค้นหา FindClass เพียงครั้งเดียวใน JNI_OnLoad และแคชการอ้างอิงคลาสเพื่อใช้ในภายหลัง FindClass การเรียกที่ทำเป็นส่วนหนึ่งของการดำเนินการ JNI_OnLoad จะใช้ตัวโหลดคลาสที่เชื่อมโยงกับ ฟังก์ชันที่เรียก System.loadLibrary (นี่เป็น กฎพิเศษที่จัดไว้เพื่อให้การเริ่มต้นไลบรารีสะดวกยิ่งขึ้น) หากโค้ดแอปโหลดไลบรารี FindClass จะใช้ตัวโหลดคลาสที่ถูกต้อง
  • ส่งอินสแตนซ์ของคลาสไปยังฟังก์ชันที่ต้องการ โดยประกาศเมธอดเนทีฟให้รับอาร์กิวเมนต์ Class แล้ว ส่ง Foo.class เข้าไป
  • แคชการอ้างอิงไปยังออบเจ็กต์ ClassLoader ไว้ที่ใดที่หนึ่ง ที่สะดวก และออกคำสั่งเรียก loadClass โดยตรง ซึ่งต้องใช้ ความพยายาม

คำถามที่พบบ่อย: ฉันจะแชร์ข้อมูลดิบกับโค้ดดั้งเดิมได้อย่างไร

คุณอาจพบว่าตัวเองอยู่ในสถานการณ์ที่ต้องเข้าถึงบัฟเฟอร์ข้อมูลดิบขนาดใหญ่จากทั้งโค้ดที่มีการจัดการและโค้ดดั้งเดิม ตัวอย่างที่พบบ่อย ได้แก่ การดัดแปลงบิตแมปหรือตัวอย่างเสียง มีแนวทางพื้นฐาน 2 แนวทาง

คุณสามารถจัดเก็บข้อมูลใน byte[] ได้ ซึ่งช่วยให้เข้าถึงจากโค้ดที่มีการจัดการได้อย่างรวดเร็ว อย่างไรก็ตาม ในฝั่งเนทีฟ คุณ ไม่รับประกันว่าจะเข้าถึงข้อมูลได้โดยไม่ต้องคัดลอก ในการใช้งานบางอย่าง GetByteArrayElements และ GetPrimitiveArrayCritical จะแสดงพอยน์เตอร์จริงไปยัง ข้อมูลดิบในฮีปที่มีการจัดการ แต่ในการใช้งานอื่นๆ จะจัดสรรบัฟเฟอร์ ในฮีปดั้งเดิมและคัดลอกข้อมูล

อีกทางเลือกหนึ่งคือการจัดเก็บข้อมูลในบัฟเฟอร์ไบต์โดยตรง โดยสร้างได้ด้วย java.nio.ByteBuffer.allocateDirect หรือฟังก์ชัน NewDirectByteBuffer ของ JNI ต่างจากบัฟเฟอร์ไบต์ปกติที่ไม่ได้จัดสรรพื้นที่เก็บข้อมูลในฮีปที่มีการจัดการ และเข้าถึงได้โดยตรงจากโค้ดเนทีฟเสมอ (รับที่อยู่ด้วย GetDirectBufferAddress) การเข้าถึงข้อมูลจากโค้ดที่มีการจัดการอาจช้ามาก ทั้งนี้ขึ้นอยู่กับวิธีที่ใช้ในการเข้าถึงบัฟเฟอร์ไบต์โดยตรง

การเลือกใช้ขึ้นอยู่กับ 2 ปัจจัยต่อไปนี้

  1. การเข้าถึงข้อมูลส่วนใหญ่จะมาจากโค้ดที่เขียนใน Java หรือใน C/C++ ไหม
  2. หากในที่สุดข้อมูลจะถูกส่งไปยัง API ของระบบ ข้อมูลนั้นจะต้องอยู่ในรูปแบบใด (ตัวอย่างเช่น หากในที่สุดข้อมูลจะส่งไปยังฟังก์ชันที่รับ byte[] การประมวลผลในByteBufferโดยตรงอาจไม่เหมาะสม)

หากไม่มีผู้ชนะที่ชัดเจน ให้ใช้ Direct Byte Buffer การรองรับสำหรับฟีเจอร์เหล่านี้ สร้างขึ้นโดยตรงใน JNI และประสิทธิภาพควรจะดีขึ้นในรุ่นที่จะออกในอนาคต