JNI คือ Java Native Interface ซึ่งกำหนดวิธีให้ไบต์โค้ดที่ Android คอมไพล์จากโค้ดที่มีการจัดการ (เขียนในภาษาโปรแกรม Java หรือ Kotlin) โต้ตอบกับโค้ดที่มาพร้อมเครื่อง (เขียนใน C/C++) ได้ JNI ไม่ขึ้นอยู่กับผู้ให้บริการ รองรับการโหลดโค้ดจากไลบรารีที่ใช้ร่วมกันแบบไดนามิก และแม้ว่าบางครั้งจะซับซ้อน แต่ก็มีประสิทธิภาพพอสมควร
หมายเหตุ: เนื่องจาก Android คอมไพล์ Kotlin เป็นไบต์โค้ดที่ใช้กับ ART ได้ในลักษณะเดียวกับภาษาโปรแกรม Java คุณจึงใช้คำแนะนำในหน้านี้กับทั้งภาษาโปรแกรม Kotlin และ Java ได้ในแง่ของสถาปัตยกรรม JNI และค่าใช้จ่ายที่เกี่ยวข้อง ดูข้อมูลเพิ่มเติมได้ที่ Kotlin และ Android
หากยังไม่คุ้นเคย โปรดอ่านข้อกำหนดของ Java Native Interface เพื่อทำความเข้าใจวิธีการทำงานของ JNI และฟีเจอร์ที่พร้อมใช้งาน คุณอาจไม่เข้าใจ บางส่วนของอินเทอร์เฟซในทันทีที่อ่าน ครั้งแรก ดังนั้นส่วนถัดไป 2-3 ส่วนอาจมีประโยชน์สำหรับคุณ
หากต้องการเรียกดูการอ้างอิง JNI ทั่วโลกและดูว่ามีการสร้างและลบการอ้างอิง JNI ทั่วโลกที่ใด ให้ใช้มุมมองฮีป JNI ใน Memory Profiler ใน Android Studio 3.2 ขึ้นไป
เคล็ดลับทั่วไป
พยายามลดร่องรอยของเลเยอร์ JNI มีหลายมิติที่ต้องพิจารณาในที่นี้ โซลูชัน JNI ควรพยายามปฏิบัติตามหลักเกณฑ์ต่อไปนี้ (เรียงตามลำดับความสำคัญจากมากไปน้อย โดยเริ่มจากหลักเกณฑ์ที่สำคัญที่สุด)
- ลดการจัดเรียงทรัพยากรในเลเยอร์ JNI การส่งผ่านข้อมูลในเลเยอร์ JNI มีค่าใช้จ่ายที่สำคัญ พยายามออกแบบอินเทอร์เฟซที่ลดปริมาณ ข้อมูลที่คุณต้องจัดเรียงและความถี่ที่คุณต้องจัดเรียงข้อมูล
- หลีกเลี่ยงการสื่อสารแบบไม่พร้อมกันระหว่างโค้ดที่เขียนในภาษาโปรแกรมที่มีการจัดการ และโค้ดที่เขียนใน C++ เมื่อเป็นไปได้ ซึ่งจะช่วยให้ดูแลรักษาอินเทอร์เฟซ JNI ได้ง่ายขึ้น โดยปกติแล้ว คุณจะทำให้การอัปเดต UI แบบไม่พร้อมกันง่ายขึ้นได้โดยเก็บการอัปเดตแบบไม่พร้อมกันไว้ในภาษาเดียวกับ UI ตัวอย่างเช่น แทนที่จะ เรียกใช้ฟังก์ชัน C++ จากเทรด UI ในโค้ด Java ผ่าน JNI คุณควร ใช้ Callback ระหว่าง 2 เทรดในภาษาโปรแกรม Java โดยให้เทรดใดเทรดหนึ่ง ทำการเรียก C++ ที่บล็อก แล้วแจ้งเทรด UI เมื่อการเรียกที่บล็อก เสร็จสมบูรณ์
- ลดจำนวนเธรดที่ JNI ต้องเข้าถึงหรือเข้าถึง หากจำเป็นต้องใช้ Thread Pool ทั้งในภาษา Java และ C++ ให้พยายามรักษาการสื่อสาร JNI ระหว่างเจ้าของ Pool แทนที่จะเป็นระหว่าง Worker Thread แต่ละรายการ
- เก็บโค้ดอินเทอร์เฟซไว้ในแหล่งที่มาของ C++ และ Java ที่ระบุได้ง่ายและมีจำนวนน้อย เพื่ออำนวยความสะดวกในการปรับโครงสร้างโค้ดในอนาคต พิจารณาใช้ไลบรารีการสร้าง JNI อัตโนมัติ ตามความเหมาะสม
JavaVM และ JNIEnv
JNI กำหนดโครงสร้างข้อมูลหลัก 2 อย่าง ได้แก่ "JavaVM" และ "JNIEnv" ทั้ง 2 อย่างนี้เป็นตัวชี้ไปยังตัวชี้ไปยังตารางฟังก์ชัน (ในเวอร์ชัน C++ จะเป็นคลาสที่มี พอยน์เตอร์ไปยังตารางฟังก์ชันและฟังก์ชันสมาชิกสำหรับแต่ละฟังก์ชัน JNI ที่อ้อมผ่าน ตาราง) JavaVM มีฟังก์ชัน "invocation interface" ซึ่งช่วยให้คุณสร้างและทำลาย JavaVM ได้ ในทางทฤษฎี คุณสามารถมี JavaVM หลายรายการต่อกระบวนการได้ แต่ Android อนุญาตเพียงรายการเดียว
JNIEnv มีฟังก์ชัน JNI ส่วนใหญ่ ฟังก์ชันเนทีฟทั้งหมดจะรับ JNIEnv เป็นอาร์กิวเมนต์แรก ยกเว้นเมธอด @CriticalNative
ดูการเรียกเนทีฟที่เร็วขึ้น
JNIEnv ใช้สำหรับที่เก็บข้อมูลเฉพาะเธรด ด้วยเหตุนี้ คุณจึงแชร์ JNIEnv ระหว่างเธรดไม่ได้
หากโค้ดไม่มีวิธีอื่นในการรับ JNIEnv คุณควรแชร์
JavaVM และใช้ GetEnv
เพื่อค้นหา JNIEnv ของเธรด (หากมี ดูAttachCurrentThread
ด้านล่าง)
การประกาศ JNIEnv และ JavaVM ใน C จะแตกต่างจากการประกาศใน C++
"jni.h"
include file มี typedefs ที่แตกต่างกัน
ขึ้นอยู่กับว่ารวมอยู่ใน C หรือ C++ ด้วยเหตุนี้จึงไม่ควร
รวมอาร์กิวเมนต์ JNIEnv ในไฟล์ส่วนหัวที่รวมอยู่ในทั้ง 2 ภาษา (กล่าวอีกนัยหนึ่งคือ หากไฟล์ส่วนหัวต้องใช้ #ifdef __cplusplus
คุณอาจต้องดำเนินการเพิ่มเติมหากมีสิ่งใดในส่วนหัวนั้นอ้างอิงถึง JNIEnv)
ชุดข้อความ
ชุดข้อความทั้งหมดเป็นชุดข้อความ Linux ซึ่งเคอร์เนลเป็นผู้กำหนดเวลา โดยปกติแล้วจะเริ่มจากโค้ดที่มีการจัดการ (ใช้ Thread.start()
)
แต่ก็สามารถสร้างที่อื่นแล้วแนบไปกับ JavaVM
ได้เช่นกัน ตัวอย่างเช่น คุณสามารถแนบเธรดที่เริ่มต้นด้วย pthread_create()
หรือ std::thread
ได้โดยใช้ฟังก์ชัน AttachCurrentThread()
หรือ AttachCurrentThreadAsDaemon()
เธรดจะไม่มี JNIEnv และโทรหา JNI ไม่ได้จนกว่าจะแนบเธรด
โดยปกติแล้ว การใช้ Thread.start()
เพื่อสร้างเธรดที่ต้องเรียกใช้โค้ด Java เป็นวิธีที่ดีที่สุด
การทำเช่นนี้จะช่วยให้คุณมีพื้นที่สแต็กเพียงพอ อยู่ใน ThreadGroup
ที่ถูกต้อง และใช้ ClassLoader
เดียวกันกับโค้ด Java
นอกจากนี้ การตั้งชื่อเธรดเพื่อการแก้ไขข้อบกพร่องใน Java ยังทำได้ง่ายกว่าจากโค้ดเนทีฟ (ดู pthread_setname_np()
หากคุณมี pthread_t
หรือ thread_t
และดู std::thread::native_handle()
หากคุณมี std::thread
และต้องการ pthread_t
)
การแนบเธรดที่สร้างขึ้นโดยเนทีฟจะทำให้มีการสร้างออบเจ็กต์ java.lang.Thread
และเพิ่มลงใน ThreadGroup
"main"
ซึ่งจะทำให้ดีบักเกอร์มองเห็น การเรียกใช้ AttachCurrentThread()
ในเธรดที่แนบอยู่แล้วจะไม่มีผล
Android จะไม่ระงับเธรดที่เรียกใช้โค้ดที่มาพร้อมเครื่อง หาก การเก็บขยะกำลังดำเนินการอยู่ หรือดีบักเกอร์ได้ออกคำขอระงับ Android จะหยุดชั่วคราวในครั้งถัดไปที่ทำการเรียก JNI
เธรดที่แนบผ่าน JNI ต้องเรียกใช้
DetachCurrentThread()
ก่อนที่จะออก
หากการเขียนโค้ดโดยตรงเป็นเรื่องยาก ใน Android 2.0 (Eclair) ขึ้นไป คุณ
สามารถใช้ pthread_key_create()
เพื่อกำหนดฟังก์ชัน
ทำลายล้างที่จะเรียกใช้ก่อนที่เธรดจะออก และ
เรียกใช้ DetachCurrentThread()
จากที่นั่น (ใช้คีย์นั้นกับ pthread_setspecific()
เพื่อจัดเก็บ JNIEnv ใน
ที่เก็บข้อมูลเฉพาะเธรด วิธีนี้จะส่งไปยังตัวทำลายของคุณเป็น
อาร์กิวเมนต์)
jclass, jmethodID และ jfieldID
หากต้องการเข้าถึงฟิลด์ของออบเจ็กต์จากโค้ดดั้งเดิม ให้ทำดังนี้
- รับการอ้างอิงออบเจ็กต์คลาสสำหรับคลาสที่มี
FindClass
- รับรหัสฟิลด์สำหรับฟิลด์ที่มี
GetFieldID
- รับเนื้อหาของฟิลด์ด้วยสิ่งที่เหมาะสม เช่น
GetIntField
ในทำนองเดียวกัน หากต้องการเรียกใช้เมธอด คุณต้องรับการอ้างอิงออบเจ็กต์คลาสก่อน แล้วจึงรับรหัสเมธอด โดยปกติแล้ว รหัสจะเป็นเพียง ตัวชี้ไปยังโครงสร้างข้อมูลรันไทม์ภายใน การค้นหาอาจต้องมีการเปรียบเทียบสตริงหลายรายการ แต่เมื่อพบแล้ว การเรียกจริงเพื่อรับฟิลด์หรือเรียกใช้เมธอด จะรวดเร็วมาก
หากประสิทธิภาพเป็นสิ่งสำคัญ คุณควรค้นหาค่าเพียงครั้งเดียวและแคชผลลัพธ์ ในโค้ดดั้งเดิม เนื่องจากแต่ละกระบวนการมี JavaVM ได้เพียง 1 รายการ จึงควรเก็บข้อมูลนี้ไว้ในโครงสร้างแบบคงที่ในเครื่อง
การอ้างอิงคลาส รหัสฟิลด์ และรหัสเมธอดจะรับประกันว่าถูกต้องจนกว่าจะมีการยกเลิกการโหลดคลาส ระบบจะยกเลิกการโหลดคลาสก็ต่อเมื่อคลาสทั้งหมดที่เชื่อมโยงกับ ClassLoader สามารถรวบรวมขยะได้เท่านั้น
ซึ่งเกิดขึ้นได้ยากแต่ก็ไม่ใช่ว่าจะเกิดขึ้นไม่ได้ใน Android อย่างไรก็ตาม โปรดทราบว่า jclass
เป็นข้อมูลอ้างอิงของคลาสและต้องได้รับการปกป้องด้วยการเรียกใช้
NewGlobalRef
(ดูส่วนถัดไป)
หากต้องการแคชรหัสเมื่อโหลดชั้นเรียน และแคชรหัสอีกครั้งโดยอัตโนมัติ หากมีการยกเลิกการโหลดชั้นเรียนและโหลดซ้ำ วิธีที่ถูกต้องในการเริ่มต้น รหัสคือการเพิ่มโค้ดที่มีลักษณะดังนี้ลงในชั้นเรียนที่เหมาะสม
Kotlin
companion object { /* * We use a static class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private external fun nativeInit() init { nativeInit() } }
Java
/* * We use a class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private static native void nativeInit(); static { nativeInit(); }
สร้างnativeClassInit
เมธอดในโค้ด C/C++ ที่ทำการค้นหารหัส โค้ด
จะทำงานครั้งเดียวเมื่อมีการเริ่มต้นคลาส หากคลาสถูกยกเลิกการโหลดและ
โหลดซ้ำ ระบบจะเรียกใช้ฟังก์ชันอีกครั้ง
การอ้างอิงในเครื่องและทั่วโลก
อาร์กิวเมนต์ทุกรายการที่ส่งไปยังเมธอดเนทีฟและออบเจ็กต์เกือบทุกรายการที่ฟังก์ชัน JNI แสดงผลคือ "การอ้างอิงภายใน" ซึ่งหมายความว่าตัวแปรนี้จะใช้ได้ตลอด ระยะเวลาของเมธอดดั้งเดิมปัจจุบันในเธรดปัจจุบัน แม้ว่าออบเจ็กต์จะยังคงอยู่ต่อไปหลังจากที่เมธอดดั้งเดิม ส่งคืนค่าแล้ว แต่การอ้างอิงก็ไม่ถูกต้อง
ซึ่งมีผลกับคลาสย่อยทั้งหมดของ jobject
รวมถึง
jclass
, jstring
และ jarray
(รันไทม์จะเตือนคุณเกี่ยวกับการใช้การอ้างอิงส่วนใหญ่ที่ไม่ถูกต้องเมื่อเปิดใช้การตรวจสอบ JNI แบบขยาย)
วิธีเดียวที่จะรับการอ้างอิงที่ไม่ใช่ในเครื่องคือผ่านฟังก์ชัน
NewGlobalRef
และ NewWeakGlobalRef
หากต้องการเก็บข้อมูลอ้างอิงไว้นานขึ้น คุณต้องใช้ข้อมูลอ้างอิง "ส่วนกลาง" ฟังก์ชัน NewGlobalRef
ใช้การอ้างอิงในเครื่องเป็นอาร์กิวเมนต์และแสดงผลการอ้างอิงส่วนกลาง
เรารับประกันว่าการอ้างอิงทั่วโลกจะใช้ได้จนกว่าคุณจะโทรหา
DeleteGlobalRef
โดยทั่วไปมักใช้รูปแบบนี้เมื่อแคช jclass ที่ส่งคืนจาก FindClass
เช่น
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
เมธอด JNI ทั้งหมดรับทั้งการอ้างอิงในเครื่องและการอ้างอิงส่วนกลางเป็นอาร์กิวเมนต์
การอ้างอิงไปยังออบเจ็กต์เดียวกันอาจมีค่าแตกต่างกันได้
เช่น ค่าที่ส่งคืนจากการเรียกใช้ NewGlobalRef
ในออบเจ็กต์เดียวกันติดต่อกันอาจแตกต่างกัน
หากต้องการดูว่าการอ้างอิง 2 รายการอ้างอิงถึงออบเจ็กต์เดียวกันหรือไม่
คุณต้องใช้ฟังก์ชัน IsSameObject
อย่าเปรียบเทียบ
การอ้างอิงกับ ==
ในโค้ดเนทีฟ
ผลที่ตามมาอย่างหนึ่งคือคุณต้องไม่ถือว่าการอ้างอิงออบเจ็กต์นั้นคงที่หรือมีลักษณะเฉพาะ
ในโค้ดเนทีฟ ค่าที่แสดงออบเจ็กต์อาจแตกต่างกัน
จากการเรียกใช้เมธอดครั้งหนึ่งไปยังครั้งถัดไป และเป็นไปได้ที่ออบเจ็กต์ 2 รายการ
ที่แตกต่างกันอาจมีค่าเดียวกันในการเรียกใช้ที่ต่อเนื่องกัน อย่าใช้ค่า
jobject
เป็นคีย์
โปรแกรมเมอร์ต้อง "ไม่จัดสรรมากเกินไป" สำหรับการอ้างอิงในเครื่อง ในทางปฏิบัติ หมายความว่าหากคุณสร้างการอ้างอิงในเครื่องจำนวนมาก เช่น ขณะเรียกใช้ผ่านอาร์เรย์ของออบเจ็กต์ คุณควรปล่อยการอ้างอิงเหล่านั้นด้วยตนเองโดยใช้ DeleteLocalRef
แทนที่จะปล่อยให้ JNI ทำแทน
การติดตั้งใช้งานจำเป็นต้องจองช่องสำหรับ
การอ้างอิงในเครื่อง 16 รายการเท่านั้น ดังนั้นหากต้องการมากกว่านั้น คุณควรลบรายการที่ไม่ต้องการออกไปเรื่อยๆ หรือใช้ EnsureLocalCapacity
/PushLocalFrame
เพื่อจองเพิ่มเติม
โปรดทราบว่า jfieldID
และ jmethodID
เป็นประเภทที่ทึบแสง ไม่ใช่การอ้างอิงออบเจ็กต์ และไม่ควรส่งไปยัง NewGlobalRef
พอยน์เตอร์ข้อมูลดิบ
ที่ฟังก์ชันอย่าง GetStringUTFChars
และ GetByteArrayElements
แสดงผลก็ไม่ใช่ออบเจ็กต์เช่นกัน (อาจส่งผ่านระหว่างเธรดและจะใช้ได้จนกว่าจะมีการเรียกใช้ฟังก์ชัน Release ที่ตรงกัน)
มีกรณีที่ผิดปกติกรณีหนึ่งที่ควรกล่าวถึงแยกกัน หากแนบเธรดดั้งเดิมด้วย AttachCurrentThread
โค้ดที่คุณเรียกใช้จะไม่ปล่อยการอ้างอิงในเครื่องโดยอัตโนมัติจนกว่าเธรดจะแยกออก คุณจะต้องลบการอ้างอิงในเครื่อง
ที่สร้างขึ้นด้วยตนเอง โดยทั่วไปแล้ว โค้ดเนทีฟใดๆ
ที่สร้างการอ้างอิงในเครื่องในลูปอาจต้องมีการลบด้วยตนเอง
โปรดระมัดระวังในการใช้การอ้างอิงส่วนกลาง การอ้างอิงส่วนกลางอาจหลีกเลี่ยงไม่ได้ แต่ก็แก้ไขข้อบกพร่องได้ยาก และอาจทำให้เกิดลักษณะการทำงานของหน่วยความจำที่วินิจฉัยได้ยาก เมื่อค่าอื่นๆ เท่ากันหมด โซลูชันที่มีการอ้างอิงส่วนกลางน้อยกว่าน่าจะดีกว่า
สตริง UTF-8 และ UTF-16
ภาษาโปรแกรม Java ใช้ UTF-16 JNI มีเมธอดที่ใช้ได้กับ Modified UTF-8 เพื่อความสะดวก การเข้ารหัสที่แก้ไขแล้วมีประโยชน์สำหรับโค้ด C เนื่องจากจะเข้ารหัส \u0000 เป็น 0xc0 0x80 แทน 0x00 ข้อดีของวิธีนี้คือคุณสามารถใช้สตริงที่ลงท้ายด้วย 0 ในรูปแบบ C ได้ ซึ่งเหมาะสำหรับใช้กับฟังก์ชันสตริง libc มาตรฐาน ข้อเสียคือคุณไม่สามารถส่งข้อมูล UTF-8 ที่กำหนดเองไปยัง JNI และคาดหวังให้ทำงานได้อย่างถูกต้อง
หากต้องการรับการแสดง UTF-16 ของ String
ให้ใช้ GetStringChars
โปรดทราบว่าสตริง UTF-16 ไม่ได้ลงท้ายด้วย 0 และอนุญาตให้ใช้ \u0000 ได้
ดังนั้นคุณจึงต้องเก็บความยาวของสตริงไว้พร้อมกับตัวชี้ jchar
อย่าลืมRelease
สตริงที่คุณGet
ฟังก์ชันสตริงจะแสดงผล jchar*
หรือ jbyte*
ซึ่งเป็น
พอยน์เตอร์สไตล์ C ไปยังข้อมูลดั้งเดิมแทนที่จะเป็นข้อมูลอ้างอิงในเครื่อง โดยจะรับประกันว่าโทเค็นจะใช้งานได้จนกว่าจะมีการเรียกใช้ Release
ซึ่งหมายความว่าโทเค็นจะไม่
เผยแพร่เมื่อเมธอดดั้งเดิมแสดงผล
ข้อมูลที่ส่งไปยัง NewStringUTF ต้องอยู่ในรูปแบบ UTF-8 ที่แก้ไขแล้ว
ข้อผิดพลาดที่พบบ่อยคือการอ่านข้อมูลอักขระจากไฟล์หรือสตรีมเครือข่าย
แล้วส่งให้ NewStringUTF
โดยไม่กรอง
หากไม่ทราบว่าข้อมูลเป็น MUTF-8 ที่ถูกต้อง (หรือ ASCII แบบ 7 บิต ซึ่งเป็นชุดย่อยที่เข้ากันได้)
คุณจะต้องนำอักขระที่ไม่ถูกต้องออกหรือแปลงเป็นรูปแบบ UTF-8 ที่แก้ไขแล้วที่ถูกต้อง
หากไม่ทำเช่นนั้น การแปลง UTF-16 อาจให้ผลลัพธ์ที่ไม่คาดคิด
CheckJNI ซึ่งเปิดอยู่โดยค่าเริ่มต้นสำหรับโปรแกรมจำลอง จะสแกนสตริง
และยกเลิก VM หากได้รับอินพุตที่ไม่ถูกต้อง
ก่อน Android 8 การทำงานกับสตริง UTF-16 มักจะเร็วกว่าเนื่องจาก Android
ไม่จำเป็นต้องคัดลอกใน GetStringChars
ในขณะที่
GetStringUTFChars
ต้องมีการจัดสรรและการแปลงเป็น UTF-8
Android 8 เปลี่ยนการแสดง String
ให้ใช้ 8 บิตต่ออักขระ
สำหรับสตริง ASCII (เพื่อประหยัดหน่วยความจำ) และเริ่มใช้
ตัวเก็บขยะ
แบบเคลื่อนที่ ฟีเจอร์เหล่านี้ช่วยลดจำนวนกรณีที่ ART
สามารถระบุพอยน์เตอร์ไปยังString
ข้อมูลได้โดยไม่ต้องทำสำเนา แม้แต่สำหรับ GetStringCritical
อย่างไรก็ตาม หากสตริงส่วนใหญ่ที่โค้ดประมวลผล
มีความยาวไม่มาก คุณจะหลีกเลี่ยงการจัดสรรและการยกเลิกการจัดสรรได้ในกรณีส่วนใหญ่โดย
ใช้บัฟเฟอร์ที่จัดสรรในสแต็กและ GetStringRegion
หรือ
GetStringUTFRegion
เช่น
constexpr size_t kStackBufferSize = 64u; jchar stack_buffer[kStackBufferSize]; std::unique_ptr<jchar[]> heap_buffer; jchar* buffer = stack_buffer; jsize length = env->GetStringLength(str); if (length > kStackBufferSize) { heap_buffer.reset(new jchar[length]); buffer = heap_buffer.get(); } env->GetStringRegion(str, 0, length, buffer); process_data(buffer, length);
อาร์เรย์พื้นฐาน
JNI มีฟังก์ชันสำหรับการเข้าถึงเนื้อหาของออบเจ็กต์อาร์เรย์ แม้ว่าอาร์เรย์ของออบเจ็กต์จะต้องเข้าถึงทีละรายการ แต่อาร์เรย์ของ Primitive สามารถอ่านและเขียนได้โดยตรงราวกับว่ามีการประกาศใน C
เพื่อให้อินเทอร์เฟซมีประสิทธิภาพมากที่สุดโดยไม่จำกัด
การใช้งาน VM Get<PrimitiveType>ArrayElements
ตระกูลการเรียกใช้ช่วยให้รันไทม์สามารถส่งคืนพอยน์เตอร์ไปยังองค์ประกอบจริง หรือ
จัดสรรหน่วยความจำและทำสำเนาได้ ไม่ว่าจะด้วยวิธีใดก็ตาม ระบบจะรับประกันว่าพอยน์เตอร์ดิบที่แสดงผล
จะถูกต้องจนกว่าจะมีการเรียกใช้ Release
ที่เกี่ยวข้อง (ซึ่งหมายความว่าหากไม่ได้คัดลอกข้อมูล ระบบจะปักหมุดออบเจ็กต์อาร์เรย์
และย้ายตำแหน่งไม่ได้ซึ่งเป็นส่วนหนึ่งของการกระชับฮีป)
คุณต้อง Release
อาร์เรย์ทุกรายการที่คุณ Get
นอกจากนี้ หากGet
การเรียกใช้ล้มเหลว คุณต้องตรวจสอบว่าโค้ดไม่ได้พยายามRelease
พอยน์เตอร์ NULL
ในภายหลัง
คุณตรวจสอบได้ว่ามีการคัดลอกข้อมูลหรือไม่โดยส่งพอยน์เตอร์ที่ไม่ใช่ NULL สำหรับอาร์กิวเมนต์ isCopy
ซึ่งมักไม่ค่อยมีประโยชน์
Release
การเรียกใช้จะใช้อาร์กิวเมนต์ mode
ที่มีค่าใดค่าหนึ่งใน 3 ค่าต่อไปนี้
การดำเนินการที่รันไทม์ดำเนินการจะขึ้นอยู่กับว่ารันไทม์แสดงผลพอยน์เตอร์ไปยังข้อมูลจริงหรือสำเนาของข้อมูลนั้นหรือไม่
0
- จริง: ระบบจะเลิกปักออบเจ็กต์อาร์เรย์
- คัดลอก: ระบบจะคัดลอกข้อมูลกลับ ระบบจะปล่อยบัฟเฟอร์ที่มีสำเนา
JNI_COMMIT
- การทำงานจริง: ไม่มีการดำเนินการใดๆ
- คัดลอก: ระบบจะคัดลอกข้อมูลกลับ บัฟเฟอร์ที่มีสำเนา จะไม่ได้รับการปล่อย
JNI_ABORT
- จริง: ระบบจะเลิกปักออบเจ็กต์อาร์เรย์ การเขียนก่อนหน้า จะไม่ถูกยกเลิก
- คัดลอก: ระบบจะปล่อยบัฟเฟอร์ที่มีสำเนา และการเปลี่ยนแปลงใดๆ ในบัฟเฟอร์จะหายไป
เหตุผลหนึ่งในการตรวจสอบแฟล็ก isCopy
คือการดูว่าคุณต้องเรียกใช้ Release
ด้วย JNI_COMMIT
หรือไม่หลังจากทำการเปลี่ยนแปลงอาร์เรย์ หากคุณสลับไปมาระหว่างการเปลี่ยนแปลงและการเรียกใช้โค้ดที่ใช้เนื้อหาของอาร์เรย์ คุณอาจข้ามการคอมมิตที่ไม่มีการดำเนินการได้ อีกเหตุผลหนึ่งที่ควรตรวจสอบการแจ้งคือเพื่อการจัดการ JNI_ABORT
อย่างมีประสิทธิภาพ เช่น คุณอาจต้องการ
รับอาร์เรย์ แก้ไขในตำแหน่งเดิม ส่งชิ้นส่วนไปยังฟังก์ชันอื่นๆ และ
ทิ้งการเปลี่ยนแปลง หากทราบว่า JNI กำลังทำสำเนาใหม่ให้คุณ ก็ไม่จำเป็นต้องสร้างสำเนาที่ "แก้ไขได้" อีก หาก JNI ส่ง
ต้นฉบับให้คุณ คุณจะต้องทำสำเนาของคุณเอง
ข้อผิดพลาดที่พบบ่อย (ซ้ำในโค้ดตัวอย่าง) คือการคิดว่าคุณข้ามการเรียกใช้ Release
ได้หาก
*isCopy
เป็นเท็จ ซึ่งแท้จริงแล้ว ไม่ได้เป็นเช่นนั้น หากไม่ได้จัดสรรบัฟเฟอร์สำเนา
ไว้ ระบบจะต้องปักหมุดหน่วยความจำเดิมและตัวเก็บขยะจะย้ายหน่วยความจำไม่ได้
นอกจากนี้ โปรดทราบว่าแฟล็ก JNI_COMMIT
จะไม่ปล่อยอาร์เรย์
และคุณจะต้องเรียกใช้ Release
อีกครั้งด้วยแฟล็กอื่น
ในที่สุด
การเรียกใช้ภูมิภาค
มีทางเลือกอื่นแทนการโทร เช่น Get<Type>ArrayElements
และ GetStringChars
ซึ่งอาจมีประโยชน์มากเมื่อคุณต้องการ
เพียงแค่คัดลอกข้อมูลเข้าหรือออก ลองพิจารณาสิ่งเหล่านี้
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
ซึ่งจะดึงอาร์เรย์ คัดลอกองค์ประกอบ len
ไบต์แรก
ออกจากอาร์เรย์ แล้วปล่อยอาร์เรย์ Get
จะปักหมุดหรือคัดลอกเนื้อหาอาร์เรย์
ทั้งนี้ขึ้นอยู่กับการ
ติดตั้งใช้งาน
โค้ดจะคัดลอกข้อมูล (อาจเป็นครั้งที่ 2) จากนั้นเรียกใช้ Release
ในกรณีนี้ JNI_ABORT
จะช่วยให้มั่นใจได้ว่าจะไม่มีการคัดลอกครั้งที่ 3
คุณสามารถทำสิ่งเดียวกันนี้ได้ง่ายขึ้นโดยทำดังนี้
env->GetByteArrayRegion(array, 0, len, buffer);
ซึ่งมีข้อดีหลายประการ ดังนี้
- ต้องใช้การเรียก JNI เพียงครั้งเดียวแทนที่จะเป็น 2 ครั้ง ซึ่งช่วยลดค่าใช้จ่าย
- ไม่จำเป็นต้องปักหมุดหรือทำสำเนาข้อมูลเพิ่มเติม
- ลดความเสี่ยงจากข้อผิดพลาดของโปรแกรมเมอร์ - ไม่มีความเสี่ยงที่จะลืม
เรียกใช้
Release
หลังจากที่บางอย่างล้มเหลว
ในทำนองเดียวกัน คุณสามารถใช้ Set<Type>ArrayRegion
call
เพื่อคัดลอกข้อมูลลงในอาร์เรย์ และ GetStringRegion
หรือ
GetStringUTFRegion
เพื่อคัดลอกอักขระออกจาก
String
ข้อยกเว้น
คุณต้องไม่เรียกใช้ฟังก์ชัน JNI ส่วนใหญ่ขณะที่กำลังรอดำเนินการข้อยกเว้น
โค้ดของคุณควรสังเกตข้อยกเว้น (ผ่านค่าที่ฟังก์ชันแสดงผล
ExceptionCheck
หรือ ExceptionOccurred
) และแสดงผล
หรือล้างข้อยกเว้นและจัดการ
ฟังก์ชัน JNI เดียวที่คุณได้รับอนุญาตให้เรียกใช้ขณะที่ข้อยกเว้น รอดำเนินการอยู่คือ
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
การเรียก JNI หลายรายการอาจทำให้เกิดข้อยกเว้น แต่ก็มักจะมีวิธีที่ง่ายกว่า
ในการตรวจสอบว่าการดำเนินการล้มเหลวหรือไม่ เช่น หาก NewString
แสดงผลค่าที่ไม่ใช่ NULL คุณก็ไม่จำเป็นต้องตรวจสอบข้อยกเว้น อย่างไรก็ตาม หากเรียกใช้เมธอด (โดยใช้ฟังก์ชันเช่น CallObjectMethod
) คุณต้องตรวจสอบข้อยกเว้นเสมอ เนื่องจากค่าที่ส่งคืนจะใช้ไม่ได้หากมีการส่งข้อยกเว้น
โปรดทราบว่าข้อยกเว้นที่โค้ดที่มีการจัดการส่งคืนจะไม่คลายเฟรมสแต็กเนทีฟ (และไม่ควรส่งข้อยกเว้น C++ ใน Android โดยทั่วไป และต้องไม่ส่งข้อยกเว้น
ข้ามขอบเขตการเปลี่ยน JNI จากโค้ด C++ ไปยังโค้ดที่มีการจัดการ)
คำสั่ง JNI Throw
และ ThrowNew
จะตั้งค่าพอยน์เตอร์ข้อยกเว้นในเธรดปัจจุบันเท่านั้น เมื่อกลับไปใช้โค้ดที่มีการจัดการจากโค้ดดั้งเดิม ระบบจะบันทึกข้อยกเว้นและจัดการอย่างเหมาะสม
โค้ดเนทีฟสามารถ "ดัก" ข้อยกเว้นได้โดยการเรียกใช้ ExceptionCheck
หรือ
ExceptionOccurred
และล้างข้อยกเว้นด้วย
ExceptionClear
เช่นเคย การทิ้งข้อยกเว้นโดยไม่จัดการอาจทำให้เกิดปัญหาได้
ไม่มีฟังก์ชันในตัวสำหรับการจัดการออบเจ็กต์ Throwable
เอง ดังนั้นหากต้องการ (เช่น) รับสตริงข้อยกเว้น คุณจะต้องค้นหาคลาส Throwable
ค้นหารหัสเมธอดสำหรับ getMessage "()Ljava/lang/String;"
เรียกใช้ และหากผลลัพธ์ไม่ใช่ NULL ให้ใช้ GetStringUTFChars
เพื่อรับสิ่งที่คุณส่งไปยัง printf(3)
หรือเทียบเท่าได้
การตรวจสอบเพิ่มเติม
JNI ไม่ค่อยตรวจสอบข้อผิดพลาด โดยปกติแล้วข้อผิดพลาดจะทำให้เกิดข้อขัดข้อง นอกจากนี้ Android ยังมีโหมดที่เรียกว่า CheckJNI ซึ่งจะสลับตัวชี้ตารางฟังก์ชัน JavaVM และ JNIEnv ไปยังตารางฟังก์ชันที่ทำการตรวจสอบเพิ่มเติมหลายรายการก่อนเรียกใช้การติดตั้งใช้งานมาตรฐาน
การตรวจสอบเพิ่มเติมมีดังนี้
- อาร์เรย์: พยายามจัดสรรอาร์เรย์ที่มีขนาดเป็นลบ
- พอยน์เตอร์ที่ไม่ถูกต้อง: การส่ง jarray/jclass/jobject/jstring ที่ไม่ถูกต้องไปยังการเรียก JNI หรือการส่งพอยน์เตอร์ NULL ไปยังการเรียก JNI ที่มีอาร์กิวเมนต์ที่ไม่ใช่ NULL
- ชื่อคลาส: ส่งสิ่งอื่นที่ไม่ใช่รูปแบบชื่อคลาส "java/lang/String" ไปยังการเรียก JNI
- การเรียกที่สำคัญ: การเรียก JNI ระหว่างการรับที่ "สำคัญ" กับการเผยแพร่ที่เกี่ยวข้อง
- Direct ByteBuffers: ส่งอาร์กิวเมนต์ที่ไม่ถูกต้องไปยัง
NewDirectByteBuffer
- ข้อยกเว้น: การโทร JNI ขณะที่รอดำเนินการข้อยกเว้น
- JNIEnv*s: การใช้ JNIEnv* จากเธรดที่ไม่ถูกต้อง
- jfieldIDs: การใช้ jfieldID ที่เป็น NULL หรือการใช้ jfieldID เพื่อตั้งค่าช่องเป็นค่าที่มีประเภทไม่ถูกต้อง (เช่น พยายามกำหนด StringBuilder ให้กับช่อง String) หรือการใช้ jfieldID สำหรับช่องแบบคงที่เพื่อตั้งค่าช่องอินสแตนซ์หรือในทางกลับกัน หรือการใช้ jfieldID จากคลาสหนึ่งกับอินสแตนซ์ของอีกคลาสหนึ่ง
- jmethodIDs: using the wrong kind of jmethodID when making a
Call*Method
JNI call: incorrect return type, static/non-static mismatch, wrong type for ‘this’ (for non-static calls) or wrong class (for static calls). - การอ้างอิง: การใช้
DeleteGlobalRef
/DeleteLocalRef
กับการอ้างอิงที่ไม่ถูกต้อง - โหมดการเผยแพร่: ส่งโหมดการเผยแพร่ที่ไม่ถูกต้องไปยังการเรียกการเผยแพร่ (อย่างอื่นที่ไม่ใช่
0
,JNI_ABORT
หรือJNI_COMMIT
) - ความปลอดภัยของประเภท: การส่งคืนประเภทที่ไม่เข้ากันจากเมธอดดั้งเดิม (เช่น การส่งคืน StringBuilder จากเมธอดที่ประกาศให้ส่งคืน String)
- UTF-8: การส่งลำดับไบต์ Modified UTF-8 ที่ไม่ถูกต้องไปยังการเรียก JNI
(ระบบยังไม่ได้ตรวจสอบการเข้าถึงเมธอดและฟิลด์: ข้อจำกัดในการเข้าถึงไม่มีผลกับโค้ดเนทีฟ)
คุณเปิดใช้ CheckJNI ได้หลายวิธี
หากคุณใช้โปรแกรมจำลอง CheckJNI จะเปิดอยู่โดยค่าเริ่มต้น
หากอุปกรณ์รูทแล้ว คุณสามารถใช้ลำดับคำสั่งต่อไปนี้เพื่อรีสตาร์ทรันไทม์โดยเปิดใช้ CheckJNI
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
ในกรณีใดก็ตาม คุณจะเห็นข้อความต่อไปนี้ในเอาต์พุต Logcat เมื่อรันไทม์เริ่มต้น
D AndroidRuntime: CheckJNI is ON
หากมีอุปกรณ์ปกติ คุณสามารถใช้คำสั่งต่อไปนี้
adb shell setprop debug.checkjni 1
การดำเนินการนี้จะไม่ส่งผลต่อแอปที่ทำงานอยู่แล้ว แต่แอปที่เปิดตัวจากจุดนั้นเป็นต้นไปจะเปิดใช้ CheckJNI (การเปลี่ยนพร็อพเพอร์ตี้เป็นค่าอื่นหรือการรีบูตจะปิดใช้ CheckJNI อีกครั้ง) ในกรณีนี้ คุณจะเห็นข้อความต่อไปนี้ในเอาต์พุต Logcat เมื่อแอปเริ่มทำงานในครั้งถัดไป
D Late-enabling CheckJNI
คุณยังตั้งค่าแอตทริบิวต์ android:debuggable
ในไฟล์ Manifest ของแอปพลิเคชันเพื่อ
เปิด CheckJNI สำหรับแอปของคุณเท่านั้นได้ด้วย โปรดทราบว่าเครื่องมือบิลด์ของ Android จะดำเนินการนี้โดยอัตโนมัติสำหรับ
บิลด์บางประเภท
ไลบรารีที่มาพร้อมเครื่อง
คุณโหลดโค้ดเนทีฟจากไลบรารีที่ใช้ร่วมกันได้ด้วย
System.loadLibrary
มาตรฐาน
ในทางปฏิบัติ Android เวอร์ชันเก่ามีข้อบกพร่องใน PackageManager ซึ่งทำให้การติดตั้งและการอัปเดตไลบรารีเนทีฟไม่น่าเชื่อถือ โปรเจ็กต์ ReLinker มีวิธีแก้ปัญหานี้และปัญหาอื่นๆ เกี่ยวกับการโหลดไลบรารีแบบเนทีฟ
เรียกใช้ System.loadLibrary
(หรือ ReLinker.loadLibrary
) จากตัวเริ่มต้นคลาสแบบคงที่ อาร์กิวเมนต์คือชื่อไลบรารีที่ "ไม่มีการตกแต่ง"
ดังนั้นหากต้องการโหลด libfubar.so
คุณจะต้องส่ง "fubar"
หากคุณมีคลาสที่มีเมธอดเนทีฟเพียงคลาสเดียว การเรียกใช้
System.loadLibrary
ควรอยู่ในตัวเริ่มต้นแบบคงที่ของคลาสนั้น มิเช่นนั้น คุณอาจต้องเรียกใช้จาก Application
เพื่อให้มั่นใจว่าระบบจะโหลดไลบรารีเสมอ
และโหลดตั้งแต่เนิ่นๆ
รันไทม์จะค้นหาเมธอดดั้งเดิมได้ 2 วิธี คุณจะลงทะเบียนอย่างชัดเจนกับ RegisterNatives
หรือจะให้รันไทม์ค้นหาแบบไดนามิกด้วย dlsym
ก็ได้ ข้อดีของ RegisterNatives
คือคุณจะได้รับการตรวจสอบล่วงหน้าว่ามีสัญลักษณ์อยู่จริง นอกจากนี้ คุณยังสามารถมีไลบรารีที่ใช้ร่วมกันซึ่งมีขนาดเล็กลงและเร็วขึ้นได้โดยไม่ต้องส่งออกสิ่งอื่นใดนอกจาก JNI_OnLoad
ข้อดีของการปล่อยให้รันไทม์ค้นหาฟังก์ชัน
คือคุณจะเขียนโค้ดน้อยลงเล็กน้อย
วิธีใช้ "RegisterNatives
"
- ระบุ
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
ฟังก์ชัน - ใน
JNI_OnLoad
ให้ลงทะเบียนวิธีการเนทีฟทั้งหมดโดยใช้RegisterNatives
- สร้างโดยใช้สคริปต์เวอร์ชัน (แนะนำ) หรือใช้
-fvisibility=hidden
เพื่อให้ระบบส่งออกเฉพาะJNI_OnLoad
จากคลัง ซึ่งจะทำให้โค้ดทำงานได้เร็วขึ้นและมีขนาดเล็กลง รวมถึงหลีกเลี่ยงการชนกันที่อาจเกิดขึ้นกับไลบรารีอื่นๆ ที่โหลดลงในแอป (แต่จะสร้าง Stack Trace ที่มีประโยชน์น้อยกว่าหากแอปขัดข้องในโค้ดแบบเนทีฟ)
ตัวเริ่มต้นแบบคงที่ควรมีลักษณะดังนี้
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
ฟังก์ชัน JNI_OnLoad
ควรมีลักษณะดังนี้หากเขียนใน C++
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. jclass c = env->FindClass("com/example/app/package/MyClass"); if (c == nullptr) return JNI_ERR; // Register your class' native methods. static const JNINativeMethod methods[] = { {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)}, {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)}, }; int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod)); if (rc != JNI_OK) return rc; return JNI_VERSION_1_6; }
หากต้องการใช้ "การค้นพบ" เมธอดเนทีฟแทน คุณต้องตั้งชื่อเมธอดในลักษณะที่เฉพาะเจาะจง (ดูรายละเอียดได้ที่ข้อกำหนด JNI) ซึ่งหมายความว่าหากลายเซ็นของเมธอดไม่ถูกต้อง คุณจะไม่ทราบจนกว่าจะมีการเรียกใช้เมธอดเป็นครั้งแรก
FindClass
การเรียกJNI_OnLoad
ใดๆ ที่ทำจาก JNI_OnLoad
จะแก้ไขคลาสใน
บริบทของตัวโหลดคลาสที่ใช้โหลดไลบรารีที่ใช้ร่วมกัน เมื่อเรียกใช้จากบริบทอื่นๆ FindClass
จะใช้โปรแกรมโหลดคลาสที่เชื่อมโยงกับเมธอดที่ด้านบนของสแต็ก Java หรือหากไม่มี (เนื่องจากมีการเรียกใช้จากเธรดดั้งเดิมที่เพิ่งแนบ) ก็จะใช้โปรแกรมโหลดคลาส "ระบบ" Class Loader ของระบบไม่รู้จักคลาสของแอปพลิเคชัน คุณจึงค้นหาคลาสของคุณเองด้วย FindClass
ในบริบทนั้นไม่ได้
ซึ่งทำให้ JNI_OnLoad
เป็นที่ที่สะดวกในการค้นหาและแคชคลาส เมื่อคุณมีjclass
การอ้างอิงส่วนกลางที่ถูกต้อง
คุณจะใช้การอ้างอิงนั้นจากเธรดที่แนบมาได้
การเรียกใช้เนทีฟที่เร็วขึ้นด้วย @FastNative
และ @CriticalNative
คุณใส่คำอธิบายประกอบในเมธอดเนทีฟได้ด้วย
@FastNative
หรือ
@CriticalNative
(แต่จะใช้ทั้ง 2 อย่างไม่ได้) เพื่อเร่งการเปลี่ยนผ่านระหว่างโค้ดที่จัดการและโค้ดเนทีฟ อย่างไรก็ตาม คำอธิบายประกอบเหล่านี้
มาพร้อมกับการเปลี่ยนแปลงลักษณะการทำงานบางอย่างที่ต้องพิจารณาอย่างรอบคอบก่อนใช้งาน แม้ว่าเราจะกล่าวถึงการเปลี่ยนแปลงเหล่านี้โดยย่อด้านล่าง แต่โปรดดูรายละเอียดในเอกสารประกอบ
@CriticalNative
ใช้ได้เฉพาะกับเมธอดดั้งเดิมที่ไม่ได้
ใช้ออบเจ็กต์ที่มีการจัดการ (ในพารามิเตอร์หรือค่าที่ส่งคืน หรือเป็น this
โดยนัย) และคำอธิบายประกอบนี้
จะเปลี่ยน ABI การเปลี่ยน JNI การติดตั้งใช้งานเนทีฟต้องยกเว้นพารามิเตอร์
JNIEnv
และjclass
ออกจากลายเซ็นฟังก์ชัน
ขณะเรียกใช้เมธอด @FastNative
หรือ @CriticalNative
การเก็บขยะ
จะไม่สามารถระงับเธรดสำหรับงานที่จำเป็นและอาจถูกบล็อกได้ อย่าใช้คำอธิบายประกอบเหล่านี้กับเมธอดที่ทำงานเป็นเวลานาน รวมถึงเมธอดที่มักจะทำงานอย่างรวดเร็วแต่โดยทั่วไปแล้วไม่มีขอบเขต
โดยเฉพาะอย่างยิ่ง โค้ดไม่ควรดําเนินการ I/O ที่สําคัญหรือรับการล็อกดั้งเดิมที่
อาจถือครองเป็นเวลานาน
คำอธิบายประกอบเหล่านี้ได้รับการติดตั้งใช้งานเพื่อใช้ในระบบตั้งแต่
Android 8
และกลายเป็น API สาธารณะที่ผ่านการทดสอบ CTS
ใน Android 14 การเพิ่มประสิทธิภาพเหล่านี้อาจใช้ได้กับอุปกรณ์ Android 8-13 ด้วย (แม้ว่าจะไม่มีการรับประกัน CTS ที่เข้มงวด) แต่การค้นหาแบบไดนามิกของเมธอดดั้งเดิมจะรองรับเฉพาะใน Android 12 ขึ้นไป และต้องมีการลงทะเบียนอย่างชัดเจนกับ JNI RegisterNatives
อย่างเคร่งครัดสำหรับการเรียกใช้ใน Android เวอร์ชัน 8-11 ระบบจะละเว้นคำอธิบายประกอบเหล่านี้ใน Android 7 ลงไป เนื่องจากความไม่ตรงกันของ ABI
สำหรับ @CriticalNative
จะทำให้การจัดรูปแบบอาร์กิวเมนต์ไม่ถูกต้องและอาจทำให้เกิดข้อขัดข้อง
สำหรับเมธอดที่สำคัญต่อประสิทธิภาพซึ่งจำเป็นต้องมีคำอธิบายประกอบเหล่านี้ เราขอแนะนำให้ลงทะเบียนเมธอดอย่างชัดเจนกับ JNI RegisterNatives
แทนที่จะอาศัย "การค้นหา" เมธอดเนทีฟตามชื่อ ขอแนะนำให้ใส่ผู้เรียกใช้เมธอด @FastNative
หรือ @CriticalNative
ไว้ในโปรไฟล์พื้นฐานเพื่อให้แอปเริ่มต้นทำงานได้อย่างมีประสิทธิภาพสูงสุด
ตั้งแต่ Android 12 เป็นต้นมา
การเรียกใช้@CriticalNative
เมธอดเนทีฟจากเมธอดที่มีการจัดการที่คอมไพล์แล้วนั้นแทบจะ
ไม่ต่างจากการเรียกใช้แบบไม่อินไลน์ใน C/C++ ตราบใดที่อาร์กิวเมนต์ทั้งหมดพอดีกับรีจิสเตอร์ (เช่น อาร์กิวเมนต์จำนวนเต็มสูงสุด 8 รายการและอาร์กิวเมนต์แบบจุดลอยสูงสุด 8 รายการใน arm64)
บางครั้งการแยกวิธีการดั้งเดิมออกเป็น 2 วิธีอาจเป็นทางเลือกที่ดีกว่า โดยวิธีหนึ่งเป็นวิธีที่รวดเร็วมากซึ่งอาจล้มเหลว และอีกวิธีหนึ่งจะจัดการกับกรณีที่ช้า เช่น
Kotlin
fun writeInt(nativeHandle: Long, value: Int) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value) } } @CriticalNative external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean external fun nativeWriteInt(nativeHandle: Long, value: Int)
Java
void writeInt(long nativeHandle, int value) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value); } } @CriticalNative static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value); static native void nativeWriteInt(long nativeHandle, int value);
ข้อควรพิจารณาเกี่ยวกับ 64 บิต
หากต้องการรองรับสถาปัตยกรรมที่ใช้พอยน์เตอร์ 64 บิต ให้ใช้ฟิลด์ long
แทน int
เมื่อจัดเก็บพอยน์เตอร์ไปยังโครงสร้างเนทีฟในฟิลด์ Java
ฟีเจอร์ที่ไม่รองรับ/ความเข้ากันได้กับรุ่นก่อนหน้า
ระบบรองรับฟีเจอร์ทั้งหมดของ JNI 1.6 ยกเว้นฟีเจอร์ต่อไปนี้
DefineClass
ไม่ได้ใช้ Android ไม่ได้ใช้ไบต์โค้ด Java หรือไฟล์คลาส ดังนั้นการส่งข้อมูลคลาสแบบไบนารีจึงใช้ไม่ได้
คุณอาจต้องทราบข้อมูลต่อไปนี้เพื่อความเข้ากันได้แบบย้อนหลังกับ Android เวอร์ชันเก่า
- การค้นหาฟังก์ชันเนทีฟแบบไดนามิก
จนถึง Android 2.0 (Eclair) ระบบไม่ได้แปลงอักขระ "$" เป็น "_00024" อย่างถูกต้อง ในระหว่างการค้นหาชื่อเมธอด การหลีกเลี่ยงข้อจำกัดนี้ต้องใช้การลงทะเบียนที่ชัดเจนหรือย้ายวิธีการเนทีฟออกจากคลาสภายใน
- แยกชุดข้อความ
จนถึง Android 2.0 (Eclair) คุณไม่สามารถใช้ฟังก์ชัน
pthread_key_create
Destructor เพื่อหลีกเลี่ยงการตรวจสอบ "ต้องแยกเธรดก่อน ออก" (รันไทม์ยังใช้ฟังก์ชันทำลายคีย์ pthread ด้วย ดังนั้นจึงเป็นการแข่งขันเพื่อดูว่าฟังก์ชันใดจะถูกเรียกใช้ก่อน) - การอ้างอิงทั่วโลกที่อ่อนแอ
จนถึง Android 2.2 (Froyo) ยังไม่มีการใช้การอ้างอิงส่วนกลางแบบอ่อน เวอร์ชันเก่าจะปฏิเสธการพยายามใช้เวอร์ชันเหล่านั้นอย่างแข็งขัน คุณใช้ค่าคงที่เวอร์ชันแพลตฟอร์ม Android เพื่อทดสอบการรองรับได้
จนถึง Android 4.0 (Ice Cream Sandwich) คุณจะส่งการอ้างอิงส่วนกลางแบบอ่อนได้เฉพาะ
NewLocalRef
,NewGlobalRef
และDeleteWeakGlobalRef
(ข้อกำหนดแนะนำอย่างยิ่งให้โปรแกรมเมอร์สร้างการอ้างอิงแบบฮาร์ดไปยังตัวแปรส่วนกลางแบบอ่อนก่อนที่จะดำเนินการใดๆ กับตัวแปรเหล่านั้น ดังนั้นข้อกำหนดนี้จึงไม่ควรจำกัดการทำงานเลย)ตั้งแต่ Android 4.0 (Ice Cream Sandwich) เป็นต้นไป คุณจะใช้การอ้างอิงแบบอ่อนทั่วโลกได้เหมือนกับการอ้างอิง JNI อื่นๆ
- การอ้างอิงในพื้นที่
จนถึง Android 4.0 (Ice Cream Sandwich) การอ้างอิงในเครื่องเป็น ตัวชี้โดยตรงจริงๆ Ice Cream Sandwich ได้เพิ่มการเปลี่ยนเส้นทาง ที่จำเป็นต่อการรองรับตัวเก็บขยะที่ดีขึ้น แต่ก็หมายความว่าข้อบกพร่อง JNI จำนวนมาก จะตรวจไม่พบในรุ่นเก่า ดูรายละเอียดเพิ่มเติมได้ที่ การเปลี่ยนแปลงการอ้างอิงในเครื่องของ JNI ใน ICS
ใน Android เวอร์ชันก่อน Android 8.0 จำนวนการอ้างอิงในเครื่องจะจำกัดไว้ที่ขีดจำกัดเฉพาะเวอร์ชัน ตั้งแต่ Android 8.0 เป็นต้นไป Android รองรับการอ้างอิงในเครื่องแบบไม่จำกัด
- Determining reference type with
GetObjectRefType
จนถึง Android 4.0 (Ice Cream Sandwich) การใช้ ตัวชี้โดยตรง (ดูด้านบน) ทำให้ไม่สามารถใช้งาน
GetObjectRefType
ได้อย่างถูกต้อง แต่เราใช้ฮิวริสติก ที่ดูผ่านตารางส่วนกลางที่อ่อนแอ อาร์กิวเมนต์ ตารางตัวแปรเฉพาะ และตารางส่วนกลางตามลำดับ ครั้งแรกที่พบตัวชี้โดยตรง ระบบจะรายงานว่าการอ้างอิงของคุณเป็นประเภทที่ ระบบกำลังตรวจสอบ ซึ่งหมายความว่า เช่น หาก คุณเรียกใช้GetObjectRefType
ใน jclass ทั่วโลกที่ เป็น jclass เดียวกันกับที่ส่งเป็นอาร์กิวเมนต์โดยนัยไปยังเมธอดเนทีฟแบบคงที่ คุณจะได้รับJNILocalRefType
แทนที่จะเป็นJNIGlobalRefType
@FastNative
และ@CriticalNative
ใน Android 7 ลงไป ระบบจะไม่สนใจคำอธิบายประกอบการเพิ่มประสิทธิภาพเหล่านี้ ABI ที่ไม่ตรงกันสำหรับ
@CriticalNative
จะทําให้เกิดการจัดรูปแบบอาร์กิวเมนต์ที่ไม่ถูกต้อง และอาจทําให้เกิดข้อขัดข้องการค้นหาแบบไดนามิกของฟังก์ชันเนทีฟสำหรับเมธอด
@FastNative
และ@CriticalNative
ยังไม่ได้ใช้งานใน Android 8-10 และ มีข้อบกพร่องที่ทราบใน Android 11 การใช้การเพิ่มประสิทธิภาพเหล่านี้โดยไม่มีการลงทะเบียนกับ JNI อย่างชัดเจนRegisterNatives
อาจทําให้เกิดข้อขัดข้องใน Android 8-11FindClass
การขว้างClassNotFoundException
Android จะส่ง
ClassNotFoundException
แทนNoClassDefFoundError
เพื่อให้เข้ากันได้กับเวอร์ชันก่อนหน้า เมื่อไม่พบคลาสโดยFindClass
ลักษณะการทำงานนี้สอดคล้องกับ Java Reflection APIClass.forName(name)
คำถามที่พบบ่อย: เหตุใดฉันจึงได้รับ UnsatisfiedLinkError
เมื่อทำงานกับโค้ดเนทีฟ คุณอาจเห็นข้อผิดพลาดเช่นนี้
java.lang.UnsatisfiedLinkError: Library foo not found
ในบางกรณี ข้อความนี้หมายความตามที่ระบุไว้ นั่นคือไม่พบไลบรารี ในกรณีอื่นๆ ไลบรารีมีอยู่แต่ dlopen(3)
เปิดไม่ได้ และ
รายละเอียดของความล้มเหลวจะอยู่ในข้อความรายละเอียดของข้อยกเว้น
สาเหตุที่พบบ่อยซึ่งอาจทำให้คุณพบข้อยกเว้น "ไม่พบไลบรารี" มีดังนี้
- ไม่มีไลบรารีหรือแอปเข้าถึงไลบรารีไม่ได้ ใช้
adb shell ls -l <path>
เพื่อตรวจสอบว่ามีไลบรารีอยู่หรือไม่ และตรวจสอบสิทธิ์ - ไม่ได้สร้างไลบรารีด้วย NDK ซึ่งอาจส่งผลให้เกิด การอ้างอิงฟังก์ชันหรือไลบรารีที่ไม่มีในอุปกรณ์
UnsatisfiedLinkError
ความล้มเหลวUnsatisfiedLinkError
อีกประเภทมีลักษณะดังนี้
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
ใน Logcat คุณจะเห็นข้อมูลต่อไปนี้
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
ซึ่งหมายความว่ารันไทม์พยายามค้นหาวิธีการที่ตรงกันแต่ไม่สำเร็จ สาเหตุที่พบบ่อยบางประการมีดังนี้
- ระบบไม่โหลดไลบรารี ตรวจสอบเอาต์พุต Logcat เพื่อดู ข้อความเกี่ยวกับการโหลดไลบรารี
- ไม่พบเมธอดเนื่องจากชื่อหรือลายเซ็นไม่ตรงกัน โดยทั่วไปปัญหานี้
เกิดจากสาเหตุต่อไปนี้
- สำหรับการค้นหาเมธอดแบบ Lazy การไม่ประกาศฟังก์ชัน C++
ด้วย
extern "C"
และการมองเห็นที่เหมาะสม (JNIEXPORT
) โปรดทราบว่าก่อน Ice Cream Sandwich มาโคร JNIEXPORT ไม่ถูกต้อง ดังนั้นการใช้ GCC ใหม่กับjni.h
เก่าจึงใช้ไม่ได้ คุณใช้arm-eabi-nm
เพื่อดูสัญลักษณ์ตามที่ปรากฏในไลบรารีได้ หากสัญลักษณ์ดู ผิดเพี้ยน (เช่น_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
แทนที่จะเป็นJava_Foo_myfunc
) หรือหากประเภทสัญลักษณ์เป็น ตัวพิมพ์เล็ก "t" แทนที่จะเป็นตัวพิมพ์ใหญ่ "T" คุณจะต้อง ปรับการประกาศ - สำหรับการลงทะเบียนที่ชัดเจน ข้อผิดพลาดเล็กน้อยเมื่อป้อน
ลายเซ็นของเมธอด ตรวจสอบว่าสิ่งที่คุณส่งไปยัง
การเรียกการลงทะเบียนตรงกับลายเซ็นในไฟล์บันทึก
โปรดทราบว่า "B" คือ
byte
และ "Z" คือboolean
คอมโพเนนต์ชื่อคลาสในลายเซ็นจะขึ้นต้นด้วย "L" ลงท้ายด้วย ";" ใช้ "/" เพื่อแยกชื่อแพ็กเกจ/คลาส และใช้ "$" เพื่อแยก ชื่อคลาสภายใน (Ljava/util/Map$Entry;
เช่น)
- สำหรับการค้นหาเมธอดแบบ Lazy การไม่ประกาศฟังก์ชัน C++
ด้วย
การใช้ javah
เพื่อสร้างส่วนหัว JNI โดยอัตโนมัติอาจช่วย
หลีกเลี่ยงปัญหาบางอย่างได้
คำถามที่พบบ่อย: เหตุใด FindClass
จึงไม่พบชั้นเรียนของฉัน
(คำแนะนำส่วนใหญ่นี้ใช้ได้ดีพอๆ กันกับการค้นหาวิธีการที่มี GetMethodID
หรือ GetStaticMethodID
หรือฟิลด์ที่มี GetFieldID
หรือ GetStaticFieldID
ไม่สำเร็จ)
ตรวจสอบว่าสตริงชื่อคลาสมีรูปแบบที่ถูกต้อง ชื่อคลาส JNI
จะขึ้นต้นด้วยชื่อแพ็กเกจและคั่นด้วยเครื่องหมายทับ
เช่น java/lang/String
หากคุณกำลังค้นหาคลาสอาร์เรย์
คุณต้องเริ่มต้นด้วยวงเล็บเหลี่ยมจำนวนที่เหมาะสมและ
ต้องครอบคลาสด้วย "L" และ ";" ด้วย ดังนั้นอาร์เรย์หนึ่งมิติของ
String
จะเป็น [Ljava/lang/String;
หากคุณค้นหาคลาสภายใน ให้ใช้ "$" แทน "." โดยทั่วไปแล้ว
การใช้ javap
ในไฟล์ .class เป็นวิธีที่ดีในการค้นหา
ชื่อภายในของคลาส
หากเปิดใช้การย่อโค้ด โปรดตรวจสอบว่าคุณได้ กำหนดค่าโค้ดที่จะเก็บไว้ การกำหนดค่ากฎ Keep ที่เหมาะสมเป็นสิ่งสำคัญเนื่องจากเครื่องมือลดขนาดโค้ดอาจนำคลาส เมธอด หรือฟิลด์ที่ใช้จาก JNI เท่านั้นออก
หากชื่อชั้นเรียนถูกต้อง คุณอาจพบปัญหาเกี่ยวกับตัวโหลดคลาส
FindClass
ต้องการเริ่มการค้นหาคลาสใน
เครื่องมือโหลดคลาสที่เชื่อมโยงกับโค้ดของคุณ โดยจะตรวจสอบสแต็กการเรียก
ซึ่งจะมีลักษณะดังนี้
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
วิธีที่อยู่ด้านบนสุดคือ Foo.myfunc
FindClass
จะค้นหาออบเจ็กต์ ClassLoader
ที่เชื่อมโยงกับคลาส Foo
และใช้ออบเจ็กต์นั้น
ซึ่งโดยปกติแล้วจะทำในสิ่งที่คุณต้องการ คุณอาจพบปัญหาหาก
สร้างเธรดด้วยตนเอง (อาจโดยการเรียกใช้ pthread_create
แล้วแนบด้วย AttachCurrentThread
) ตอนนี้ไม่มีเฟรมสแต็กจากแอปพลิเคชันของคุณแล้ว
หากคุณเรียกใช้ FindClass
จากเธรดนี้ JavaVM จะเริ่มต้นใน Class Loader ของ "system" แทนที่จะเป็น Class Loader ที่เชื่อมโยงกับแอปพลิเคชันของคุณ ดังนั้นการพยายามค้นหาคลาสเฉพาะของแอปจะล้มเหลว
คุณแก้ไขปัญหานี้ได้ด้วยวิธีต่อไปนี้
- ทำการค้นหา
FindClass
เพียงครั้งเดียวในJNI_OnLoad
และแคชการอ้างอิงคลาสเพื่อใช้ในภายหลังFindClass
การเรียกที่ทำเป็นส่วนหนึ่งของการดำเนินการJNI_OnLoad
จะใช้ตัวโหลดคลาสที่เชื่อมโยงกับ ฟังก์ชันที่เรียกSystem.loadLibrary
(นี่เป็น กฎพิเศษที่จัดไว้เพื่อให้การเริ่มต้นไลบรารีสะดวกยิ่งขึ้น) หากโค้ดแอปโหลดไลบรารีFindClass
จะใช้ตัวโหลดคลาสที่ถูกต้อง - ส่งอินสแตนซ์ของคลาสไปยังฟังก์ชันที่ต้องการ
โดยประกาศเมธอดเนทีฟให้รับอาร์กิวเมนต์ Class แล้ว
ส่ง
Foo.class
เข้าไป - แคชการอ้างอิงไปยังออบเจ็กต์
ClassLoader
ไว้ที่ใดที่หนึ่ง ที่สะดวก และออกคำสั่งเรียกloadClass
โดยตรง ซึ่งต้องใช้ ความพยายาม
คำถามที่พบบ่อย: ฉันจะแชร์ข้อมูลดิบกับโค้ดดั้งเดิมได้อย่างไร
คุณอาจพบว่าตัวเองอยู่ในสถานการณ์ที่ต้องเข้าถึงบัฟเฟอร์ข้อมูลดิบขนาดใหญ่จากทั้งโค้ดที่มีการจัดการและโค้ดดั้งเดิม ตัวอย่างที่พบบ่อย ได้แก่ การดัดแปลงบิตแมปหรือตัวอย่างเสียง มีแนวทางพื้นฐาน 2 แนวทาง
คุณสามารถจัดเก็บข้อมูลใน byte[]
ได้ ซึ่งช่วยให้เข้าถึงจากโค้ดที่มีการจัดการได้อย่างรวดเร็ว
อย่างไรก็ตาม ในฝั่งเนทีฟ คุณ
ไม่รับประกันว่าจะเข้าถึงข้อมูลได้โดยไม่ต้องคัดลอก ในการใช้งานบางอย่าง GetByteArrayElements
และ
GetPrimitiveArrayCritical
จะแสดงพอยน์เตอร์จริงไปยัง
ข้อมูลดิบในฮีปที่มีการจัดการ แต่ในการใช้งานอื่นๆ จะจัดสรรบัฟเฟอร์
ในฮีปดั้งเดิมและคัดลอกข้อมูล
อีกทางเลือกหนึ่งคือการจัดเก็บข้อมูลในบัฟเฟอร์ไบต์โดยตรง โดยสร้างได้ด้วย java.nio.ByteBuffer.allocateDirect
หรือฟังก์ชัน NewDirectByteBuffer
ของ JNI ต่างจากบัฟเฟอร์ไบต์ปกติที่ไม่ได้จัดสรรพื้นที่เก็บข้อมูลในฮีปที่มีการจัดการ และเข้าถึงได้โดยตรงจากโค้ดเนทีฟเสมอ (รับที่อยู่ด้วย GetDirectBufferAddress
) การเข้าถึงข้อมูลจากโค้ดที่มีการจัดการอาจช้ามาก ทั้งนี้ขึ้นอยู่กับวิธีที่ใช้ในการเข้าถึงบัฟเฟอร์ไบต์โดยตรง
การเลือกใช้ขึ้นอยู่กับ 2 ปัจจัยต่อไปนี้
- การเข้าถึงข้อมูลส่วนใหญ่จะมาจากโค้ดที่เขียนใน Java หรือใน C/C++ ไหม
- หากในที่สุดข้อมูลจะถูกส่งไปยัง API ของระบบ ข้อมูลนั้นจะต้องอยู่ในรูปแบบใด (ตัวอย่างเช่น หากในที่สุดข้อมูลจะส่งไปยังฟังก์ชันที่รับ byte[] การประมวลผลใน
ByteBuffer
โดยตรงอาจไม่เหมาะสม)
หากไม่มีผู้ชนะที่ชัดเจน ให้ใช้ Direct Byte Buffer การรองรับสำหรับฟีเจอร์เหล่านี้ สร้างขึ้นโดยตรงใน JNI และประสิทธิภาพควรจะดีขึ้นในรุ่นที่จะออกในอนาคต