JNI là Giao diện gốc Java. JNI xác định một cách để mã byte mà Android biên dịch từ mã được quản lý (viết bằng ngôn ngữ lập trình Java hoặc Kotlin) tương tác với mã gốc (viết bằng C/C++). JNI không phụ thuộc vào nhà cung cấp, có hỗ trợ tải mã từ các thư viện dùng chung động và đôi khi khá rườm rà nhưng vẫn tương đối hiệu quả.
Lưu ý: Vì Android biên dịch Kotlin thành mã byte thân thiện với ART theo cách tương tự như ngôn ngữ lập trình Java, nên 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ề mặt cấu trúc JNI và các chi phí liên quan. Để tìm hiểu thêm, hãy xem phần Kotlin và Android.
Nếu bạn chưa quen thuộc với JNI, hãy đọc 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 có sẵn. Một số khía cạnh của giao diện không rõ ràng ngay khi đọc lần đầu, vì vậy, bạn có thể thấy vài phần tiếp theo rất hữu ích.
Để duyệt xem các lượt tham chiếu JNI toàn cục và xem nơi tạo cũng như xoá các lượt tham chiếu JNI toàn cục, hãy sử dụng chế độ xem vùng nhớ khối xếp JNI trong Trình phân tích bộ nhớ trong Android Studio 3.2 trở lên.
Mẹo chung
Hãy cố gắng giảm thiểu mức sử dụng bộ nhớ của lớp JNI. Có một số khía cạnh cần cân nhắc ở đây. Giải pháp JNI của bạn nên cố gắng tuân theo các nguyên tắc sau (được liệt kê bên dưới theo thứ tự quan trọng, bắt đầu từ nguyên tắc quan trọng nhất):
- Giảm thiểu việc sắp xếp các tài nguyên trên lớp JNI. Việc sắp xếp trên 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 lượng dữ liệu bạn cần sắp xếp và tần suất 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 ngôn ngữ lập trình được quản lý và mã được viết bằng C++ khi có thể. Điều này sẽ giúp bạn dễ dàng duy trì giao diện JNI. Thông thường, bạn có thể đơn giản hoá các bản cập nhật giao diện người dùng không đồng bộ bằng cách giữ 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 một hàm C++ từ luồng giao diện người dùng trong mã Java thông qua JNI, bạn nên thực hiện lệnh gọi lại giữa 2 luồng trong ngôn ngữ lập trình Java, trong đó một luồng thực hiện lệnh gọi C++ chặn rồi thông báo cho luồng giao diện người dùng khi lệnh gọi chặn hoàn tất.
- Giảm thiểu số lượng luồng cần chạm hoặc được JNI chạm vào. Nếu bạn cần sử dụng nhóm luồng ở cả ngôn ngữ Java và C++, hãy cố gắng duy trì giao tiếp JNI giữa các chủ sở hữu nhóm thay vì giữa các luồng worker riêng lẻ.
- Giữ mã giao diện của bạn ở một số ít vị trí nguồn C++ và Java dễ xác định để tạo điều kiện cho việc tái cấu trúc trong tương lai. Cân nhắc sử dụng thư viện tự động tạo JNI nếu thích hợp.
JavaVM và JNIEnv
JNI xác định 2 cấu trúc dữ liệu chính là "JavaVM" và "JNIEnv". Về cơ bản, cả hai đều là con trỏ đến con trỏ đến các bảng hàm. (Trong phiên bản C++, đây là các lớp có con trỏ đến bảng hàm và một hàm thành phần cho mỗi hàm JNI gián tiếp thông qua bảng.) JavaVM cung cấp các hàm "giao diện gọi", cho phép bạn tạo và huỷ 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 một JavaVM.
JNIEnv cung cấp hầu hết các hàm JNI. Tất cả các hàm gốc của bạn đều nhận JNIEnv làm đối số đầu tiên, ngoại trừ các phương thức @CriticalNative
, hãy xem các lệnh gọi gốc nhanh hơn.
JNIEnv được dùng cho bộ nhớ cục bộ theo luồng. Vì lý do này, 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ử thiết bị có một cổng; xem AttachCurrentThread
bên dưới.)
Khai báo C của JNIEnv và JavaVM khác với khai báo C++. Tệp "jni.h"
include cung cấp nhiều typedef tuỳ thuộc vào việc tệp đó có được đưa vào C hay C++ hay không. Vì lý do này, bạn không nên đưa các đối số JNIEnv vào tệp tiêu đề mà cả hai ngôn ngữ đều đưa vào. (Nói cách khác: nếu tệp tiêu đề của bạn 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 có 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. Các luồng này thường bắt đầu từ mã được quản lý (bằng Thread.start()
), nhưng cũng có thể được tạo ở nơi khác rồi đính kèm vào JavaVM
. Ví dụ: bạn có thể đính kèm một luồng được bắt đầu bằng pthread_create()
hoặc std::thread
bằng cách sử dụng các hàm AttachCurrentThread()
hoặc AttachCurrentThreadAsDaemon()
. Cho đến khi được đính kèm, một luồng sẽ không có JNIEnv và không thể thực hiện các lệnh gọi JNI.
Thông thường, tốt nhất là bạn nên dùng Thread.start()
để tạo mọi luồng cần gọi vào mã Java. Việc này sẽ đảm bảo bạn có đủ không gian ngăn xếp, bạn đang ở trong ThreadGroup
chính xác và bạn đang sử dụng cùng một ClassLoader
với mã Java của mình. Việc đặt tên cho luồng để gỡ lỗi trong Java cũng dễ dàng hơn so với việc đặt tên 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 tự nhiên sẽ khiến một đối tượng java.lang.Thread
được tạo và thêm vào ThreadGroup
"chính", giúp trình gỡ lỗi nhìn thấy đối tượng đó. Việc gọi AttachCurrentThread()
trên một luồng đã được đính kèm là một thao tác không có tác dụng.
Android không tạm ngưng các luồng thực thi mã gốc. Nếu quá trình thu gom rác đang diễn ra hoặc trình gỡ lỗi đã đưa ra yêu cầu tạm dừng, thì Android sẽ tạm dừng luồng vào lần tiếp theo khi thực hiện một 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 thoát.
Nếu việc mã hoá trực tiếp này gây khó khăn, thì trong Android 2.0 (Eclair) trở lên, bạn có thể dùng pthread_key_create()
để xác định một hàm huỷ sẽ được gọi trước khi luồng thoát và gọi DetachCurrentThread()
từ đó. (Sử dụng khoá đó với pthread_setspecific()
để lưu trữ JNIEnv trong bộ nhớ cục bộ theo luồng; bằng cách đó, khoá sẽ được truyền vào hàm huỷ 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 sẽ 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ã nhận dạng trường cho trường có
GetFieldID
- Lấy nội dung của trường bằng một 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 được một tham chiếu đối tượng lớp rồi đến một mã nhận dạng phương thức. Các mã nhận dạng thường chỉ là con trỏ đến các cấu trúc dữ liệu thời gian chạy nội bộ. Việc tra cứu các phương thức này có thể yêu cầu một số phép so sánh chuỗi, nhưng sau khi bạn có các phương thức này, lệnh gọi thực tế để lấy trường hoặc gọi phương thức sẽ diễn ra rất nhanh.
Nếu hiệu suất là yếu 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. Vì có giới hạn là một JavaVM cho mỗi quy trình, nên việc lưu trữ dữ liệu này trong một cấu trúc tĩnh cục bộ là hợp lý.
Các tham chiếu lớp, mã nhận dạng trường và mã nhận dạng phương thức được đảm bảo hợp lệ cho đến khi lớp được huỷ tải. Các lớp chỉ được huỷ tải nếu tất cả các lớp được liên kết với một ClassLoader có thể được thu gom rác. Điều này hiếm khi xảy ra nhưng không phải là không thể trong Android. Tuy nhiên, lưu ý rằng jclass
là một tham chiếu lớp và phải được bảo vệ bằng một lệnh gọi đến NewGlobalRef
(xem phần tiếp theo).
Nếu bạn muốn lưu vào bộ nhớ đệm các mã nhận dạng khi một lớp được tải và tự động lưu vào bộ nhớ đệm lại nếu lớp đó bị huỷ tải và tải lại, thì cách chính xác để khởi tạo các mã nhận dạng là thêm một đoạn mã 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 các hoạt động tra cứu mã nhận dạng. Mã này sẽ được thực thi một lần khi lớp được khởi tạo. Nếu lớp học từng được huỷ tải rồi tải lại, thì lớp học đó sẽ được thực thi lại.
Tài liệu tham khảo 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 do một hàm JNI trả về đều là "tham chiếu cục bộ". Điều này có nghĩa là nó hợp lệ trong thời gian của phương thức gốc hiện tại trong luồng hiện tại. Ngay cả khi bản thân đối tượng vẫn tiếp tục tồn tại sau khi phương thức gốc trả về, thì giá trị tham chiếu đó vẫn 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 cả 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 tham chiếu khi bạn bật chế độ kiểm tra JNI mở rộng.)
Cách duy nhất để lấy các giá trị tham chiếu không phải cục bộ là thông qua các hàm NewGlobalRef
và NewWeakGlobalRef
.
Nếu muốn giữ lại một giá trị tham chiếu trong thời gian dài hơn, bạn phải sử dụng giá trị tham chiếu "toàn cầu". Hàm NewGlobalRef
nhận tham chiếu cục bộ làm đối số và trả về tham chiếu toàn cục.
Tham chiếu chung được đảm bảo là hợp lệ cho đến khi bạn gọi DeleteGlobalRef
.
Mẫu này thường được dùng khi lưu vào bộ nhớ đệm một jclass được trả về từ FindClass
, ví dụ:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Tất cả các phương thức JNI đều chấp nhận cả các tham chiếu cục bộ và 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ụ: các 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.
Để biết liệu hai thông tin tham chiếu có đề cập đến cùng một đối tượng hay không, bạn phải sử dụng hàm IsSameObject
. Tuyệt đối không so sánh các giá trị 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 rằng các tham chiếu đối tượng là hằng số hoặc duy nhất trong mã gốc. Giá trị đại diện cho một đối tượng có thể khác nhau từ lần gọi này đến lần gọi tiếp theo của một phương thức và có thể hai đối tượng khác nhau có cùng giá trị trong các lệnh gọi liên tiếp. Không sử dụng giá trị jobject
làm khoá.
Các lập trình viên phải "không phân bổ quá mức" các giá trị tham chiếu cục bộ. Trên thực tế, điều này có nghĩa là nếu đang tạo một số lượng lớn các tệp tham chiếu cục bộ, có thể là trong khi chạy qua một mảng đối tượng, bạn nên giải phóng các tệp tham chiếu đó theo cách thủ công bằng DeleteLocalRef
thay vì để JNI thực hiện việc này cho bạn. Bạn chỉ cần triển khai để đặt trước các vị trí cho 16 tài liệu tham khảo cục bộ. Vì vậy, nếu cần nhiều hơn, bạn nên xoá khi thực hiện hoặc sử dụng EnsureLocalCapacity
/PushLocalFrame
để đặt trước thêm.
Xin lưu ý rằng jfieldID
và jmethodID
là các loại mờ, không phải là thông tin tham chiếu đối tượng và không được truyền đến NewGlobalRef
. Con trỏ dữ liệu thô do các hàm như GetStringUTFChars
và GetByteArrayElements
trả về cũng không phải là đối tượng. (Chúng có thể được truyền giữa các luồng và có hiệu lực cho đến khi lệnh gọi Release tương ứng được thực hiện.)
Một trường hợp bất thường cần được đề cập riêng. Nếu bạn đính kèm một luồng gốc bằng AttachCurrentThread
, mã mà bạn đang chạy sẽ không bao giờ tự động giải phóng các tham chiếu cục bộ cho đến khi luồng tách ra. Bạn sẽ phải xoá mọi tham chiếu cục bộ mà bạn tạo theo cách thủ công. Nói chung, mọi mã gốc tạo ra các tham chiếu cục bộ trong một vòng lặp có thể cần phải xoá theo cách thủ công.
Hãy cẩn thận khi sử dụng các giá trị tham chiếu chung. Bạn không thể tránh khỏi các tham chiếu chung, nhưng rất khó gỡ lỗi và có thể gây ra các hành vi (sai) bộ nhớ khó chẩn đoán. Theo dự tính, một giải pháp có ít tài liệu tham khảo toàn cầu 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 cũng cung cấp các phương thức hoạt động với UTF-8 đã sửa đổi. Phương thức mã hoá đã sửa đổi này rất hữu ích cho mã C vì phương thức này mã hoá \u0000 thành 0xc0 0x80 thay vì 0x00. Ưu điểm của việc này là bạn có thể tin tưởng vào việc có các chuỗi kết thúc bằng số 0 theo kiểu C, phù hợp để sử dụng với các hàm chuỗi libc tiêu chuẩn. Nhược điểm là bạn không thể truyền dữ liệu UTF-8 tuỳ ý đến JNI và mong đợi dữ liệu đó hoạt động chính xác.
Để lấy biểu thị UTF-16 của một String
, hãy dùng GetStringChars
.
Xin lưu ý rằng các chuỗi UTF-16 không kết thúc bằng ký tự rỗng và \u0000 được phép, vì vậy, bạn cần giữ lại độ dài chuỗi cũng như con trỏ jchar.
Đừng quên Release
những chuỗi mà bạn Get
. Các hàm chuỗi trả về jchar*
hoặc jbyte*
, là con trỏ kiểu C đến dữ liệu nguyên thuỷ thay vì các tham chiếu cục bộ. Chúng đảm bảo hợp lệ cho đến khi Release
được gọi, tức là chúng không được phát hành khi phương thức gốc trả về.
Dữ liệu được truyền đến NewStringUTF phải ở định dạng UTF-8 đã sửa đổi. Một lỗi thường gặp là đọc dữ liệu ký tự từ một tệp hoặc luồng mạng và chuyển dữ liệu đó đến 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à một 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 chúng sang dạng UTF-8 đã sửa đổi thích hợp.
Nếu không, quá trình 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) sẽ quét các chuỗi và huỷ VM nếu nhận được dữ liệu đầu vào không hợp lệ.
Trước Android 8, việc thao tác với các chuỗi UTF-16 thường nhanh hơn vì Android không yêu cầu bản sao trong GetStringChars
, trong khi GetStringUTFChars
yêu cầu một hoạt động phân bổ và chuyển đổi sang UTF-8.
Android 8 đã thay đổi cách biểu thị String
để sử dụng 8 bit cho mỗi ký tự đối với các chuỗi ASCII (để tiết kiệm bộ nhớ) và bắt đầu sử dụng trình thu gom rác di động. Các tính năng này giúp giảm đáng kể số lượng trường hợp mà ART có thể cung cấp con trỏ đến dữ liệu String
mà không cần sao chép, ngay cả đối với GetStringCritical
. Tuy nhiên, nếu hầu hết các chuỗi được xử lý bằng mã đều ngắn, thì bạn có thể tránh việc phân bổ và huỷ phân bổ trong hầu hết các trường hợp bằng cách sử dụng vùng đệm được 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 nguyên thuỷ
JNI cung cấp các hàm để truy cập vào nội dung của các đối tượng mảng. Mặc dù bạn phải truy cập vào mảng đối tượng từng mục một, nhưng bạn có thể đọc và ghi trực tiếp mảng nguyên thuỷ như thể chúng được khai báo trong C.
Để giao diện hoạt động hiệu quả nhất có thể mà không hạn chế việc triển khai VM, họ lệnh gọi Get<PrimitiveType>ArrayElements
cho phép thời gian chạy trả về con trỏ đến các phần tử thực tế hoặc phân bổ một số bộ nhớ và tạo bản sao. Dù bằng cách nào, con trỏ thô được trả về đều đảm bảo hợp lệ cho đến khi lệnh gọi Release
tương ứng được phát hành (điều này ngụ ý rằng, nếu dữ liệu không được sao chép, thì đối tượng mảng sẽ được ghim xuống và không thể di dời như một phần của việc nén heap).
Bạn phải Release
mọi mảng mà bạn Get
. Ngoài ra, nếu lệnh gọi Get
không thành công, bạn phải đảm bảo rằng mã của bạn không cố gắng Release
con trỏ NULL sau này.
Bạn có thể xác định xem dữ liệu có được sao chép hay không bằng cách truyền con trỏ không phải NULL cho đối số isCopy
. Điều 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 mà thời gian chạy thực hiện phụ thuộc vào việc thời gian chạy trả về con trỏ đến dữ liệu thực tế hay bản sao của dữ liệu đó:
0
- Thực tế: đối tượng mảng không được ghim.
- Sao chép: dữ liệu được sao chép trở lại. Vùng đệm có bản sao sẽ được giải phóng.
JNI_COMMIT
- Thực tế: không làm gì cả.
- Sao chép: dữ liệu đượ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 không được ghim. Các thao tác ghi trước đó sẽ không bị huỷ.
- Sao chép: bộ đệm có bản sao sẽ được giải phóng; mọi thay đổi đối với bộ đệm đó sẽ bị mất.
Một lý do để kiểm tra cờ isCopy
là để biết bạn có cần gọi Release
bằng JNI_COMMIT
sau khi thay đổi một mảng hay không. Nếu đang xen kẽ giữa việc thay đổi và thực thi mã sử dụng nội dung của mảng, thì bạn có thể bỏ qua cam kết không hoạt động. Một lý do khác có thể khiến bạn cần kiểm tra cờ này là để xử lý JNI_ABORT
một cách hiệu quả. 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 vào các hàm khác rồi loại bỏ các thay đổi. Nếu biết rằng JNI đang tạo một bản sao mới cho bạn, thì bạn không cần tạo một bản sao "có thể chỉnh sửa" khác. 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 lỗi thường gặp (lặp lại trong mã ví dụ) là giả định rằng bạn có thể bỏ qua lệnh gọi Release
nếu *isCopy
là false. Điều này không đúng. Nếu không có vùng đệm sao chép nào được phân bổ, thì bộ nhớ gốc phải được ghim và không thể di chuyển bởi trình thu gom rác.
Ngoài ra, lưu ý rằng cờ JNI_COMMIT
không giải phóng mảng và cuối cùng bạn sẽ cần gọi lại Release
bằng một cờ khác.
Cuộc gọi theo khu vực
Có một phương án thay thế cho các lệnh gọi như Get<Type>ArrayElements
và GetStringChars
. Phương án này có thể rất hữu ích khi bạn chỉ muố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
phần tử byte đầu tiên ra khỏi mảng đó rồi giải phóng mảng. Tuỳ thuộc vào quá trình triển khai, lệnh gọi Get
sẽ ghim hoặc sao chép nội dung mảng.
Mã này 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ó bản sao thứ ba.
Bạn có thể thực hiện việc này một cách đơn giản hơn:
env->GetByteArrayRegion(array, 0, len, buffer);
Việc này có một số lợi ích:
- Chỉ cần một lệnh gọi JNI thay vì 2, giúp giảm mức hao tổn.
- Không yêu cầu ghim hoặc 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ơ quên gọi
Release
sau khi có lỗi xảy ra.
Tương tự, bạn có thể 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ự ra khỏi một String
.
Ngoại lệ
Bạn không được gọi hầu hết các hàm JNI trong khi một ngoại lệ đang chờ xử 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ý ngoại lệ đó.
Các hàm JNI duy nhất mà bạn được phép gọi trong khi một 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ây ra 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ề một giá trị không phải là NULL, bạn không cần kiểm tra ngoại lệ. Tuy nhiên, nếu gọi một phương thức (bằng cách sử dụng một hàm như CallObjectMethod
), bạn phải luôn kiểm tra xem có ngoại lệ hay không, vì giá trị trả về sẽ không hợp lệ nếu có ngoại lệ được gửi.
Xin lưu ý rằng các trường hợp ngoại lệ do mã được quản lý tạo ra sẽ không huỷ các khung ngăn xếp gốc. (Và các ngoại lệ C++ (thường không được khuyến khích trên Android) không được truyền qua ranh giới chuyển đổi JNI từ mã C++ sang mã được quản lý.)
Các chỉ dẫn JNI Throw
và ThrowNew
chỉ đặt con trỏ ngoại lệ trong luồng hiện tại. Khi quay lại mã được quản lý từ mã gốc, ngoại lệ sẽ được ghi nhận và xử lý một cách thích hợp.
Mã gốc có thể "bắt" một ngoại lệ bằng cách gọi ExceptionCheck
hoặc ExceptionOccurred
và xoá ngoại lệ đó bằng ExceptionClear
. Như thường lệ, việc loại bỏ các ngoại lệ mà không xử lý chúng có thể dẫn đến các vấn đề.
Không có hàm tích hợp nào để thao tác với chính đối tượng Throwable
, vì vậy, nếu muốn (ví dụ) lấy chuỗi ngoại lệ, bạn sẽ cần tìm lớp Throwable
, tra cứu mã phương thức cho getMessage "()Ljava/lang/String;"
, gọi mã đó và nếu kết quả không phải là NULL, hãy dùng GetStringUTFChars
để lấy thứ gì đó mà bạn có thể chuyển cho printf(3)
hoặc tương đương.
Kiểm tra mở rộng
JNI kiểm tra rất ít lỗi. Lỗi thường dẫn đến sự cố. Android cũng cung cấp một chế độ có tên là CheckJNI, trong đó con trỏ bảng hàm JavaVM và JNIEnv được chuyển sang bảng gồm các hàm thực hiện một loạt các quy trình kiểm tra mở rộng trước khi gọi quá trình triển khai tiêu 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 một lệnh gọi JNI hoặc truyền một con trỏ NULL đến một lệnh gọi JNI có đối số không thể rỗng.
- Tên lớp: truyền bất kỳ tên lớp nào khác ngoài kiểu "java/lang/String" đến một 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ệnh gọi "quan trọng" và lệnh gọi phát hành tương ứng.
- Direct ByteBuffer: truyền đố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ừ sai luồng.
- jfieldIDs: sử dụng jfieldID NULL hoặc sử dụng jfieldID để đặt một trường thành giá trị có loại không chính xác (ví dụ: cố gắng chỉ định một StringBuilder cho trường String) 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 có các thực thể của một lớp khác.
- jmethodID: sử dụng loại jmethodID không chính xác khi thực hiện lệnh gọi JNI
Call*Method
: kiểu trả về không chính xác, không khớp giữa tĩnh và không tĩnh, kiểu không chính xác cho "this" (đối với các lệnh gọi không tĩnh) hoặc lớp không chính xác (đối với các lệnh gọi tĩnh). - Tham chiếu: sử dụng
DeleteGlobalRef
/DeleteLocalRef
cho loại tham chiếu không phù hợp. - Chế độ phát hành: truyền chế độ phát hành không hợp lệ đến một lệnh gọi phát hành (một chế độ khác với
0
,JNI_ABORT
hoặcJNI_COMMIT
). - Tính an toàn về kiểu: trả về một kiểu không tương thích từ phương thức gốc (ví dụ: trả về một StringBuilder từ một phương thức được khai báo để trả về một String).
- UTF-8: truyền một chuỗi byte UTF-8 đã sửa đổi không hợp lệ đến một 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 kiểm tra: các hạn chế về quyền 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ó thiết bị đã được root, bạn có thể dùng chuỗi lệnh sau để khởi động lại thời gian chạy khi CheckJNI được bật:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
Trong cả hai trường hợp này, bạn sẽ thấy nội dung như sau trong đầu ra logcat khi thời gian chạy bắt đầu:
D AndroidRuntime: CheckJNI is ON
Nếu có một thiết bị thông thường, bạn có thể dùng lệnh sau:
adb shell setprop debug.checkjni 1
Điều này sẽ không ảnh hưởng đến các ứng dụng đang chạy, nhưng mọi ứng dụng được khởi chạy từ thời điểm đó trở đi sẽ được bật CheckJNI. (Thay đổi thuộc tính 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 một lần nữa.) Trong trường hợp này, bạn sẽ thấy nội dung như sau trong đầu ra logcat vào lần tiếp theo một ứ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 để bật CheckJNI chỉ cho ứng dụng của bạn. Xin lưu ý rằng các công cụ tạo bản dựng Android sẽ tự động thực hiện việc này cho 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ừ các thư viện dùng chung bằng System.loadLibrary
tiêu chuẩn.
Trên thực tế, các phiên bản Android cũ vướng phải nhiều lỗi trong PackageManager, khiến quá trình cài đặt và cập nhật thư viện gốc không đáng tin cậy. Dự án ReLinker đưa ra giải pháp cho vấn đề này và các vấn đề khác liên quan đến việc tải thư viện gốc.
Gọi System.loadLibrary
(hoặc ReLinker.loadLibrary
) từ một trình khởi tạo lớp tĩnh. Đối số là tên thư viện "không được trang trí", vì vậy để tải libfubar.so
, bạn sẽ truyền vào "fubar"
.
Nếu bạn chỉ có một lớp có các phương thức gốc, thì việc gọi System.loadLibrary
sẽ nằm trong một 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
để biết rằng thư viện luôn được tải và luôn được tải sớm.
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ể đăng ký rõ ràng các thành phần này bằng RegisterNatives
hoặc để thời gian chạy tra cứu động các thành phần này bằng dlsym
. Ưu điểm của RegisterNatives
là bạn sẽ được kiểm tra trước rằng các ký hiệu tồn tại, đồng thời bạn có thể có các thư viện dùng chung nhỏ hơn và nhanh hơn bằng cách không xuất bất cứ thứ gì ngoài JNI_OnLoad
. Lợi thế của việc cho phép thời gian chạy khám phá các hàm của bạn là bạn sẽ viết ít mã 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ả các phương thức gốc bằngRegisterNatives
. - Tạo bằng tập lệnh phiên bản (nên dùng) hoặc dùng
-fvisibility=hidden
để chỉJNI_OnLoad
của bạn được xuất từ thư viện. Điều này tạo ra mã nhanh hơn và nhỏ hơn, đồng thời tránh được các xung đột tiềm ẩn với các thư viện khác được tải vào ứng dụng của bạn (nhưng sẽ tạo ra các dấu vết ngăn xếp ít 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 tạo 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; }
Để sử dụng "khám phá" các phương thức gốc, bạn cần đặt tên cho các phương thức đó theo một cách cụ thể (xem quy cách JNI để biết thông tin chi tiết). Điều này có nghĩa là nếu chữ ký phương thức không chính xác, bạn sẽ không biết về điều đó cho đến lần đầu tiên phương thức thực sự được gọi.
Mọi lệnh gọi FindClass
được thực hiện từ JNI_OnLoad
sẽ phân giải các lớp trong bối cảnh của trình tải lớp được dùng để tải thư viện dùng chung. Khi được gọi từ các ngữ cảnh khác, FindClass
sẽ sử dụng trình tải lớp được liên kết với phương thức ở đầu ngăn xếp Java, hoặc nếu không có (vì lệnh gọi đến từ một luồng gốc vừa được đính kèm), thì trình tải lớp sẽ sử dụng trình tải lớp "hệ thống". Trình tải lớp hệ thống không biết về các lớp của ứng dụng, vì vậy, bạn sẽ không thể tra cứu các lớp của riêng mình bằng FindClass
trong ngữ cảnh đó. Điều này khiến JNI_OnLoad
trở thành một nơi thuận tiện để tra cứu và lưu vào bộ nhớ đệm các lớp: sau khi có một jclass
tham chiếu chung hợp lệ, bạn có thể sử dụng tham chiếu đó từ bất kỳ luồng nào được đính kèm.
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 được dùng cả hai) để tăng tốc độ chuyển đổi giữa mã được quản lý và mã gốc. Tuy nhiên, những chú thích này có một số thay đổi về hành vi mà bạn cần cân nhắc kỹ lưỡng trước khi sử dụng. Mặc dù chúng tôi đề cập ngắn gọn đến những thay đổi này bên dưới, nhưng vui lòng tham khảo tài liệu để biết thông tin chi tiết.
Bạn chỉ có thể áp dụng chú thích @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à chú thích này sẽ thay đổi ABI chuyển đổi JNI. Hoạt động triển khai gốc phải loại trừ các tham số JNIEnv
và jclass
khỏi chữ ký hàm.
Trong khi thực thi phương thức @FastNative
hoặc @CriticalNative
, quá trình thu thập 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 dùng các chú thích này 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ị giới hạn.
Cụ thể, mã không được thực hiện các thao tác I/O quan trọng hoặc thu thập các khoá gốc có thể được giữ trong thời gian dài.
Các chú thích này được triển khai để hệ thống sử dụng kể từ Android 8 và trở thành API công khai được kiểm thử CTS trong Android 14. Những hoạt động tối ưu hoá này cũng có thể hoạt động trên các thiết bị Android 8 – 13 (mặc dù không có các đảm bảo CTS mạnh mẽ), nhưng hoạt độ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 bắt buộc phải đăng ký rõ ràng bằng JNI RegisterNatives
để chạy trên các phiên bản Android 8 – 11. Các chú thích này sẽ bị bỏ qua trên Android 7 trở xuống, sự không khớp ABI cho @CriticalNative
sẽ dẫn đến việc sắp xếp đối số không chính xác và có thể gây ra 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ý(các) phương thức một cách rõ ràng bằng JNI RegisterNatives
thay vì dựa vào "khám phá" dựa trên tên của các phương thức gốc. Để đạt được hiệu suất tối ưu khi khởi động ứng dụng, bạn nên thêm các phương thức gọi @FastNative
hoặc @CriticalNative
vào hồ sơ cơ sở. Kể từ Android 12, lệnh gọi đến phương thức gốc @CriticalNative
từ mộ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ụ: tối đa 8 đối số nguyê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 phương thức: một phương thức rất nhanh có thể không thành công và một phương thức 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 cân nhắc về 64 bit
Để hỗ trợ các cấu trúc sử dụng con trỏ 64 bit, hãy dùng trường long
thay vì int
khi lưu trữ con trỏ đến một 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ừ:
DefineClass
chưa được triển khai. Android không sử dụng mã byte Java hoặc tệp lớp, vì vậy, việc truyền dữ liệu lớp nhị phân sẽ không hoạt động.
Để tương thích ngược với các bản phát hành Android cũ, bạn có thể cần lưu ý những điều sau:
- Tra cứu động các hàm gốc
Cho đến Android 2.0 (Eclair), ký tự "$" không được chuyển đổi đúng cách thành "_00024" trong quá trình tìm kiếm tên phương thức. Để giải quyết vấn đề này, bạn cần sử dụng chế độ đăng ký rõ ràng hoặc di chuyển các phương thức gốc ra khỏi các lớp bên trong.
- Tách luồng
Cho đến Android 2.0 (Eclair), bạn không thể dùng hàm huỷ
pthread_key_create
để tránh kiểm tra "luồng phải được tách trước khi thoát". (Thời gian chạy cũng sử dụng một hàm huỷ khoá pthread, vì vậy, sẽ có một cuộc đua để xem hàm nào được gọi trước.) - Tham chiếu toàn cục yếu
Cho đến Android 2.2 (Froyo), các tham chiếu chung yếu chưa được triển khai. Các phiên bản cũ sẽ từ chối mạnh mẽ những nỗ lực sử dụng chúng. Bạn có thể sử dụng các hằng số phiên bản nền tảng Android để kiểm tra khả năng hỗ trợ.
Cho đến Android 4.0 (Ice Cream Sandwich), các tham chiếu chung yếu chỉ có thể được truyền đến
NewLocalRef
,NewGlobalRef
vàDeleteWeakGlobalRef
. (Thông số kỹ thuật này khuyến khích các lập trình viên tạo các tham chiếu cố định đến các biến toàn cục yếu trước khi làm bất cứ điều gì với chúng, vì vậy, điều này hoàn toàn không nên hạn chế.)Từ Android 4.0 (Ice Cream Sandwich) trở đi, bạn có thể sử dụng các lượt tham chiếu toàn cục yếu như mọi lượt tham chiếu JNI khác.
- Lựa chọn ưu tiên mang tính địa phương
Cho đến Android 4.0 (Ice Cream Sandwich), các tham chiếu cục bộ thực sự là con trỏ trực tiếp. Ice Cream Sandwich đã thêm lớp 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à nhiều lỗi JNI không thể phát hiện được trên các bản phát hành cũ. Hãy xem phần Các thay đổi về tham chiếu cục bộ JNI trong ICS để biết thêm thông tin chi tiết.
Trong các phiên bản Android trước Android 8.0, số lượng tài liệu tham khảo cục bộ bị giới hạn ở một giới hạn dành riêng cho phiên bản. Kể từ Android 8.0, Android hỗ trợ số lượng tham chiếu cục bộ không giới hạn.
- Xác định loại tệp đối 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
một cách chính xác. Thay vào đó, chúng tôi đã sử dụng một phương pháp phỏng đoán để xem xét bảng toàn cục yếu, các đối số, bảng cục bộ và bảng toàn cục theo thứ tự đó. Lần đầu tiên tìm thấy con trỏ trực tiếp của bạn, nó sẽ báo cáo rằng tham chiếu của bạn thuộc loại mà nó đang kiểm tra. Ví dụ: điều này có nghĩa là nếu bạn gọiGetObjectRefType
trên một jclass chung tình cờ giống với jclass được truyền dưới dạng một đối số ngầm đến phương thức gốc tĩnh của bạn, thì bạn sẽ nhận đượcJNILocalRefType
thay vìJNIGlobalRefType
. @FastNative
và@CriticalNative
Cho đến Android 7, những chú thích tối ưu hoá này đã bị bỏ qua. Sự không khớp ABI cho
@CriticalNative
sẽ dẫn đến việc sắp xếp đối số không chính xác và có thể gặp sự cố.Tính năng tra cứu động các hàm gốc cho phương thức
@FastNative
và@CriticalNative
chưa được triển khai trong Android 8-10 và có các lỗi đã biết trong Android 11. Việc sử dụng các hoạt động tối ưu hoá này mà không đăng ký rõ ràng với JNIRegisterNatives
có thể 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
khiFindClass
không tìm thấy một lớp. 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 trên mã gốc, bạn 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 này có nghĩa là thư viện không được tìm thấy. Trong các trường hợp khác, thư viện tồn tại nhưng dlopen(3)
không mở được và bạn có thể tìm thấy thông tin chi tiết về lỗi trong thông báo chi tiết của ngoại lệ.
Các lý do phổ biến khiến bạn có thể gặp phải ngoại lệ "không tìm thấy thư viện":
- Thư viện không tồn tại hoặc ứng dụng không truy cập được. Hãy dùng
adb shell ls -l <path>
để kiểm tra sự hiện diện và các quyền của thư viện. - Thư viện này không được tạo bằng NDK. Điều này có thể dẫn đến các phần phụ thuộc vào những hàm hoặc thư viện không có trên thiết bị.
Một lớp lỗi UnsatisfiedLinkError
khác 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
Điều này có nghĩa là thời gian chạy đã cố gắng tìm một phương thức phù hợp nhưng không thành công. Một số lý do phổ biến dẫn đến việc này là:
- Thư viện không tải được. Kiểm tra đầu ra logcat để xem các 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. Điều này thường là do:
- Đối với phương thức tìm kiếm lười biếng, không khai báo các hàm C++ bằng
extern "C"
và chế độ hiển thị thích hợp (JNIEXPORT
). Xin lưu ý rằng trước Ice Cream 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 biểu tượng xuất hiện trong thư viện; nếu chúng trông bị biến dạng (chẳng hạn như_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
thay vìJava_Foo_myfunc
) hoặc nếu loại biểu tượng là chữ "t" viết thường thay vì chữ "T" viết hoa, thì bạn cần điều chỉnh khai báo. - Đối với quá trình đăng ký rõ ràng, các lỗi nhỏ khi nhập chữ ký phương thức. Đảm bảo rằng những gì bạn đang truyền đến lệnh gọi đăng ký khớp với chữ ký trong tệp nhật ký.
Hãy nhớ rằng "B" là
byte
và "Z" làboolean
. Các 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 (ví dụ:Ljava/util/Map$Entry;
).
- Đối với phương thức tìm kiếm lười biếng, không khai báo các hàm C++ bằng
Việc sử dụng javah
để tự động tạo tiêu đề JNI có thể giúp 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 các lời khuyên này cũng áp dụng cho trường hợp không tìm thấy các phương thức có GetMethodID
hoặc GetStaticMethodID
, hoặc các trường có GetFieldID
hoặc GetStaticFieldID
.)
Đảm bảo rằng chuỗi tên lớp có định dạng chính xác. Tên lớp JNI 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 đang tra cứu một lớp mảng, bạn cần bắt đầu bằng số lượng dấu ngoặc vuông thích hợp và cũng phải bao bọc lớp bằng "L" và ";", vì vậy, một mảng một chiều của String
sẽ là [Ljava/lang/String;
.
Nếu bạn đang tra cứu một lớp bên trong, hãy sử dụng "$" thay vì ".". Nhìn chung, việc sử dụng javap
trên tệp .class là một cách hay để tìm ra 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 nhớ định cấu hình mã cần giữ lại. Việc định cấu hình các quy tắc giữ lại thích 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 trường chỉ được dùng từ JNI.
Nếu tên lớp có vẻ đúng, thì có thể bạn đang gặp phải vấn đề về trình tải lớp. FindClass
muốn bắt đầu tìm kiếm lớp trong trình tải lớp được liên kết với mã của bạn. Nó kiểm tra ngăn xếp lệnh gọi, 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 thấy đối tượng ClassLoader
được liên kết với lớp Foo
và sử dụng đối tượng đó.
Thao tác này thường sẽ thực hiện những gì bạn muố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 bằng AttachCurrentThread
). Giờ đây, 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ừ luồng này, JavaVM sẽ bắt đầu trong trình tải lớp "hệ thống" thay vì trình tải lớp được liên kết với ứng dụng của bạn, vì vậy, các nỗ lực tìm lớp dành riêng cho ứng dụng sẽ không thành công.
Có một số cách để khắc phục vấn đề này:
- Thực hiện các lượt tìm kiếm
FindClass
một lần trongJNI_OnLoad
và lưu vào bộ nhớ đệm các tham chiếu lớp để sử dụng sau này. 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 được liên kết với hàm đã gọiSystem.loadLibrary
(đây là một quy tắc đặc biệt, được cung cấp để giúp việc khởi chạy thư viện trở nên thuận tiện hơn). Nếu mã ứng dụng của bạn đang tải thư viện,FindClass
sẽ sử dụng trình tải lớp chính xác. - Truyền một thực thể của lớp vào các hàm cần thực thể đó bằng cách khai báo phương thức gốc để lấy đối số Lớp, rồi truyền
Foo.class
vào. - Lưu vào bộ nhớ đệm một tham chiếu đến đối tượng
ClassLoader
ở một nơi nào đó thuận tiện và đưa ra các lệnh gọiloadClass
trực tiếp. Bạn cần phải nỗ lực một chút.
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?
Bạn có thể gặp phải tình huống cần truy cập vào một vùng đệm lớn gồm dữ liệu thô từ cả mã được quản lý và mã gốc. Ví dụ thường gặp bao gồm việc thao tác với các mẫu bitmap hoặc âm thanh. Có hai phương pháp cơ bản.
Bạn có thể lưu trữ dữ liệu trong một byte[]
. Điều này cho phép truy cập rất nhanh từ mã được quản lý. Tuy nhiên, ở phía gốc, bạn không được đảm bảo có thể truy cập vào dữ liệu mà không cần sao chép. Trong một số cách triển khai, GetByteArrayElements
và GetPrimitiveArrayCritical
sẽ trả về con trỏ thực tế đến dữ liệu thô trong vùng nhớ heap được quản lý, nhưng trong các cách triển khai khác, nó sẽ phân bổ một vùng đệm trên vùng nhớ heap gốc và sao chép dữ liệu.
Một cách khác là lưu trữ dữ liệu trong bộ đệm byte trực tiếp. Bạn có thể tạo các đối tượng này bằng java.nio.ByteBuffer.allocateDirect
hoặc hàm JNI NewDirectByteBuffer
. Không giống như các vùng đệm byte thông thường, bộ nhớ không được phân bổ trên vùng nhớ heap được quản lý và luôn có thể truy cập trực tiếp từ mã gốc (lấy địa chỉ bằng GetDirectBufferAddress
). Tuỳ thuộc vào cách triển khai quyền truy cập trực tiếp vào vùng đệm byte, việc truy cập vào dữ liệu từ mã được quản lý có thể rất chậm.
Việc lựa chọn sử dụng loại nào phụ thuộc vào 2 yếu tố:
- Hầu hết các hoạt động truy cập dữ liệu sẽ diễn ra từ mã được viết bằng Java hay bằng C/C++?
- Nếu cuối cùng dữ liệu được truyền đến một API hệ thống, thì dữ liệu đó phải ở dạng nào? (Ví dụ: nếu dữ liệu cuối cùng được truyền đến một hàm nhận byte[], thì việc xử lý trong một
ByteBuffer
trực tiếp có thể là không nên.)
Nếu không có biến thể nào có hiệu quả rõ ràng, hãy sử dụng bộ đệm byte trực tiếp. Hỗ trợ cho các hàm này đượ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.