Mẹo về JNI

JNI là Giao diện gốc Java. JNI xác định 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 là giao diện trung lập với nhà cung cấp, hỗ trợ tải mã từ các thư viện dùng chung động và mặc dù đôi khi cồng kềnh nhưng vẫn hiệu quả.

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 bài viết 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 phiên bản 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 số phương diện cần cân nhắc ở đâ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. Việc điều phối 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 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, với một trong hai 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 thư viện 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". Về cơ bản, cả hai đều là con trỏ đến con trỏ đến bảng hàm. (Trong phiên bản C++, đó là các lớp có con trỏ đến bảng hàm và hàm thành viên cho mỗi hàm JNI 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 một JavaVM.

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 đề của bạn yêu cầu #ifdef __cplusplus, bạn có thể phải làm thêm một số việc nếu có bất kỳ nội dung nào trong tiêu đề đó tham chiếu đến JNIEnv.)

Luồng

Tất cả luồng đều là luồng Linux, do nhân 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. Để 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. Việc này sẽ đảm bảo rằng bạn có đủ không gian ngăn xếp, bạn đang ở đúng ThreadGroup và bạn đang sử dụng cùng một ClassLoader với mã Java. 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_tstd::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 gom rác hoặc trình gỡ lỗi đã đưa ra yêu cầu tạm ngưng, 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 đó, mã 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. 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 chúng có thể cần vài chuỗi các phép so sánh, nhưng 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à một tệp tham chiếu lớp và phải được bảo vệ bằng lệnh gọi đến NewGlobalRef (xem phần tiếp theo).

Nếu bạn muốn lưu các mã nhận dạng vào bộ nhớ đệm khi một lớp được tải và tự động lưu lại các mã nhận dạng đó vào bộ nhớ đệm nếu lớp đó từng được tải xuống và tải lại, thì cách chính xác để khởi chạy cá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, lệnh này mới được thực thi lại.

Tham chiếu cục bộ và toàn cục

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 hàm JNI trả về đều 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 khi phương thức gốc trả về, 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 jclass, jstringjarray. (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 NewGlobalRefNewWeakGlobalRef.

Nếu muốn giữ lại một tệp tham chiếu trong thời gian dài hơn, bạn phải sử dụng tệp tham chiếu "toàn cục". 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 toàn cục đượ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 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 giá trị 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.

Xin lưu ý rằng jfieldIDjmethodID là các loại mờ, 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 luồng và có hiệu lực cho đến khi có lệnh gọi Phát hành trùng khớp.)

