JNI là Giao diện gốc Java. Mã này xác định cách mã byte mà Android biên dịch mã được quản lý (được viết bằng ngôn ngữ lập trình Java hoặc Kotlin) để tương tác với mã gốc (được viết bằng C/C++). JNI trung lập với nhà cung cấp, hỗ trợ tải mã từ tính năng chia sẻ động thư viện, mặc dù rườm rà, đôi khi lại hiệu quả một cách hợp lý.
Lưu ý: Vì Android biên dịch Kotlin thành mã byte thân thiện với ART trong theo cách tương tự như ngôn ngữ lập trình Java, bạn có thể áp dụng hướng dẫn trên trang này cho cả ngôn ngữ lập trình Kotlin và Java về cấu trúc JNI và các chi phí liên quan. Để tìm hiểu thêm, hãy xem Kotlin và Android.
Nếu bạn chưa quen với ngôn ngữ này, hãy đọc qua Thông số kỹ thuật của giao diện gốc Java để nắm được cách hoạt động của JNI và những tính năng hiện có. Hơi nhiều các khía cạnh của giao diện sẽ không rõ ràng ngay trên buổi đọc đầu tiên, nhờ vậy bạn có thể thấy một số phần tiếp theo hữu ích.
Để duyệt qua các lượt tham chiếu JNI toàn cục, cũng như xem vị trí tạo và xoá các lượt tham chiếu JNI toàn cục, hãy sử dụng chế độ xem JNI heap (vùng nhớ khối xếp JNI) trong Memory Profiler (Trình phân tích bộ nhớ) trong Android Studio 3.2 trở lên.
Mẹo chung
Cố gắng giảm thiểu dấu vết của lớp JNI. Có một vài phương diện cần xem xét ở đây. Giải pháp JNI của bạn cần tuân thủ các nguyên tắc sau (được liệt kê dưới đây theo thứ tự mức độ quan trọng, bắt đầu bằng phần quan trọng nhất):
- Giảm thiểu việc tổng hợp tài nguyên trên lớp JNI. Tổng hợp lại lớp JNI có chi phí không nhỏ. Hãy cố gắng thiết kế một giao diện giúp giảm thiểu số lượng dữ liệu bạn cần tổng hợp cũng như tần suất mà bạn phải sắp xếp dữ liệu.
- Tránh giao tiếp không đồng bộ giữa mã được viết bằng chương trình được quản lý ngôn ngữ và mã được viết bằng C++ nếu có thể. Việc này sẽ giúp giao diện JNI dễ duy trì hơn. Thường thì bạn có thể đơn giản hoá tính không đồng bộ Cập nhật giao diện người dùng bằng cách duy trì bản cập nhật không đồng bộ cùng ngôn ngữ với giao diện người dùng. Ví dụ: thay vì gọi hàm C++ từ luồng giao diện người dùng trong mã Java qua JNI, sẽ tốt hơn để thực hiện lệnh gọi lại giữa hai luồng trong ngôn ngữ lập trình Java, trong đó có một luồng thực hiện lệnh gọi chặn C++ rồi thông báo luồng giao diện người dùng khi lệnh gọi chặn là đã hoàn tất.
- Giảm thiểu số lượng luồng mà JNI cần chạm hoặc chạm vào. Nếu bạn cần sử dụng nhóm luồng bằng cả ngôn ngữ Java và C++, hãy cố gắng giữ lại JNI hoạt động giao tiếp giữa các chủ sở hữu nhóm thay vì giữa các luồng worker riêng lẻ.
- Giữ cho mã giao diện của bạn có ít nguồn C++ và Java dễ xác định để hỗ trợ việc tái cấu trúc trong tương lai. Cân nhắc sử dụng tính năng tự động tạo JNI khi phù hợp.
JavaVM và JNIEnv
JNI xác định hai cấu trúc dữ liệu chính là "JavaVM" và "JNIEnv". Cả hai đều về cơ bản con trỏ trỏ đến bảng hàm. (Trong phiên bản C++, chúng là các lớp có con trỏ đến bảng hàm và hàm thành phần cho mỗi hàm JNI mà gián tiếp thông qua bảng.) JavaVM cung cấp "giao diện gọi" hàm, cho phép bạn tạo và huỷ bỏ JavaVM. Về lý thuyết, bạn có thể có nhiều JavaVM cho mỗi quy trình, nhưng Android chỉ cho phép 1.
JNIEnv cung cấp hầu hết các hàm JNI. Các hàm gốc của bạn đều nhận được một JNIEnv dưới dạng
đối số đầu tiên, ngoại trừ các phương thức @CriticalNative
,
xem cuộc gọi gốc nhanh hơn.
JNIEnv được dùng để lưu trữ cục bộ luồng. Do đó, bạn không thể chia sẻ JNIEnv giữa các luồng.
Nếu một đoạn mã không có cách nào khác để lấy JNIEnv, bạn nên chia sẻ
JavaVM và sử dụng GetEnv
để khám phá JNIEnv của luồng. (Giả sử có một tệp; xem AttachCurrentThread
bên dưới.)
Phần khai báo C của JNIEnv và JavaVM khác với C++
nội dung khai báo. Tệp " include" (bao gồm) "jni.h"
cung cấp các định dạng typedef khác nhau
tuỳ thuộc vào việc mã đó được đưa vào C hay C++. Vì lý do này, bạn không nên
đưa các đối số JNIEnv vào tệp tiêu đề có trong cả hai ngôn ngữ. (Nói cách khác: nếu
tệp tiêu đề yêu cầu #ifdef __cplusplus
, bạn có thể phải thực hiện thêm một số thao tác nếu bất kỳ nội dung nào trong
tiêu đề đó đề cập đến JNIEnv.)
Luồng
Tất cả các luồng đều là luồng Linux, do nhân hệ điều hành lên lịch. Thường
bắt đầu từ mã được quản lý (sử dụng Thread.start()
),
nhưng cũng có thể tạo các tệp này ở nơi khác rồi đính kèm vào JavaVM
. Cho
ví dụ: một chuỗi bắt đầu bằng pthread_create()
hoặc std::thread
có thể được đính kèm bằng AttachCurrentThread()
hoặc
Các hàm AttachCurrentThreadAsDaemon()
. Cho đến khi một chuỗi là
đính kèm, tệp này không có JNIEnv và không thể thực hiện lệnh gọi JNI.
Thông thường, tốt nhất bạn nên sử dụng Thread.start()
để tạo bất cứ luồng nào cần
vào mã Java. Làm như vậy sẽ đảm bảo rằng bạn có đủ không gian ngăn xếp để bạn
ở đúng ThreadGroup
và bạn đang sử dụng cùng một ClassLoader
làm mã Java của bạn. Việc đặt tên của luồng để gỡ lỗi trong Java cũng sẽ dễ dàng hơn so với từ
mã gốc (xem pthread_setname_np()
nếu bạn có pthread_t
hoặc
thread_t
và std::thread::native_handle()
nếu bạn có
std::thread
và muốn có pthread_t
).
Việc đính kèm một luồng được tạo nguyên gốc sẽ gây ra java.lang.Thread
đối tượng được tạo và thêm vào phần "chính" ThreadGroup
,
cho trình gỡ lỗi thấy được thông tin đó. Đang gọi cho AttachCurrentThread()
trên chuỗi đã được đính kèm là không hoạt động.
Android không tạm ngưng các luồng thực thi mã gốc. Nếu đang thu thập rác hoặc trình gỡ lỗi đã đưa ra lệnh tạm ngưng thì Android sẽ tạm dừng luồng vào lần tiếp theo thực hiện lệnh gọi JNI.
Các luồng được đính kèm thông qua JNI phải gọi
DetachCurrentThread()
trước khi họ thoát.
Nếu việc lập trình trực tiếp này thật khó khăn, thì trong Android 2.0 (Eclair) trở lên, bạn
có thể sử dụng pthread_key_create()
để xác định hàm khởi tạo
hàm sẽ được gọi trước khi luồng thoát, và
gọi DetachCurrentThread()
từ đó. (Sử dụng
khoá bằng pthread_setspecific()
để lưu trữ JNIEnv trong
thread-local-storage; bằng cách đó, nó sẽ được chuyển vào hàm khởi tạo của bạn dưới dạng
đối số.)
jclass, jmethodID và jfieldID
Nếu muốn truy cập vào trường của một đối tượng từ mã gốc, bạn làm như sau:
- Lấy thông tin tham chiếu đối tượng lớp cho lớp bằng
FindClass
- Lấy mã trường cho trường bằng
GetFieldID
- Lấy nội dung của trường bằng nội dung phù hợp, chẳng hạn như
GetIntField
Tương tự, để gọi một phương thức, trước tiên, bạn sẽ nhận tham chiếu đối tượng lớp rồi mới đến mã nhận dạng phương thức. Các mã này thường chỉ con trỏ đến cấu trúc dữ liệu thời gian chạy nội bộ. Việc tra cứu chúng có thể cần vài chuỗi các phép so sánh, nhưng sau khi bạn có lệnh gọi thực tế để lấy trường hoặc gọi phương thức rất nhanh.
Nếu hiệu suất quan trọng, bạn nên tra cứu các giá trị một lần và lưu kết quả vào bộ nhớ đệm trong mã gốc của bạn. Vì có giới hạn một JavaVM cho mỗi quy trình nên điều này là hợp lý để lưu trữ dữ liệu này trong cấu trúc cục bộ tĩnh.
Mã tham chiếu lớp, mã trường và mã phương thức đều được đảm bảo hợp lệ cho đến khi lớp được huỷ tải. Hạng
chỉ được huỷ tải nếu tất cả các lớp liên kết với một ClassLoader có thể được thu thập rác,
điều này hiếm khi xảy ra nhưng không phải là không thể thực hiện được trong Android. Tuy nhiên, xin lưu ý rằng
jclass
là tham chiếu lớp và phải được bảo vệ bằng lệnh gọi
vào NewGlobalRef
(xem phần tiếp theo).
Nếu bạn muốn lưu mã vào bộ nhớ đệm khi một lớp được tải và tự động lưu lại bộ nhớ đệm vào bộ nhớ đệm nếu lớp đã được huỷ tải và tải lại, thì đây là cách khởi chạy chính xác mã nhận dạng là thêm một đoạn mã có dạng như sau vào lớp thích hợp:
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(); }
Tạo một phương thức nativeClassInit
trong mã C/C++ để thực hiện tra cứu mã nhận dạng. Đoạn mã
sẽ được thực thi một lần khi lớp này được khởi tạo. Nếu lớp đã từng bị huỷ tải và
sau đó tải lại thì lệnh này mới được thực thi lại.
Tham chiếu cục bộ và toàn cầu
Mọi đối số được truyền đến một phương thức gốc và hầu hết mọi đối tượng được trả về bởi hàm JNI là "tham chiếu cục bộ". Điều này có nghĩa là URL hợp lệ cho thời lượng của phương thức gốc hiện tại trong luồng hiện tại. Ngay cả khi đối tượng đó tiếp tục tồn tại sau phương thức gốc thì tham chiếu không hợp lệ.
Điều này áp dụng cho tất cả các lớp con của jobject
, bao gồm
jclass
, jstring
và jarray
.
(Thời gian chạy sẽ cảnh báo bạn về hầu hết các trường hợp sử dụng sai tệp đối chiếu khi JNI mở rộng
bật tính năng kiểm tra.)
Cách duy nhất để lấy thông tin tham chiếu không cục bộ là thông qua các hàm
NewGlobalRef
và NewWeakGlobalRef
.
Nếu muốn giữ lại tệp đối chiếu trong một khoảng thời gian dài hơn, bạn phải sử dụng
"toàn cầu" tham chiếu. Hàm NewGlobalRef
sẽ nhận giá trị
tham chiếu cục bộ làm đối số và trả về một tham chiếu toàn cục.
Tham chiếu chung được đảm bảo hợp lệ cho đến khi bạn gọi
DeleteGlobalRef
.
Mẫu này thường được dùng khi lưu một jclass vào bộ nhớ đệm
từ FindClass
, ví dụ:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Tất cả phương thức JNI đều chấp nhận cả tham chiếu cục bộ và tham chiếu toàn cục làm đối số.
Các tham chiếu đến cùng một đối tượng có thể có các giá trị khác nhau.
Ví dụ: giá trị trả về từ các lệnh gọi liên tiếp đến
NewGlobalRef
trên cùng một đối tượng có thể khác nhau.
Để xem liệu hai tham chiếu có tham chiếu đến cùng một đối tượng hay không,
bạn phải dùng hàm IsSameObject
. Không bao giờ so sánh
tham chiếu với ==
trong mã gốc.
Một hệ quả của việc này là bạn
không được giả định các tham chiếu đối tượng là không đổi hoặc duy nhất
trong mã gốc. Giá trị biểu thị một đối tượng có thể khác nhau
từ một lời gọi phương thức sang lời gọi phương thức tiếp theo và có thể là hai
các đối tượng khác nhau có thể có cùng giá trị trong các lệnh gọi liên tiếp. Không sử dụng
jobject
làm khoá.
Lập trình viên phải "không phân bổ quá mức" thông tin tham khảo cục bộ. Trong thực tế, điều này có nghĩa là
nếu bạn đang tạo một số lượng lớn các tham chiếu cục bộ, có thể là trong khi chạy qua một mảng
bạn nên giải phóng chúng theo cách thủ công bằng
DeleteLocalRef
thay vì để JNI làm việc này cho bạn. Chiến lược phát hành đĩa đơn
việc triển khai chỉ bắt buộc để đặt trước chỗ cho
16 tệp tham chiếu cục bộ, vì vậy nếu cần nhiều hơn số lượng đó, bạn nên xoá khi bạn sử dụng hoặc sử dụng
EnsureLocalCapacity
/PushLocalFrame
để đặt trước thêm.
Lưu ý rằng jfieldID
và jmethodID
mờ
chứ không phải là tham chiếu đối tượng và không được truyền đến
NewGlobalRef
. Dữ liệu thô
con trỏ do các hàm như GetStringUTFChars
trả về
và GetByteArrayElements
cũng không phải là đối tượng. (Các bước này có thể được thông qua
giữa các chuỗi và có hiệu lực cho đến khi có lệnh gọi Phát hành trùng khớp.)
Một trường hợp bất thường đáng được đề cập riêng. Nếu bạn đính kèm một tệp gốc
chuỗi có AttachCurrentThread
, mã bạn đang chạy sẽ
không bao giờ tự động giải phóng tệp tham chiếu cục bộ cho đến khi chuỗi tách ra. Bất kỳ quảng cáo địa phương nào
các tệp đối chiếu bạn tạo sẽ phải được xoá theo cách thủ công. Nói chung, bất kỳ quảng cáo gốc nào
mã tạo tham chiếu cục bộ trong một vòng lặp có thể cần phải thực hiện một số thao tác thủ công
xóa.
Hãy cẩn thận khi sử dụng tệp tham chiếu toàn cục. Tham chiếu toàn cầu có thể không tránh khỏi, nhưng chúng rất khó để gỡ lỗi và có thể gây ra các hành vi khó chẩn đoán (sai) cho bộ nhớ. Mọi yếu tố khác đều bằng nhau, a với ít tệp tham chiếu toàn cục hơn có lẽ sẽ tốt hơn.
Chuỗi UTF-8 và UTF-16
Ngôn ngữ lập trình Java sử dụng UTF-16. Để thuận tiện, JNI cung cấp các phương thức hoạt động với Bạn cũng sửa đổi UTF-8. Chiến lược phát hành đĩa đơn mã hóa được sửa đổi rất hữu ích cho mã C vì nó mã hóa \u0000 là 0xc0 0x80 thay vì 0x00. Điều tuyệt vời ở đây là bạn có thể dựa vào việc có các chuỗi kết thúc bằng 0 theo kiểu C, thích hợp để sử dụng với hàm chuỗi libc chuẩn. Nhược điểm là bạn không thể chuyển dữ liệu UTF-8 tuỳ ý đến JNI và kỳ vọng mã này hoạt động chính xác.
Để xem giá trị đại diện UTF-16 của String
, hãy sử dụng GetStringChars
.
Xin lưu ý rằng các chuỗi UTF-16 không kết thúc bằng 0 và được phép sử dụng \u0000,
vì vậy, bạn cần tiếp tục dựa trên độ dài chuỗi cũng như con trỏ jchar.
Đừng quên Release
các chuỗi bạn Get
. Chiến lược phát hành đĩa đơn
các hàm chuỗi trả về jchar*
hoặc jbyte*
là con trỏ kiểu C đến dữ liệu gốc thay vì tham chiếu cục bộ. Chúng
đều được đảm bảo hợp lệ cho đến khi Release
được gọi, có nghĩa là chúng sẽ không
bị loại bỏ khi phương thức gốc trả về.
Dữ liệu được chuyển đến NewStringUTF phải ở định dạng UTF-8 đã sửa đổi. Đáp
lỗi thường gặp là đọc dữ liệu ký tự trong một tệp hoặc luồng mạng
rồi đưa cho NewStringUTF
mà không lọc.
Trừ phi bạn biết dữ liệu là MUTF-8 hợp lệ (hoặc 7 bit ASCII, là tập hợp con tương thích),
bạn cần loại bỏ các ký tự không hợp lệ hoặc chuyển đổi các ký tự đó thành biểu mẫu UTF-8 đã sửa đổi.
Nếu không, lượt chuyển đổi UTF-16 có thể mang lại kết quả không mong muốn.
CheckJNI (được bật theo mặc định cho trình mô phỏng) quét chuỗi
và huỷ máy ảo nếu nhận được dữ liệu đầu vào không hợp lệ.
Trước Android 8, hoạt động sử dụng chuỗi UTF-16 thường nhanh hơn dưới dạng Android
không yêu cầu bản sao trong GetStringChars
, trong khi đó
GetStringUTFChars
yêu cầu phân bổ và chuyển đổi thành UTF-8.
Android 8 thay đổi cách biểu diễn String
để sử dụng 8 bit cho mỗi ký tự
cho chuỗi ASCII (để tiết kiệm bộ nhớ) và bắt đầu sử dụng
di chuyển
trình thu gom rác. Những tính năng này làm giảm đáng kể số lượng trường hợp ART
có thể cung cấp con trỏ đến dữ liệu String
mà không cần tạo bản sao, thậm chí
trong GetStringCritical
. Tuy nhiên, nếu hầu hết các chuỗi được mã này xử lý
ngắn gọn, có thể tránh được việc phân bổ và sắp xếp trong hầu hết các trường hợp bằng cách
sử dụng vùng đệm phân bổ ngăn xếp và GetStringRegion
hoặc
GetStringUTFRegion
Ví dụ:
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);
Mảng gốc
JNI cung cấp các hàm để truy cập vào nội dung của đối tượng mảng. Mặc dù các mảng đối tượng phải được truy cập vào từng mục nhập tại một thời điểm, nhưng các mảng dữ liệu nguyên gốc có thể được đọc và ghi trực tiếp như thể chúng được khai báo trong C.
Để làm cho giao diện hiệu quả nhất có thể mà không hạn chế
cách triển khai máy ảo, Get<PrimitiveType>ArrayElements
nhóm lệnh gọi cho phép môi trường thời gian chạy trả về một con trỏ đến các phần tử thực tế, hoặc
phân bổ một số bộ nhớ và tạo một bản sao. Dù bằng cách nào, con trỏ thô cũng đều được trả về
được đảm bảo là hợp lệ cho đến khi lệnh gọi Release
tương ứng
được phát hành (có nghĩa là nếu dữ liệu không được sao chép, đối tượng mảng
sẽ được ghim và không thể được di chuyển trong quá trình nén vùng nhớ khối xếp).
Bạn phải Release
mọi mảng mà bạn Get
. Ngoài ra, nếu Get
không gọi được, bạn phải đảm bảo rằng mã của bạn không cố Release
một giá trị NULL
con trỏ sau đó.
Bạn có thể xác định xem dữ liệu đã được sao chép hay chưa bằng cách chuyển một giá trị
con trỏ không NULL cho đối số isCopy
. Trường hợp này hiếm khi
hữu ích.
Lệnh gọi Release
nhận một đối số mode
có thể
có một trong ba giá trị. Các thao tác do thời gian chạy thực hiện phụ thuộc vào
liệu nó có trả về một con trỏ đến dữ liệu thực tế hoặc một bản sao của dữ liệu đó hay không:
0
- Thực tế: đối tượng mảng chưa được ghim.
- Sao chép: dữ liệu sẽ được sao chép trở lại. Vùng đệm có bản sao được giải phóng.
JNI_COMMIT
- Thực tế: không làm gì cả.
- Sao chép: dữ liệu sẽ được sao chép trở lại. Vùng đệm có bản sao không được giải phóng.
JNI_ABORT
- Thực tế: đối tượng mảng chưa được ghim. Trước đó không bị huỷ bỏ.
- Sao chép: vùng đệm có bản sao được giải phóng; mọi thay đổi đối với tệp đó đều sẽ bị mất.
Bạn nên kiểm tra cờ isCopy
để biết liệu
bạn cần gọi Release
bằng JNI_COMMIT
sau khi thay đổi một mảng – nếu bạn xen kẽ giữa việc thực hiện
các thay đổi và thực thi mã có sử dụng nội dung của mảng, bạn có thể
có thể
bỏ qua cam kết không hoạt động. Một lý do khác có thể để kiểm tra cờ là
xử lý hiệu quả JNI_ABORT
. Ví dụ: bạn có thể muốn
để lấy một mảng, sửa đổi mảng đó tại chỗ, truyền các phần đến các hàm khác và
sau đó loại bỏ các thay đổi đó. Nếu bạn biết rằng JNI đang tạo một bản sao mới cho
bạn không cần tạo một sao chép. Nếu JNI đang truyền
cho bạn bản gốc, thì bạn cần tạo bản sao của riêng mình.
Một sai lầm thường gặp (lặp lại trong mã ví dụ) cho rằng bạn có thể bỏ qua lệnh gọi Release
nếu
*isCopy
là false. Tuy nhiên, trường hợp này không đúng. Nếu không có vùng đệm sao chép nào
thì bộ nhớ ban đầu phải được ghim và không thể di chuyển bằng
bộ thu gom rác.
Ngoài ra, xin lưu ý rằng cờ JNI_COMMIT
không giải phóng mảng,
và bạn sẽ phải gọi lại Release
bằng một cờ khác
cuối cùng.
Cuộc gọi trong vùng
Có một phương án thay thế cho các lệnh gọi như Get<Type>ArrayElements
và GetStringChars
có thể rất hữu ích khi bạn muốn
cần sao chép dữ liệu vào hoặc ra. Hãy cân nhắc thực hiện những bước sau:
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
Thao tác này sẽ lấy mảng, sao chép len
byte đầu tiên
ra khỏi mảng, sau đó giải phóng mảng. Tuỳ thuộc vào
thì lệnh gọi Get
sẽ ghim hoặc sao chép mảng
.
Mã này sẽ sao chép dữ liệu (có thể là lần thứ hai), sau đó gọi Release
; trong trường hợp này
JNI_ABORT
đảm bảo không có cơ hội tạo bản sao thứ ba.
Người dùng có thể thực hiện cùng một việc theo cách đơn giản hơn:
env->GetByteArrayRegion(array, 0, len, buffer);
Việc này có một số ưu điểm:
- Cần có một lệnh gọi JNI thay vì 2 để giảm mức hao tổn.
- Không cần ghim hay sao chép thêm dữ liệu.
- Giảm nguy cơ xảy ra lỗi của lập trình viên — không có nguy cơ bị quên
để gọi
Release
sau khi không thành công.
Tương tự, bạn có thể sử dụng lệnh gọi Set<Type>ArrayRegion
để sao chép dữ liệu vào một mảng và GetStringRegion
hoặc
GetStringUTFRegion
để sao chép các ký tự từ
String
.
Ngoại lệ
Bạn không được gọi hầu hết các hàm JNI khi đang chờ xử lý một ngoại lệ.
Mã của bạn dự kiến sẽ nhận thấy ngoại lệ (thông qua giá trị trả về của hàm,
ExceptionCheck
hoặc ExceptionOccurred
) và trả về,
hoặc xoá ngoại lệ và xử lý.
Các hàm JNI duy nhất mà bạn được phép gọi khi có trường hợp ngoại lệ đang chờ xử lý là:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
Nhiều lệnh gọi JNI có thể gửi một ngoại lệ, nhưng thường cung cấp một cách đơn giản hơn
kiểm tra lỗi. Ví dụ: nếu NewString
trả về
giá trị không phải là NULL, nên bạn không cần kiểm tra trường hợp ngoại lệ. Tuy nhiên, nếu
bạn gọi một phương thức (sử dụng một hàm như CallObjectMethod
),
bạn phải luôn kiểm tra xem có ngoại lệ không, vì giá trị trả về không
sẽ có hiệu lực nếu có một ngoại lệ.
Lưu ý rằng các ngoại lệ do mã được quản lý gửi ra sẽ không gỡ bỏ ngăn xếp gốc
khung hình. (Ngoài ra, các ngoại lệ đối với C++, thường không được khuyến khích trên Android, không được
được gửi qua ranh giới chuyển đổi JNI từ mã C++ sang mã được quản lý.)
Hướng dẫn Throw
và ThrowNew
của JNI
đặt một con trỏ ngoại lệ trong luồng hiện tại. Sau khi chuyển về thư mục được quản lý
từ mã gốc, ngoại lệ sẽ được ghi chú và xử lý thích hợp.
Mã gốc có thể "catch" ngoại lệ bằng cách gọi ExceptionCheck
hoặc
ExceptionOccurred
và xoá bằng
ExceptionClear
. Như thường lệ,
việc loại bỏ ngoại lệ mà không xử lý chúng có thể dẫn đến các sự cố.
Không có hàm tích hợp nào để thao tác với đối tượng Throwable
, vì vậy, nếu muốn (giả sử) lấy chuỗi ngoại lệ, bạn sẽ cần
tìm lớp Throwable
, hãy tra cứu mã nhận dạng phương thức cho
getMessage "()Ljava/lang/String;"
, gọi phương thức đó và nếu kết quả
không phải là NULL (Rỗng) sử dụng GetStringUTFChars
để nhận nội dung bạn có thể
tay cho printf(3)
hoặc tương đương.
Kiểm tra mở rộng
JNI thực hiện rất ít việc kiểm tra lỗi. Lỗi thường dẫn đến sự cố. Android cũng cung cấp một chế độ có tên CheckJNI, trong đó các con trỏ trong bảng hàm JavaVM và JNIEnv được chuyển sang các bảng hàm thực hiện một loạt các hoạt động kiểm tra mở rộng trước khi gọi phương thức triển khai chuẩn.
Các bước kiểm tra bổ sung bao gồm:
- Mảng: cố gắng phân bổ một mảng có kích thước âm.
- Con trỏ không hợp lệ: truyền một jarray/jclass/jobject/jstring không hợp lệ đến lệnh gọi JNI hoặc truyền con trỏ NULL đến lệnh gọi JNI có đối số không rỗng.
- Tên lớp: truyền bất kỳ nội dung nào ngoại trừ kiểu tên lớp “java/lang/String” đến lệnh gọi JNI.
- Lệnh gọi quan trọng: thực hiện lệnh gọi JNI giữa một lượt nhận "quan trọng" và bản phát hành tương ứng.
- ByteBuffers trực tiếp: truyền các đối số không hợp lệ đến
NewDirectByteBuffer
. - Trường hợp ngoại lệ: thực hiện lệnh gọi JNI trong khi có một ngoại lệ đang chờ xử lý.
- JNIEnv*s: sử dụng JNIEnv* từ luồng không chính xác.
- jfieldIDs: sử dụng jfieldID NULL hoặc sử dụng jfieldID để đặt một trường thành một giá trị không đúng kiểu (chẳng hạn như cố gắng gán một StringBuilder cho một trường Chuỗi), hoặc sử dụng jfieldID cho một trường tĩnh để đặt một trường thực thể hoặc ngược lại, hoặc sử dụng jfieldID từ một lớp với các phiên bản của một lớp khác.
- jmethodID: sử dụng sai loại jmethodID khi thực hiện lệnh gọi JNI
Call*Method
: loại dữ liệu trả về không chính xác, không khớp tĩnh/không tĩnh, sai loại cho "this" (đối với lệnh gọi không tĩnh) hoặc sai lớp (đối với lệnh gọi tĩnh). - Tệp đối chiếu: sử dụng
DeleteGlobalRef
/DeleteLocalRef
không đúng loại tệp đối chiếu. - Chế độ phát hành: truyền một chế độ phát hành không hợp lệ đến một lệnh gọi phát hành (không phải là
0
,JNI_ABORT
hoặcJNI_COMMIT
). - An toàn về kiểu: trả về một kiểu dữ liệu không tương thích từ phương thức gốc của bạn (chẳng hạn như trả về một StringBuilder từ một phương thức được khai báo để trả về một Chuỗi).
- UTF-8: truyền một trình tự byte Modified UTF-8 không hợp lệ đến lệnh gọi JNI.
(Khả năng truy cập vào các phương thức và trường vẫn chưa được đánh dấu: các hạn chế truy cập không áp dụng cho mã gốc.)
Có một số cách để bật CheckJNI.
Nếu bạn đang sử dụng trình mô phỏng, thì CheckJNI sẽ bật theo mặc định.
Nếu có một thiết bị bị can thiệp hệ thống, bạn có thể sử dụng trình tự các lệnh sau để khởi động lại thời gian chạy khi bật CheckJNI:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
Ở một trong hai trường hợp này, bạn sẽ thấy nội dung tương tự trong đầu ra logcat khi thời gian chạy bắt đầu:
D AndroidRuntime: CheckJNI is ON
Nếu có thiết bị thông thường, bạn có thể sử dụng lệnh sau:
adb shell setprop debug.checkjni 1
Việc này sẽ không ảnh hưởng đến các ứng dụng đang chạy, nhưng mọi ứng dụng chạy từ thời điểm đó trở đi sẽ được bật CheckJNI. (Thay đổi thuộc tính này thành bất kỳ giá trị nào khác hoặc chỉ cần khởi động lại sẽ tắt CheckJNI lần nữa.) Trong trường hợp này, bạn sẽ thấy nội dung tương tự trong đầu ra logcat vào lần tiếp theo ứng dụng khởi động:
D Late-enabling CheckJNI
Bạn cũng có thể đặt thuộc tính android:debuggable
trong tệp kê khai của ứng dụng thành
bật CheckJNI cho ứng dụng của bạn. Xin lưu ý rằng các công cụ xây dựng của Android sẽ tự động thực hiện việc này đối với
một số loại bản dựng nhất định.
Thư viện gốc
Bạn có thể tải mã gốc từ thư viện dùng chung bằng phương thức
System.loadLibrary
.
Trên thực tế, các phiên bản Android cũ hơn gặp lỗi trong PackageManager, khiến quá trình cài đặt và cập nhật thư viện gốc trở nên không đáng tin cậy. ReLinker dự án cung cấp giải pháp cho vấn đề này và các vấn đề tải thư viện gốc khác.
Gọi System.loadLibrary
(hoặc ReLinker.loadLibrary
) từ một lớp tĩnh
trình khởi tạo. Đối số là "không được trang trí" tên thư viện,
do đó, để tải libfubar.so
, bạn sẽ truyền vào "fubar"
.
Nếu chỉ có một lớp có các phương thức gốc, thì bạn cần thực hiện lệnh gọi đến
System.loadLibrary
nằm trong trình khởi tạo tĩnh cho lớp đó. Nếu không, bạn có thể
muốn thực hiện lệnh gọi từ Application
để bạn biết rằng thư viện luôn được tải,
và luôn được tải sớm.
Môi trường thời gian chạy có thể tìm thấy các phương thức gốc của bạn theo hai cách. Bạn có thể chọn
hãy đăng ký chúng bằng RegisterNatives
hoặc bạn có thể để thời gian chạy tự động tra cứu chúng
cùng với dlsym
. Ưu điểm của RegisterNatives
là bạn được ưu tiên
kiểm tra xem các ký hiệu có tồn tại không, đồng thời bạn có thể có thư viện dùng chung nhỏ hơn và nhanh hơn bằng cách không
xuất bất kỳ nội dung nào trừ JNI_OnLoad
. Lợi thế của việc để thời gian chạy khám phá
là vì nó ít viết hơn một chút.
Cách sử dụng RegisterNatives
:
- Cung cấp một hàm
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - Trong
JNI_OnLoad
, hãy đăng ký tất cả phương thức gốc bằngRegisterNatives
. - Tạo bản dựng bằng
-fvisibility=hidden
để chỉJNI_OnLoad
của bạn sẽ được xuất từ thư viện của bạn. Việc này sẽ tạo ra mã nhanh hơn và nhỏ hơn, đồng thời tránh được rủi ro xung đột với các thư viện khác được tải vào ứng dụng của bạn (nhưng nó tạo ra dấu vết ngăn xếp kém hữu ích hơn nếu ứng dụng của bạn gặp sự cố trong mã gốc).
Trình khởi động tĩnh sẽ có dạng như sau:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
Hàm JNI_OnLoad
sẽ có dạng như sau nếu
được viết bằng 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; }
Thay vào đó, bạn có thể sử dụng từ "khám phá" của các phương thức gốc, bạn cần đặt tên cho chúng theo một cách cụ thể (xem thông số JNI để biết chi tiết). Điều này có nghĩa là nếu chữ ký phương thức sai, bạn sẽ không biết về điều này cho đến khi lần đầu tiên phương thức này thực sự được gọi.
Mọi lệnh gọi FindClass
thực hiện từ JNI_OnLoad
sẽ phân giải các lớp trong
ngữ cảnh của trình tải lớp dùng để tải thư viện dùng chung. Khi được gọi từ thiết bị khác
theo ngữ cảnh, FindClass
sẽ dùng trình tải lớp liên kết với phương thức ở đầu
Ngăn xếp Java hoặc nếu không có ngăn xếp Java (vì lệnh gọi đến từ một luồng gốc vừa được đính kèm)
nó sử dụng "hệ thống" trình tải lớp. Trình tải lớp hệ thống không biết về ứng dụng của bạn
nên bạn không thể tự tra cứu các lớp của mình bằng FindClass
trong đó
ngữ cảnh. Điều này giúp JNI_OnLoad
trở thành một nơi thuận tiện để tra cứu và lưu các lớp vào bộ nhớ đệm: một lần
bạn có một tham chiếu chung jclass
hợp lệ
bạn có thể sử dụng đoạn mã đó từ bất kỳ chuỗi thư đính kèm nào.
Cuộc gọi gốc nhanh hơn bằng @FastNative
và @CriticalNative
Bạn có thể chú thích các phương thức gốc bằng
@FastNative
hoặc
@CriticalNative
(nhưng không phải cả hai) để tăng tốc quá trình chuyển đổi giữa mã gốc và mã được quản lý. Tuy nhiên, những chú thích này
đi kèm với một số thay đổi nhất định về hành vi cần được xem xét cẩn thận trước khi sử dụng. Trong khi chúng tôi
hãy đề cập ngắn gọn đến những thay đổi này bên dưới, vui lòng tham khảo tài liệu để biết chi tiết.
Bạn chỉ có thể áp dụng chú giải @CriticalNative
cho các phương thức gốc không
sử dụng các đối tượng được quản lý (trong các tham số hoặc giá trị trả về hoặc dưới dạng this
ngầm ẩn) và đây
chú thích thay đổi ABI chuyển đổi JNI. Việc triển khai gốc phải loại trừ
Tham số JNIEnv
và jclass
qua chữ ký hàm.
Trong khi thực thi phương thức @FastNative
hoặc @CriticalNative
, rác
không thể tạm ngưng luồng cho công việc thiết yếu và có thể bị chặn. Không sử dụng
chú giải cho các phương thức chạy trong thời gian dài, bao gồm cả các phương thức thường nhanh nhưng thường không bị ràng buộc.
Cụ thể, mã không được thực hiện các thao tác I/O quan trọng hoặc có được khoá gốc
có thể bị lưu giữ trong một thời gian dài.
Các chú giải này đã được triển khai để sử dụng trong hệ thống kể từ
Android 8
và trở thành công khai
được thử nghiệm CTS
API trong Android 14. Những tính năng tối ưu hoá này có thể cũng hoạt động trên thiết bị Android 8-13 (mặc dù
không có đảm bảo CTS mạnh mẽ), nhưng tính năng tra cứu động các phương thức gốc chỉ được hỗ trợ trên
Android 12 trở lên, bạn phải đăng ký rõ ràng với JNI RegisterNatives
để chạy trên Android phiên bản 8-11. Các chú thích này bị bỏ qua trên Android 7-, ABI không khớp
cho @CriticalNative
sẽ dẫn đến việc kết hợp đối số không chính xác và có thể dẫn đến sự cố.
Đối với các phương thức quan trọng về hiệu suất cần những chú thích này, bạn nên
đăng ký rõ ràng(các) phương thức bằng JNI RegisterNatives
thay vì dựa vào
"khám phá" dựa trên tên phương thức gốc. Để có được hiệu suất khởi động ứng dụng tối ưu, bạn nên
để bao gồm phương thức gọi của phương thức @FastNative
hoặc @CriticalNative
trong
hồ sơ cơ sở. Kể từ Android 12,
lệnh gọi đến phương thức gốc @CriticalNative
từ phương thức được quản lý đã biên dịch gần như
rẻ như một lệnh gọi không nội tuyến trong C/C++ miễn là tất cả các đối số đều phù hợp với các thanh ghi (ví dụ lên đến
8 tích phân và tối đa 8 đối số dấu phẩy động trên arm64).
Đôi khi, bạn nên chia một phương thức gốc thành hai, một phương thức rất nhanh có thể không thành công và một lỗi khác xử lý các trường hợp chậm. Ví dụ:
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);
Những điểm cần lưu ý đối với phiên bản 64 bit
Để hỗ trợ kiến trúc dùng con trỏ 64 bit, hãy dùng trường long
thay vì
int
khi lưu trữ con trỏ đến cấu trúc gốc trong trường Java.
Các tính năng không được hỗ trợ/khả năng tương thích ngược
Tất cả các tính năng của JNI 1.6 đều được hỗ trợ, ngoại trừ những tính năng sau:
DefineClass
chưa được triển khai. Android không sử dụng Các mã byte Java hoặc tệp lớp, vì vậy, việc truyền dữ liệu lớp nhị phân không có hiệu quả.
Để tương thích ngược với các bản phát hành Android cũ, bạn có thể phải hãy lưu ý:
- Tra cứu động các hàm gốc
Cho đến Android 2.0 (Eclair), '$' ký tự không đúng đã chuyển đổi thành "_00024" trong quá trình tìm kiếm tên phương thức. Đang hoạt động về vấn đề này yêu cầu phải sử dụng đăng ký rõ ràng hoặc chuyển các phương thức gốc bên trong các lớp bên trong.
- Dây tã
Cho đến Android 2.0 (Eclair), bạn không thể sử dụng
pthread_key_create
hàm huỷ để tránh tình trạng "luồng phải được tách trước khi thoát" . (Môi trường thời gian chạy cũng sử dụng hàm huỷ khoá pthread, nên sẽ cần một cuộc đua để xem cái nào được gọi trước.) - Tệp đối chiếu chung yếu
Cho đến Android 2.2 (Froyo), tệp tham chiếu toàn cục yếu chưa được triển khai. Các phiên bản cũ sẽ từ chối mạnh mẽ những yêu cầu sử dụng phiên bản đó. Bạn có thể sử dụng hằng số phiên bản nền tảng Android để kiểm thử khả năng hỗ trợ.
Cho đến Android 4.0 (Ice Cream Sandwich), các tệp tham chiếu toàn cầu yếu chỉ có thể sẽ được truyền đến
NewLocalRef
,NewGlobalRef
vàDeleteWeakGlobalRef
. (Thông số kỹ thuật này khuyến khích để lập trình viên tạo ra các tham chiếu cứng đến các tập lệnh toàn cục yếu trước khi thực hiện bất kỳ thứ gì với họ, vì vậy, bạn không nên hạn chế việc này.)Từ Android 4.0 (Ice Cream Sandwich) trở lên, các tệp tham chiếu toàn cầu yếu có thể được sử dụng như mọi tham chiếu JNI khác.
- Thông tin tham khảo tại địa phương
Cho đến Android 4.0 (Ice Cream Sandwich), các tài liệu tham khảo cục bộ là con trỏ thực sự trực tiếp. Ice Cream Sandwich đã thêm phần gián tiếp cần thiết để hỗ trợ các trình thu gom rác tốt hơn, nhưng điều này có nghĩa là không phát hiện được lỗi JNI trên các bản phát hành cũ hơn. Xem Các thay đổi về tham chiếu cục bộ JNI trong ICS để biết thêm thông tin.
Trong các phiên bản Android trước Android 8.0, số lượng tệp tham chiếu cục bộ bị giới hạn ở giới hạn dành riêng cho phiên bản. Kể từ Android 8.0, Android hỗ trợ số lượng tệp tham chiếu cục bộ không giới hạn.
- Xác định loại tham chiếu bằng
GetObjectRefType
Cho đến Android 4.0 (Ice Cream Sandwich), do việc sử dụng con trỏ trực tiếp (xem ở trên), không thể triển khai
GetObjectRefType
đúng cách. Thay vào đó, chúng tôi đã sử dụng phương pháp phỏng đoán đã xem xét bảng toàn cục yếu, các đối số, các phương thức và bảng toàn cục theo thứ tự đó. Lần đầu tiên Google AdSense tìm thấy con trỏ trực tiếp, nó sẽ báo cáo rằng tham chiếu của bạn thuộc loại tham chiếu đã kiểm tra. Ví dụ: điều này nghĩa là nếu bạn đã gọiGetObjectRefType
trên một jclass toàn cục đã xảy ra giống với jclass được truyền dưới dạng đối số ngầm ẩn cho phương thức tĩnh gốc, bạn sẽ nhận đượcJNILocalRefType
thay vìJNIGlobalRefType
. @FastNative
và@CriticalNative
Trên Android 7, các chú thích tối ưu hoá này đã bị bỏ qua. ABI không khớp cho
@CriticalNative
sẽ dẫn đến đối số sai tổng hợp và có thể gặp sự cố.Tra cứu động các hàm gốc cho
@FastNative
và Các phương thức@CriticalNative
chưa được triển khai trong Android 8-10 và chứa các lỗi đã biết trong Android 11. Sử dụng những biện pháp tối ưu hoá này mà không có thể đăng ký rõ ràng với JNIRegisterNatives
dẫn đến sự cố trên Android 8-11.FindClass
némClassNotFoundException
Để tương thích ngược, Android sẽ gửi
ClassNotFoundException
thay vìNoClassDefFoundError
khi không tìm thấy lớp bằngFindClass
. Hành vi này nhất quán với API phản chiếu JavaClass.forName(name)
.
Câu hỏi thường gặp: Tại sao tôi nhận được UnsatisfiedLinkError
?
Khi làm việc với mã gốc, chúng ta thường thấy lỗi như sau:
java.lang.UnsatisfiedLinkError: Library foo not found
Trong một số trường hợp, thông báo đó có nghĩa là nội dung đó — không tìm thấy thư viện. Trong
các trường hợp khác thư viện tồn tại nhưng không thể mở bằng dlopen(3)
và
Bạn có thể xem thông tin chi tiết về lỗi trong thông báo chi tiết về ngoại lệ.
Lý do phổ biến khiến bạn có thể gặp phải vấn đề "không tìm thấy thư viện" ngoại lệ:
- Thư viện không tồn tại hoặc ứng dụng không truy cập được. Sử dụng
adb shell ls -l <path>
để kiểm tra sự hiện diện và quyền. - Thư viện này không được xây dựng bằng NDK. Điều này có thể dẫn đến các phần phụ thuộc vào hàm hoặc thư viện không tồn tại trên thiết bị.
Một lớp lỗi UnsatisfiedLinkError
khác sẽ có dạng như sau:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
Trong logcat, bạn sẽ thấy:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
Tức là môi trường thời gian chạy đã cố gắng tìm một phương thức so khớp nhưng lại không thành công. Một số lý do phổ biến dẫn đến điều này là:
- Chưa tải được thư viện. Kiểm tra đầu ra logcat cho thông báo về việc tải thư viện.
- Không tìm thấy phương thức do tên hoặc chữ ký không khớp. Chiến dịch này
thường do:
- Đối với quá trình tra cứu phương thức tải từng phần, do đó không khai báo được các hàm C++
với
extern "C"
và phù hợp khả năng hiển thị (JNIEXPORT
). Lưu ý rằng trước khi dùng kem Sandwich, macro JNIEXPORT không chính xác, vì vậy, việc sử dụng GCC mới vớijni.h
cũ sẽ không hoạt động. Bạn có thể dùngarm-eabi-nm
để xem các ký hiệu khi chúng xuất hiện trong thư viện; nếu họ nhìn bị xáo trộn (ví dụ như_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
thay vìJava_Foo_myfunc
) hoặc nếu loại biểu tượng là một chữ 't' viết thường thay vì dùng chữ "T" viết hoa, thì bạn cần điều chỉnh nội dung khai báo. - Để đăng ký rõ ràng, các lỗi nhỏ khi nhập
của phương thức. Hãy đảm bảo rằng những gì bạn đang chuyển đến
lệnh gọi đăng ký khớp với chữ ký trong tệp nhật ký.
Hãy nhớ là "B" là
byte
và "Z" làboolean
. Thành phần tên lớp trong chữ ký bắt đầu bằng "L", kết thúc bằng ";", sử dụng '/' để phân tách tên gói/lớp và sử dụng '$' để phân tách tên lớp bên trong (chẳng hạn nhưLjava/util/Map$Entry;
).
- Đối với quá trình tra cứu phương thức tải từng phần, do đó không khai báo được các hàm C++
với
Việc sử dụng javah
để tự động tạo tiêu đề JNI có thể giúp ích cho bạn
để tránh một số vấn đề.
Câu hỏi thường gặp: Tại sao FindClass
không tìm thấy lớp học của tôi?
(Hầu hết lời khuyên này đều áp dụng hiệu quả như nhau cho những trường hợp thất bại trong việc tìm phương pháp
với GetMethodID
hoặc GetStaticMethodID
hoặc các trường
với GetFieldID
hoặc GetStaticFieldID
.)
Đảm bảo rằng chuỗi tên lớp có định dạng đúng. lớp JNI
tên bắt đầu bằng tên gói và được phân tách bằng dấu gạch chéo,
chẳng hạn như java/lang/String
. Nếu bạn đang tìm kiếm một lớp mảng,
bạn cần bắt đầu với số lượng dấu ngoặc vuông thích hợp và
cũng phải gói lớp bằng 'L' và ":" nên mảng một chiều của
String
sẽ là [Ljava/lang/String;
.
Nếu bạn đang tìm một lớp bên trong, hãy sử dụng '$' thay vì '.'. Nhìn chung,
sử dụng javap
trên tệp .class là một cách hay để tìm hiểu
tên nội bộ của lớp.
Nếu bạn bật tính năng rút gọn mã, hãy đảm bảo rằng bạn định cấu hình mã cần giữ. Đang định cấu hình các quy tắc lưu giữ phù hợp là rất quan trọng vì trình rút gọn mã có thể xoá các lớp, phương thức hoặc các trường chỉ được dùng từ JNI.
Nếu tên lớp hiển thị chính xác, bạn có thể đang gặp phải một trình tải lớp
vấn đề. FindClass
muốn bắt đầu tìm kiếm lớp học trong
trình tải lớp liên kết với mã của bạn. Nó kiểm tra ngăn xếp lệnh gọi,
mã này sẽ có dạng như sau:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
Phương thức trên cùng là Foo.myfunc
. FindClass
tìm đối tượng ClassLoader
được liên kết với Foo
lớp và sử dụng lớp đó.
Việc này thường diễn ra theo ý bạn. Bạn có thể gặp rắc rối nếu
tự tạo một luồng (có thể bằng cách gọi pthread_create
rồi đính kèm với AttachCurrentThread
). Đã có
không có khung ngăn xếp nào từ ứng dụng của bạn.
Nếu bạn gọi FindClass
từ chuỗi này, thì
JavaVM sẽ bắt đầu trong "hệ thống" trình tải lớp thay vì trình tải lớp được liên kết
ứng dụng của bạn, vì vậy cố gắng tìm các lớp dành riêng cho ứng dụng sẽ không thành công.
Có một số cách để giải quyết vấn đề này:
- Thực hiện
FindClass
tra cứu một lần, trongJNI_OnLoad
rồi lưu các tham chiếu lớp vào bộ nhớ đệm để xem sau sử dụng. Mọi lệnh gọiFindClass
được thực hiện trong quá trình thực thiJNI_OnLoad
sẽ sử dụng trình tải lớp liên kết với được gọi làSystem.loadLibrary
(đây là một hàm quy tắc đặc biệt, được cung cấp để giúp việc khởi chạy thư viện thuận tiện hơn). Nếu mã ứng dụng của bạn đang tải thư viện, hãyFindClass
sẽ sử dụng đúng trình tải lớp. - Truyền một thực thể của lớp vào các hàm cần
bằng cách khai báo phương thức gốc để lấy đối số Lớp và
sau đó truyền
Foo.class
vào. - Lưu tệp tham chiếu đến đối tượng
ClassLoader
vào bộ nhớ đệm ở một nơi nào đó thuận tiện và trực tiếp thực hiện các cuộc gọiloadClass
. Điều này yêu cầu bạn phải mất chút công sức.
Câu hỏi thường gặp: Làm cách nào để chia sẻ dữ liệu thô với mã gốc?
Có thể bạn đang ở trong tình huống mà bạn cần truy cập vào vùng đệm dữ liệu thô từ cả mã được quản lý lẫn mã gốc. Ví dụ thường gặp bao gồm cả việc thao túng bitmap hoặc mẫu âm thanh. Có hai phương pháp cơ bản.
Bạn có thể lưu trữ dữ liệu trong byte[]
. Thao tác này cho phép rất nhanh
quyền truy cập từ mã được quản lý. Tuy nhiên, về bản chất, bạn
không đảm bảo được là sẽ truy cập được
vào dữ liệu mà không phải sao chép dữ liệu đó. Trong
một số cách triển khai, GetByteArrayElements
và
GetPrimitiveArrayCritical
sẽ trả về các con trỏ thực tế đến
dữ liệu thô trong vùng nhớ khối xếp được quản lý, nhưng trong các vùng khác, dữ liệu này sẽ phân bổ một bộ đệm
trên vùng nhớ khối xếp gốc rồi sao chép dữ liệu.
Phương án thay thế là lưu trữ dữ liệu trong vùng đệm byte trực tiếp. Các
có thể được tạo bằng java.nio.ByteBuffer.allocateDirect
hoặc
hàm JNI NewDirectByteBuffer
. Không giống như thông thường
vùng đệm byte, bộ nhớ không được phân bổ trên vùng nhớ khối xếp được quản lý và có thể
luôn có thể truy cập trực tiếp từ mã gốc (lấy địa chỉ
cùng với GetDirectBufferAddress
). Tuỳ thuộc vào mức độ trực tiếp
truy cập vùng đệm byte được triển khai, truy cập dữ liệu từ mã được quản lý
có thể rất chậm.
Việc lựa chọn cách sử dụng phụ thuộc vào hai yếu tố:
- Hầu hết các lượt truy cập dữ liệu có xảy ra từ mã được viết bằng Java hoặc trong C/C++?
- Nếu dữ liệu cuối cùng được truyền đến API hệ thống, thì
CANNOT TRANSLATE (Ví dụ: nếu cuối cùng dữ liệu được truyền đến một
nhận một byte[], xử lý theo chiều
ByteBuffer
có thể không hữu ích.)
Nếu không có biến thể chiến thắng rõ ràng, hãy sử dụng vùng đệm byte trực tiếp. Hỗ trợ cho họ được tích hợp trực tiếp vào JNI và hiệu suất sẽ cải thiện trong các bản phát hành sau này.