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 การใช้การเพิ่มประสิทธิภาพเหล่านี้โดยไม่ การจดทะเบียนกับ JNIRegisterNatives
อย่างชัดเจน ทำให้เกิดข้อขัดข้องใน Android 8-11FindClass
ขว้างClassNotFoundException
สำหรับความเข้ากันได้แบบย้อนหลัง Android ต้องใช้
ClassNotFoundException
แทนที่จะเป็นNoClassDefFoundError
เมื่อไม่พบชั้นเรียนFindClass
ลักษณะการทำงานนี้สอดคล้องกับ Java Recallion APIClass.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;
)
- สำหรับการค้นหาเมธอดแบบเลื่อนเวลา ไม่ได้ประกาศฟังก์ชัน C++ ด้วย
การใช้ 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 ปัจจัยดังนี้
- การเข้าถึงข้อมูลส่วนใหญ่จะเกิดขึ้นจากโค้ดที่เขียนด้วย Java หรือ C/C++
- หากสุดท้ายแล้วส่งข้อมูลไปยัง API ของระบบ รูปแบบใด
ต้องอยู่ในนั้นไหม (ตัวอย่างเช่น หากในที่สุดแล้วข้อมูลถูกส่งไปยัง
ที่ใช้เวลา 1 ไบต์[] ประมวลผลใน
ByteBuffer
อาจไม่ฉลาดก็ได้)
หากไม่มีรายการที่มีประสิทธิภาพสูงอย่างชัดเจน ให้ใช้บัฟเฟอร์ไบต์โดยตรง การสนับสนุนสำหรับคนเหล่านี้ สร้างขึ้นใน JNI โดยตรง และประสิทธิภาพจะดีขึ้นในรุ่นต่อๆ ไป