Có 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 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. Nhìn chung, mọi mã gốc tạo tham chiếu cục bộ trong một vòng lặp có thể cần phải xoá một số tham chiếu theo cách thủ công.

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 cũng cung cấp các phương thức hoạt động với UTF-8 đã sửa đổi. Bộ mã hoá đã sửa đổi rất hữu ích cho mã C vì bộ mã hoá này mã hoá \u0000 thành 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 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ữ nguyê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ộ. Các đối tượng này được đảm bảo hợp lệ cho đến khi Release được gọi, tức là các đối tượng này không được phát hành 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. Một lỗi phổ biến 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 dữ liệu. 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) sẽ quét các 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 trình bày 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 trình thu gom rác di chuyển. Các tính năng này làm 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 tạo bản sao, ngay cả đối với 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ù phải truy cập từng mục một vào các mảng đối tượng, nhưng bạn có thể đọc và ghi trực tiếp các mảng nguyên gốc 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 (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 liệu dữ liệu có được sao chép hay không bằng cách truyền vào một con trỏ không phải 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 hành động do môi trường thời gian chạy thực hiện phụ thuộc vào việc môi trường 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 đã bị bỏ ghim.
    • Sao chép: dữ liệu được sao chép 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 đã bị bỏ 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ể khiến bạ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 đế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.

Lệnh gọi vùng

Có một phương án thay thế cho các lệnh gọi như Get<Type>ArrayElementsGetStringChars 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ơ lập trình viên mắc lỗi — không có nguy cơ quên gọi Release sau khi có lỗi.

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 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ử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ệ.

Xin lưu ý rằng các trường hợp ngoại lệ do mã được quản lý gửi sẽ không tháo dỡ các khung ngăn xếp gốc. (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ý.) Các hướng dẫn ThrowThrowNew của JNI chỉ đặ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, trường hợp 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 (ví dụ) nhận chuỗi ngoại lệ, bạn cần tìm lớp Throwable, tra cứu mã 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, hãy sử dụng GetStringUTFChars để nhận nội dung mà bạn có thể chuyển 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 là CheckJNI, trong đó con trỏ 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 bước 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 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.
  • ByteBuffer trực tiếp: 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ừ luồng không chính xác.
  • jfieldID: sử dụng jfieldID NULL hoặc sử dụng jfieldID để đặt trường thành một giá trị thuộc loại không chính xác (chẳng hạn như cố gắng gán StringBuilder cho trường String), hoặc sử dụng jfieldID cho trường tĩnh để đặ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 thực thể 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, loại dữ liệu không chính xác cho "this" (đối với lệnh gọi không tĩnh) hoặc lớp không chính xác (đố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ặc JNI_COMMIT).
  • An toàn 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 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

Trong cả hai trường hợp này, bạn sẽ thấy nội dung tương tự như sau trong kết quả 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ừ 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ũ hơn gặp lỗi trong PackageManager, khiến việc 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ố này là tên thư viện "chưa được trang trí", vì vậy, để 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ể đăng ký rõ ràng các lớp này bằng RegisterNatives hoặc cho phép thời gian chạy tra cứu các lớp này một cách linh động bằng 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ì mã cần viết í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ằng RegisterNatives.
  • Tạo bằng tập lệnh phiên bản (ưu tiên) hoặc sử dụng -fvisibility=hidden để chỉ xuất JNI_OnLoad từ thư viện. Điều này giúp tạo ra mã nhanh hơn và nhỏ hơn, đồng thời tránh các xung đột tiềm ẩn với các thư viện khác được tải vào ứng dụng (nhưng sẽ tạo ra 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 độ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;
}

Để sử dụng tính nă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 thông số kỹ thuật 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 đ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 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ề 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 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: sau khi có thông tin tham chiếu toàn cục jclass hợp lệ, bạn có thể sử dụng thông tin tham chiếu đó từ bất kỳ luồng đính kèm nào.

Cuộc gọi gốc nhanh hơn bằng @FastNative@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. Phương thức triển khai gốc phải loại trừ các tham số JNIEnvjclass khỏi 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ý(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 phương thức gốc. Để đạt được hiệu suất khởi động ứng dụng tối ưu, bạn nên đưa các phương thức gọi của @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ừ 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 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ều cần cân nhắc về 64 bit

Để hỗ trợ các kiến trúc sử dụng con trỏ 64 bit, hãy sử 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ả tính năng JNI 1.6 đều được hỗ trợ, ngoại trừ các ngoại lệ 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 di chuyển các phương thức gốc bên trong các lớp bên trong.

  • Tách các luồng

    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.)

  • Tham chiếu toàn cục 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, NewGlobalRefDeleteWeakGlobalRef. (Thông số kỹ thuật khuyến khích mạnh mẽ các lập trình viên tạo tệp tham chiếu cứng đến các biến toàn cục yếu trước khi thực hiện bất kỳ thao tác nào với các biến đó, vì vậy, điều này không nên bị hạn chế.)

    Kể từ Android 4.0 (Ice Cream Sandwich), bạn có thể sử dụng các tệp tham chiếu toàn cục yếu như mọi tệp 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 tính năng gián tiếp cần thiết để hỗ trợ các trình thu gom rác hiệu quả hơn, nhưng điều này có nghĩa là nhiều lỗi JNI không phát hiện được trên các bản phát hành cũ. 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 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 một phương pháp phỏng đoán duyệt qua bảng toàn cục yếu, đối số, bảng cục bộ 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, thì 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 có nghĩa là nếu bạn gọi GetObjectRefType trên một jclass toàn cục trùng khớp với jclass được truyền dưới dạng đối số ngầm ẩn đến phương thức gốc tĩnh, bạn sẽ nhận được JNILocalRefType thay vì JNIGlobalRefType.

  • @FastNative@CriticalNative

    Cho đến Android 7, các chú thích tối ưu hoá này bị bỏ qua. Việc không khớp ABI cho @CriticalNative sẽ dẫn đến việc điều phối đối số không chính xác 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 JNI RegisterNatives dẫn đến sự cố trên Android 8-11.

  • FindClass ném ClassNotFoundException

    Để tương thích ngược, Android sẽ gửi ClassNotFoundException thay vì NoClassDefFoundError khi FindClass 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 Java Class.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, tên đó có nghĩa là nội dung đó — không tìm thấy thư viện. Ngang bằng 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 tính năng tra cứu phương thức lazy, không khai báo được 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ới jni.h cũ sẽ không hoạt động. Bạn có thể sử dụng arm-eabi-nm để xem các ký hiệu khi chúng xuất hiện trong thư viện; nếu các ký hiệu đó trông bị xáo trộn (chẳng hạn như _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass thay vì Java_Foo_myfunc) hoặc nếu loại ký hiệu là "t" viết thường thay vì "T" viết hoa, thì bạn cần điều chỉnh nội dung khai báo.
    • Đối với phương thức đăng ký rõ ràng, các lỗi nhỏ khi nhập chữ ký phương thức. Đảm bảo rằng nội dung 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ớ là "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;).

Việc sử dụng javah để tự động tạo tiêu đề JNI có thể giúp bạn tránh được 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 phương thức bằng GetMethodID hoặc GetStaticMethodID hoặc trường bằng GetFieldID hoặc GetStaticFieldID.)

