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

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 ไม่ใช่วัตถุ (พวกเขาอาจผ่าน ระหว่างเทรด และจะใช้ได้จนกว่าการเรียกใช้ "ถอนการอ้างสิทธิ์" ที่ตรงกัน)

กรณีหนึ่งที่น่าสนใจซึ่งควรกล่าวถึงแยกต่างหาก หากคุณแนบโฆษณาเนทีฟ เทรดที่มี 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 โดยไม่กรอง คุณต้องตัดอักขระที่ไม่ถูกต้องออกหรือแปลงเป็นรูปแบบ UTF-8 ที่แก้ไขแล้วที่เหมาะสม เว้นแต่คุณจะทราบว่าข้อมูลเป็น MUTF-8 ที่ถูกต้อง (หรือ ASCII แบบ 7 บิต ซึ่งเป็นชุดย่อยที่เข้ากันได้) หากไม่ดำเนินการดังกล่าว การแปลง 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 ที่มีอาร์กิวเมนต์ที่ไม่ใช่ NULL
  • ชื่อคลาส: ส่งอะไรก็ได้ยกเว้นรูปแบบ "java/lang/String" ของชื่อคลาสไปยังการเรียกใช้ JNI
  • การโทรที่สำคัญ: การเรียก JNI ระหว่างการเรียกใช้ที่ "สำคัญ" กับผลงานที่เกี่ยวข้อง
  • Direct ByteBuffers: ส่งอาร์กิวเมนต์ที่ไม่ถูกต้องไปยัง NewDirectByteBuffer
  • ข้อยกเว้น: การโทรหา JNI ในขณะที่มีข้อยกเว้นที่รอดำเนินการ
  • JNIEnv*: ใช้ 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 หรือหากไม่มี (เนื่องจากการเรียกมาจากเธรดเนทีฟที่เพิ่งแนบ) ก็จะใช้ตัวโหลดคลาส "ระบบ" ตัวโหลดคลาสระบบไม่ทราบเกี่ยวกับ ดังนั้นคุณจะค้นหาชั้นเรียนของคุณเองที่มี FindClass อยู่ในนั้นไม่ได้ บริบท ซึ่งจะทำให้ JNI_OnLoad เป็นที่ที่สะดวกในการค้นหาและแคชคลาส 1 ครั้ง คุณมีข้อมูลอ้างอิงทั่วโลกที่ถูกต้องของ jclass คุณจะใช้รหัสดังกล่าวจากชุดข้อความที่แนบได้

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

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

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

ขณะเรียกใช้เมธอด @FastNative หรือ @CriticalNative การรวบรวมขยะจะระงับเธรดสำหรับงานที่สำคัญไม่ได้และอาจถูกบล็อก ไม่ใช้ สำหรับวิธีการที่ใช้มานาน ซึ่งรวมถึง Method ที่มักรวดเร็วแต่โดยทั่วไปไม่มีขอบเขต โดยเฉพาะอย่างยิ่ง โค้ดไม่ควรดำเนินการ 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" แล้ว ในระหว่างการค้นหาชื่อวิธีการ กำลังทำงาน ซึ่งจำเป็นต้องมีการลงทะเบียนอย่างชัดเจนหรือการย้าย วิธีการแบบเนทีฟ ออกจากชั้นเรียนภายใน

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

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

หากคุณเปิดใช้งานการย่อโค้ด ให้ตรวจสอบว่าคุณ กำหนดค่ารหัสที่จะเก็บไว้ กำลังกำหนดค่า กฎการเก็บที่เหมาะสมมีความสำคัญ เพราะ ตัวลดขนาดโค้ดอาจนำคลาส เมธอด หรือช่องที่ใช้จาก 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 จะใช้ Class Loader ที่เชื่อมโยงกับฟังก์ชันที่เรียก System.loadLibrary (นี่เป็นกฎพิเศษที่มีไว้เพื่อให้การเริ่มต้นใช้งานไลบรารีสะดวกขึ้น) หากโค้ดแอปของคุณโหลดไลบรารี FindClassจะใช้ตัวโหลดคลาสที่ถูกต้อง
  • ส่งอินสแตนซ์ของคลาสไปยังฟังก์ชันที่ต้องการ โดยประกาศเมธอดเนทีฟให้รับอาร์กิวเมนต์คลาส แล้วส่ง 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 โดยตรง และประสิทธิภาพจะดีขึ้นในรุ่นต่อๆ ไป