Mẹo về JNI

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_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 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, 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 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 jfieldIDjmethodID 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>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ơ 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 ThrowThrowNew 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ặc JNI_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ằng RegisterNatives.
  • 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@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ố JNIEnvjclass 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, NewGlobalRefDeleteWeakGlobalRef. (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ọi GetObjectRefType 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 được JNILocalRefType thay vì JNIGlobalRefType.

  • @FastNative@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 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 không tìm thấy lớp bằng FindClass. 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, 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ới jni.h cũ sẽ không hoạt động. Bạn có thể dùng arm-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;).

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, 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 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ọ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ụ 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, GetByteArrayElementsGetPrimitiveArrayCritical 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ố:

  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 đế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.