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

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

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

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

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

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

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

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

JavaVM และ JNIEnv

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

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

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

การประกาศ C ของ JNIEnv และ JavaVM แตกต่างจาก C++ ประกาศ ไฟล์ที่รวม "jni.h" มี typedef ที่แตกต่างกัน โดยขึ้นอยู่กับว่าได้รวมอยู่ใน 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 ที่จะสร้างและเพิ่มลงใน "main" ThreadGroup, เพื่อให้โปรแกรมแก้ไขข้อบกพร่องเห็นได้ กำลังโทรหา AttachCurrentThread() ในชุดข้อความที่แนบอยู่แล้วจะถือว่าไม่ดำเนินการ

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

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

jclass, jmethodID และ jfieldID

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

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

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

หากประสิทธิภาพเป็นสิ่งสำคัญ การค้นหาค่าเพียงครั้งเดียวและแคชผลลัพธ์ก็อาจเป็นประโยชน์ ในโค้ดดั้งเดิมของคุณ เพราะมีการจำกัด JavaVM ไว้ที่ 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 อย่าเปรียบเทียบ การอ้างอิงที่มี == ในโค้ดแบบเนทีฟ

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

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

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

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

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

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

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

และโปรดทราบว่า Flag JNI_COMMIT จะไม่เผยแพร่อาร์เรย์ และคุณจะต้องเรียกใช้ Release อีกครั้งด้วย Flag อื่น ในที่สุด

การโทรในภูมิภาค

มีอีกตัวเลือกหนึ่งที่ใช้แทนการโทร เช่น 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 1 ครั้งแทน 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 ให้ค้นหารหัสเมธอด getMessage "()Ljava/lang/String;" ให้เรียกใช้ และถ้าผลลัพธ์ ไม่ใช่ค่าว่าง ให้ใช้ GetStringUTFChars เพื่อรับสิ่งที่คุณสามารถ มือ printf(3) หรือเทียบเท่า

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

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

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

  • อาร์เรย์: กำลังพยายามจัดสรรอาร์เรย์ขนาดลบ
  • ตัวชี้ไม่ถูกต้อง: การส่ง jarray/jclass/jobject/jstring ที่ไม่ถูกต้องไปยังการเรียกใช้ JNI หรือส่งตัวชี้ NULL ไปยังการเรียกใช้ JNI ด้วยอาร์กิวเมนต์ที่ไม่เป็นค่าว่าง
  • ชื่อคลาส: ส่งผ่านอะไรก็ได้ยกเว้นรูปแบบ "java/lang/String" ของชื่อคลาสไปยังการเรียกใช้ JNI
  • การโทรที่สำคัญ: การเรียก JNI ระหว่างการเรียกใช้ที่ "สำคัญ" กับผลงานที่เกี่ยวข้อง
  • Direct ByteBuffers: ส่งอาร์กิวเมนต์ที่ไม่ถูกต้องไปยัง NewDirectByteBuffer
  • ข้อยกเว้น: การโทรหา JNI ในขณะที่มีข้อยกเว้นที่รอดำเนินการ
  • JNIEnv*s: ใช้ JNIEnv* จากชุดข้อความที่ไม่ถูกต้อง
  • jfieldID: การใช้ jfieldID ที่เป็น NULL หรือใช้ jfieldID เพื่อตั้งค่าฟิลด์เป็นค่าผิดประเภท (เช่น พยายามกำหนด StringBuilder ไปยังฟิลด์สตริง เป็นต้น) หรือใช้ jfieldID สำหรับฟิลด์แบบคงที่เพื่อตั้งค่าฟิลด์อินสแตนซ์ หรือในทางกลับกัน หรือใช้ jfieldID จากคลาสหนึ่งกับอินสแตนซ์ของคลาสอื่น
  • jmethodID: การใช้ jmethodID ผิดประเภทเมื่อเรียก Call*Method JNI: ประเภทการแสดงผลไม่ถูกต้อง คงที่/ไม่คงที่ ประเภทที่ไม่ถูกต้องสำหรับ "this" (สำหรับการเรียกที่ไม่คงที่) หรือคลาสที่ไม่ถูกต้อง (สำหรับการเรียกแบบคงที่)
  • การอ้างอิง: ใช้ DeleteGlobalRef/DeleteLocalRef กับข้อมูลอ้างอิงประเภทที่ไม่ถูกต้อง
  • โหมดการเผยแพร่: การส่งโหมดการเผยแพร่ที่ไม่ถูกต้องไปยังการเรียกใช้การเผยแพร่ (เวอร์ชันอื่นที่ไม่ใช่ 0, JNI_ABORT หรือ JNI_COMMIT)
  • ความปลอดภัยของประเภท: การส่งคืนประเภทที่เข้ากันไม่ได้จากเมธอดเนทีฟ (แสดงผล StringBuilder จากเมธอดที่ประกาศให้แสดงผลสตริง เช่น)
  • 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 ของคุณ จะส่งออกจากคลังของคุณ วิธีนี้จะสร้างโค้ดที่เร็วขึ้นและมีขนาดเล็กลง และหลีกเลี่ยง ข้อผิดพลาดกับไลบรารีอื่นๆ ที่โหลดในแอปของคุณ (แต่สร้างสแต็กเทรซที่มีประโยชน์น้อยกว่า หากแอปขัดข้องในโค้ดแบบเนทีฟ)

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

Kotlin

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

Java

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

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

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

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

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

    return JNI_VERSION_1_6;
}

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

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

โทรเนทีฟได้เร็วขึ้นด้วย @FastNative และ @CriticalNative

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

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

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

คำอธิบายประกอบถูกนำมาใช้สำหรับการใช้งานระบบตั้งแต่ Android 8 กลายเป็นสาธารณชนที่ผ่านการทดสอบ CTS API ใน 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" แล้ว ในระหว่างการค้นหาชื่อเมธอด กำลังทำงาน ซึ่งจำเป็นต้องมีการลงทะเบียนอย่างชัดเจนหรือการย้าย วิธีการแบบเนทีฟ ออกจากชั้นเรียนภายใน

  • การถอดชุดข้อความ

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

  • การพิจารณาประเภทการอ้างอิงด้วย 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 Recallion API Class.forName(name)

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

ขณะทำงานกับโค้ดแบบเนทีฟ การเห็นความล้มเหลวเช่นนี้ไม่ใช่เรื่องแปลก

java.lang.UnsatisfiedLinkError: Library foo not found

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

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

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

คลาสของความล้มเหลวอื่นๆ ใน 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) โปรดทราบว่าก่อนใช้ไอศกรีม 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 เป็นวิธีที่ดีในการค้นหา ชื่อภายในของชั้นเรียน

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

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

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

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

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

วิธีหลีกเลี่ยงปัญหามีดังนี้

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

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

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

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

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

วิธีเลือกใช้จะขึ้นอยู่กับ 2 ปัจจัยดังนี้

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

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