Đảm bảo rằng chuỗi tên lớp có định dạng chính xác. 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 nhớ định cấu hình mã cần giữ lại. Đ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 trong trình tải lớp liên kết với mã của bạn. Phương thức này 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 đối tượng ClassLoader được liên kết với Foo lớp và sử dụng lớp đó.

Thao tác này thường làm được việc bạn muốn. Bạn có thể gặp sự cố nếu tự tạo luồng (có thể bằng cách gọi pthread_create rồi đính kèm luồng đó bằng AttachCurrentThread). Bây giờ, ứng dụng của bạn không có khung ngăn xếp nào. Nếu bạn gọi FindClass từ luồng này, JavaVM sẽ khởi động trong trình tải lớp "hệ thống" thay vì trình tải lớp liên kết với ứng dụng của bạn, vì vậy, các nỗ lực 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, trong JNI_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ọi FindClass được thực hiện trong quá trình thực thi JNI_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ãy FindClass 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 tham chiếu đến đối tượng ClassLoader vào bộ nhớ đệm ở một nơi thuận tiện và trực tiếp đưa ra lệnh gọi loadClass. Đ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ụ phổ biến bao gồm thao tác với 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[]. Điều này cho phép truy cập rất nhanh từ mã được quản lý. Tuy nhiên, ở phía mã gốc, bạn không được đảm bảo rằng có thể truy cập vào dữ liệu mà không cần sao chép dữ liệu. Trong một số cách triển khai, GetByteArrayElementsGetPrimitiveArrayCritical sẽ trả về 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 một số cách triển khai khác, các hàm này sẽ phân bổ vùng đệm trên vùng nhớ khối xếp gốc và sao chép dữ liệu sang.

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ố:

  1. 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++?
  2. Nếu dữ liệu cuối cùng được truyền tới API hệ thống, thì CANNOT TRANSLATE (Ví dụ: nếu dữ liệu cuối cùng được truyền vào một hàm nhận byte[], thì việc xử lý trong ByteBuffer trực tiếp có thể là không khôn ngoan.)

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 các API này được tích hợp trực tiếp vào JNI và hiệu suất sẽ được cải thiện trong các bản phát hành sau này.