Mẹo về JNI

JNI là Giao diện gốc Java. Đây là cách 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 (được viết bằng C/C++). JNI không liên quan đến nhà cung cấp, có hỗ trợ tải mã từ các thư viện chia sẻ động, và đôi khi rườm rà cũng hiệu quả một cách hợp lý.

Lưu ý: Vì Android biên dịch mã byte Kotlin thành mã byte phù hợp 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ề kiến trúc JNI và 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 hiểu rõ về tính năng này, hãy đọc kỹ Thông số kỹ thuật của giao diện gốc Java để hiểu 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 sẽ không thể hiện rõ ngay trong lần đọc đầu tiên, vì vậy, bạn có thể thấy hữu ích trong một vài phần tiếp theo.

Để duyệt xem các lượt tham chiếu JNI toàn cục cũng như xem nơi các lượt tham chiếu JNI toàn cục được tạo và xoá, hãy sử dụng chế độ xem JNI heap (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

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 xem xét ở đây. Giải pháp JNI nên cố gắng tuân thủ các nguyên tắc sau (liệt kê dưới đây theo thứ tự mức độ quan trọng, bắt đầu từ yếu tố 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 so sánh trên lớp JNI có thể tốn không ít chi phí. Hãy cố gắng thiết kế một giao diện giảm thiểu lượng dữ liệu bạn cần sắp xếp và tần suất sắp xếp dữ liệu.
  • Tránh hoạt động giao tiếp không đồng bộ giữa mã viết bằng ngôn ngữ lập trình được quản lý và mã viết bằng C++ khi có thể. Việc này sẽ giúp bạn dễ dàng duy trì giao diện JNI. Thường thì 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ữ nguyên bản cập nhật không đồng bộ bằng cùng một 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, bạn nên thực hiện lệnh gọi lại giữa 2 luồng bằng ngôn ngữ lập trình Java, trong đó một trong số đó thực hiện lệnh gọi C++ chặn, sau đó 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 JNI chạm hoặc chạm. 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 duy trì hoạt động 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 ở một số ít vị trí nguồn C++ và Java dễ xác định để tạo điều kiện cho các hoạt động tái cấu trúc trong tương lai. Hãy cân nhắc sử dụng thư viện tự động tạo JNI nếu 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ỏ tới con trỏ đến bảng hàm. (Trong phiên bản C++, chúng là các lớp có một 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ỷ một 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 máy chủ.

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 được 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 lưu trữ thread-local trên máy tính. 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ử có một mã, hãy xem AttachCurrentThread bên dưới.)

Phần khai báo C của JNIEnv và JavaVM khác với các phần khai báo C++. Tệp bao gồm "jni.h" cung cấp các typedef khác nhau, tuỳ thuộc vào việc tệp đó đượ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 các tệp tiêu đề mà cả hai ngôn ngữ đư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, thì có thể bạn sẽ phải thực hiện thêm một số thao tác nếu có bất kỳ mục 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 hệ điều hành lên lịch. Các lớp này thường bắt đầu từ mã được quản lý (sử dụ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 bắt đầu bằng pthread_create() hoặc std::thread bằng các hàm AttachCurrentThread() hoặc AttachCurrentThreadAsDaemon(). Cho đến khi một luồng được đính kèm, 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 bạn nên sử dụng Thread.start() để tạo bất kỳ luồng nào cần gọi 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à đang sử dụng cùng một ClassLoader với mã Java. Việc đặt tên luồng để gỡ lỗi trong Java cũng sẽ dễ dàng hơn so với việc đặt tên cho mã gốc (xem pthread_setname_np() nếu bạn có pthread_t hoặc thread_t, và std::thread::native_handle() nếu bạn có std::thread và muốn có pthread_t).

Việc đính kèm một luồng được tạo nguyên gốc sẽ khiến đối tượng java.lang.Thread được tạo và thêm vào ThreadGroup "chính", giúp đối tượng này hiển thị với trình gỡ lỗi. Việc gọi AttachCurrentThread() trên một luồng đã đượ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, 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 thoát. Nếu việc lập trình trực tiếp việc này gây khó khăn, 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 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 thread-local-storage; theo cách đó, khoá sẽ được truyền vào hàm khởi tạo làm đối số.)

jclass, jmethodID và jfieldID

Nếu muốn truy cập vào trường của một đối tượng qua mã gốc, bạn cần làm như sau:

  • Lấy 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 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ự như vậy, để gọi một phương thức, trước tiên bạn sẽ nhận được thông tin tham chiếu đối tượng lớp rồi sau đó là mã nhận dạng phương thức. Các mã nhận dạng thường chỉ là con trỏ đến cấu trúc dữ liệu thời gian chạy nội bộ. Việc tra cứu có thể yêu cầu một vài phép so sánh chuỗi, nhưng sau khi bạn có chúng, lệnh gọi thực tế để lấy trường hoặc gọi phương thức rất nhanh chóng.

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ả trong mã gốc vào bộ nhớ đệm. Vì có giới hạn một JavaVM cho mỗi quy trình, bạn nên lưu trữ dữ liệu này trong cấu trúc cục bộ tĩnh.

Các tệp tham chiếu lớp, mã trường và mã phương thức đều được đảm bảo là hợp lệ cho đến khi lớp bị 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 đều có thể được thu thập rác. Điều này hiếm xảy ra nhưng không phải là không thể thực hiện trong Android. Tuy nhiên, hãy lưu ý rằng jclass là một 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 mã vào bộ nhớ đệm khi một lớp được tải và tự động lưu lại các mã đó vào bộ nhớ đệm nếu lớp đã từng bị huỷ tải và tải lại, thì cách đúng để khởi tạo 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 hoạt động tra cứu mã nhận dạng. Mã sẽ được thực thi một lần, khi lớp được khởi chạy. Nếu lớp đã từng bị huỷ tải và sau đó được tải lại, thì lớp đó sẽ được thực thi lại.

Tham chiếu tại địa phương 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 hàm JNI trả về đều là một "tham chiếu cục bộ". Điều này có nghĩa là giá trị này hợp lệ với 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ề, tệp tham chiếu không hợp lệ.

Chính sách này áp dụng cho mọi lớp con của jobject, bao gồm cả 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 bạn bật chế độ kiểm tra JNI mở rộng.)

Cách duy nhất để lấy các tệp 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 tham chiếu trong thời gian dài hơn, bạn phải sử dụng tham chiếu "toàn cầu". Hàm NewGlobalRef lấy tham chiếu cục bộ làm đối số và trả về một tham chiếu cục bộ. Tham chiếu chung được đảm bảo sẽ 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 tệp tham chiếu cục bộ và toàn cục làm đối số. 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. Để xem hai tham chiếu có tham chiếu đến cùng một đối tượng hay không, bạn phải sử dụng hàm IsSameObject. Không bao giờ so sánh các tệp 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à 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 giữa các lệnh gọi một phương thức. Ngoài ra, có thể 2 đố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 dùng các giá trị jobject làm khoá.

Lập trình viên cần phải "không phân bổ quá nhiều" các tham chiếu cục bộ. Trên thực tế, điều này có nghĩa là nếu đang tạo số lượng lớn lượt 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 đối tượng đó theo cách thủ công bằng DeleteLocalRef thay vì để JNI làm việc đó cho bạn. Bạn chỉ bắt buộc phải triển khai để đặ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 thế, bạn nên xoá khi di chuyển hoặc sử dụng EnsureLocalCapacity/PushLocalFrame để đặt trước thêm.

Xin lưu ý rằng jfieldIDjmethodID là các loại không rõ ràng, không phải là tham chiếu đối tượng và không được truyền vào NewGlobalRef. Con trỏ dữ liệu thô do các hàm như GetStringUTFCharsGetByteArrayElements 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 lệnh gọi Release phù hợp.)

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, thì mã bạn đang chạy sẽ không bao giờ tự động giải phóng các tệp tham chiếu cục bộ cho đến khi luồng tách ra. Mọi tham chiếu cục bộ mà 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á theo cách thủ công.

Hãy cẩn thận khi sử dụng các tham chiếu toàn cục. Tệp tham chiếu chung có thể không tránh được, nhưng rất khó gỡ lỗi và có thể gây ra những hành vi khó chẩn đoán (sai) bộ nhớ. Tất cả yếu tố khác đều như nhau, thì một giải pháp có ít lượt 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 Đã sửa đổi UTF-8. Phương thức mã hoá đã sửa đổi hữu ích cho mã C vì mã này mã hoá \u0000 dưới dạng 0xc0 0x80 thay vì 0x00. Điều tuyệt vời ở đây là bạn có thể sử dụng các chuỗi 0 kết thúc 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à hy vọng nó sẽ hoạt động chính xác.

Để nhận mã đại diện UTF-16 của String, hãy sử dụng GetStringChars. Lưu ý rằng các chuỗi UTF-16 không bị chấm dứt bằng 0 và \u0000 được phép, vì vậy, bạn cần phải tiếp tục độ dài chuỗi cũng như con trỏ jchar.

Đừng quên Release các chuỗi 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 gốc thay vì đến các tệp tham chiếu cục bộ. Các báo cáo này được đảm bảo có hiệu lực cho đến khi Release được gọi, có nghĩa là các giá trị này sẽ 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à giao dữ liệu đó cho NewStringUTF mà không lọc. Trừ phi bạn biết dữ liệu là MUTF-8 (hoặc ASCII 7 bit, là một tập hợp con tương thích), bạn sẽ cần loại bỏ các ký tự không hợp lệ hoặc chuyển đổi chúng thành dạng UTF-8 đã sửa đổi phù hợp. Nếu không, việc chuyển đổi UTF-16 có thể mang lại kết quả ngoài dự kiến. CheckJNI (được bật theo mặc định cho trình mô phỏng) 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 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 phân bổ và chuyển đổi sang UTF-8. Android 8 thay đổi cách biểu diễn String để sử dụng 8 bit mỗi ký tự cho các chuỗi ASCII (để tiết kiệm bộ nhớ) và bắt đầu sử dụng trình thu gom rác. Các tính năng này làm giảm đáng kể số lượng các 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 mà mã xử lý đều ngắn, thì trong hầu hết trường hợp, bạn có thể tránh phân bổ và giải phóng 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 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 các đối tượng mảng. Mặc dù bạn phải truy cập các mảng đối tượng mỗi lần một mục, nhưng các mảng nguyên gốc có thể được đọc và ghi trực tiếp như thể được khai báo bằng C.

Để giao diện hiệu quả nhất có thể mà không ràng buộc việc triển khai máy ảo, nhóm lệnh gọi Get<PrimitiveType>ArrayElements cho phép 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 bản sao. Dù bằng cách nào, con trỏ thô được trả về vẫn đảm bảo sẽ 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 xuống và không thể 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 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 sao chép hay chưa bằng cách truyền một con trỏ không có giá trị 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à môi trường thời gian chạy thực hiện phụ thuộc vào việc ứng dụng đó trả về một 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 chưa được ghim.
    • Sao chép: dữ liệu đượ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 đượ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. Các hoạt động ghi trước đó không bị huỷ.
    • Sao chép: vùng đệm có bản sao được giải phóng; mọi thay đổi đối với bản sao đó sẽ bị mất.

Một lý do để kiểm tra cờ isCopy là để biết liệu bạn có cần gọi Release bằng JNI_COMMIT sau khi thực hiện các thay đổi đối với một mảng hay không – nếu xen kẽ giữa việc thực hiện thay đổi và thực thi mã có sử dụng nội dung của mảng đó, thì bạn có thể bỏ qua lệnh xác nhận không hoạt động. Một lý do khác để kiểm tra cờ là để xử lý hiệu quả JNI_ABORT. Ví dụ: có thể bạn 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, sau đó huỷ 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 chuyển dữ liệu gốc, thì bạn cần tạo bản sao của riêng mình.

Đây là một lỗi phổ biến (lặp lại trong mã ví dụ) khi giả định rằng bạn có thể bỏ qua lệnh gọi Release nếu *isCopy sai. Không phải lúc này. 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 xuống và trình thu gom rác không thể di chuyển được.

Ngoài ra, xin 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 trong khu vực

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 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 các phần tử len byte đầu tiên từ mảng đó, sau đó giải phóng mảng. Tuỳ thuộc vào cách 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ó khả năng tạo bản sao thứ ba.

Bạn 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ố lợi ích như sau:

  • Yêu cầu một lệnh gọi JNI thay vì 2, giúp giảm mức hao tổn.
  • Không cần ghim hoặc sao chép thêm dữ liệu.
  • Giảm nguy cơ xảy ra lỗi lập trình viên – không có nguy cơ quên gọi Release khi xảy ra sự cố.

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 trong khi có một ngoại lệ đang chờ xử lý. Mã của bạn dự kiến sẽ phát hiện trường hợp 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ửi một trường hợp 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, thì bạn không cần kiểm tra trường hợp ngoại lệ. Tuy nhiên, nếu gọi một phương thức (sử dụng hàm như CallObjectMethod), bạn luôn phải kiểm tra xem có ngoại lệ hay không vì giá trị trả về sẽ không hợp lệ nếu hệ thống trả về một ngoại lệ.

Lưu ý rằng các ngoại lệ do mã được quản lý gửi không gỡ bỏ 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, bạn không được gửi trên ranh giới chuyển đổi JNI từ mã C++ sang mã được quản lý.) Các lệnh ThrowThrowNew của JNI chỉ đặt một con trỏ ngoại lệ trong luồng hiện tại. Khi quay lại phương thức quản lý từ mã gốc, trường hợp ngoại lệ sẽ được ghi chú và xử lý một cách thích hợp.

Mã gốc có thể "phát hiện" một trường hợp ngoại lệ bằng cách gọi ExceptionCheck hoặc ExceptionOccurred và xoá trường hợp đó bằng ExceptionClear. Như thường lệ, việc loại bỏ các ngoại lệ mà không xử lý có thể dẫn đến sự cố.

Không có hàm tích hợp nào để tự điều khiển đối tượng Throwable. Vì vậy, nếu muốn (giả sử) nhận được 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 mã đó. Nếu kết quả không phải là NULL, hãy dùng GetStringUTFChars để lấy thứ gì đó bạn có thể đưa vào printf(3) hoặc tương đương.

Chế độ kiểm tra mở rộng

JNI kiểm tra lỗi rất ít. 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 chuỗi 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: đang cố gắng phân bổ một mảng có kích thước âm.
  • Con trỏ lỗi: chuyển một jarray/jclass/jobject/jstring không hợp lệ đến lệnh gọi JNI hoặc chuyển con trỏ NULL đến lệnh gọi JNI với một đối số không thể có giá trị rỗng.
  • Tên lớp: truyền mọi giá trị ngoại trừ kiểu "java/lang/String" của tên lớp sang lệnh gọi JNI.
  • Lệnh gọi quan trọng: thực hiện lệnh gọi JNI giữa lần nhận "quan trọng" và bản phát hành tương ứng.
  • Direct ByteBuffers: chuyể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ừ một luồng không chính xác.
  • jfieldID: sử dụng jfieldID NULL hoặc sử dụng jfieldID để đặt một trường thành giá trị không đúng kiểu (cố gắng gán StringBuilder với trường Chuỗi), chẳng hạn như) hoặc sử dụng jfieldID cho một trường tĩnh để thiết lập một trường thực thể hay ngược lại hay sử dụng jfieldID từ một lớp có các phiên bản của lớp khác.
  • jmethodIDs: sử dụng không đúng loại jmethodID khi thực hiện lệnh gọi JNI Call*Method: loại 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 cho loại tệp đối chiếu không chính xác.
  • Chế độ phát hành: chuyển một chế độ phát hành không hợp lệ sang 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 loại 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ừ phương thức đã khai báo để trả về một Chuỗi).
  • UTF-8: chuyển chuỗi byte Đã sửa đổi UTF-8 không hợp lệ đến lệnh gọi JNI.

(Khả năng hỗ trợ tiếp cận của các phương thức và trường vẫn chưa được kiểm tra: 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ó 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

Ở cả hai trường hợp trên, 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 dùng 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 chạy từ thời điểm đó sẽ 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.) 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 để chỉ bật CheckJNI cho ứng dụng của mình. Xin lưu ý rằng các công cụ xây dựng Android sẽ tự động làm 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 System.loadLibrary tiêu chuẩn.

Trên thực tế, các phiên bản Android cũ hơn 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 về việc tải thư viện gốc.

Gọi System.loadLibrary (hoặc ReLinker.loadLibrary) từ trình khởi tạo lớp tĩnh. Đối số 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 bạn chỉ có một lớp có phương thức gốc, thì lệnh gọi đến System.loadLibrary nằm trong trình khởi tạo tĩnh cho lớp đó là điều hợp lý. 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.

Có 2 cách 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. Bạn có thể đăng ký rõ ràng bằng RegisterNatives hoặc có thể để môi trường thời gian chạy tra cứu mã này một cách linh động bằng dlsym. Ưu điểm của RegisterNatives là bạn được kiểm tra trước xem các biểu tượng có tồn tại hay không, ngoài ra 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ứ dữ liệu nào trừ JNI_OnLoad. Ưu điểm của việc cho phép môi trường thời gian chạy khám phá các hàm của bạn là 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ả phương thức gốc bằng RegisterNatives.
  • Hãy tạo bằng -fvisibility=hidden để chỉ xuất JNI_OnLoad từ thư viện của bạn. Điều này sẽ 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 của bạn (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 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;
}

Thay vào đó, để sử dụng tính năng "discovery" (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 phần 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 về điều đó cho đến lần đầu tiên phương thức này 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 là trình tải lớp được dùng để tải thư viện chia sẻ. 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ì FindClass 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 trong ứng dụng của bạn, vì vậy, bạn sẽ không thể tra cứu các lớp của mình bằng FindClass trong ngữ cảnh đó. Việc này khiến 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ó tài liệu tham khảo chung jclass hợp lệ, bạn có thể sử dụng nó từ bất kỳ luồng đính kèm nào.

Lệnh gọi gốc nhanh hơn với @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 độ chuyển đổi giữa mã gốc và mã được quản lý. Tuy nhiên, các chú giải 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 kỹ lưỡng trước khi sử dụng. Ở bên dưới, chúng tôi đề cập ngắn gọn những thay đổi này, 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 đố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ú giải này sẽ thay đổi ABI chuyển đổi JNI. Phương thức triển khai gốc phải loại trừ các thông số JNIEnvjclass khỏi chữ ký hàm.

Khi thực thi phương thức @FastNative hoặc @CriticalNative, việc thu thập rác không thể tạm ngưng luồng cho các công việc thiết yếu và có thể bị chặn. Không sử dụng các chú giải 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ị 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ác khoá gốc có thể bị giữ trong thời gian dài.

Các chú giải này được triển khai để sử dụng trên hệ thống kể từ Android 8 và trở thành API công khai được CTS kiểm thử 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ó sự đảm bảo mạnh mẽ của CTS), 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 bằng JNI RegisterNatives để chạy trên các phiên bản Android 8-11. Các chú giải này bị bỏ qua trên Android 7 – việc ABI không khớp đối với @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ố.

Đối với các phương thức quan trọng về hiệu suất cần các chú giải 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. Để 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 phương thức @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 có mức giá rẻ gần bằng lệnh gọi không cùng dòng trong C/C++, miễn là tất cả đối số vừa với thanh ghi (ví dụ: tối đa 8 đối số 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 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ề phiên bản 64 bit

Để hỗ trợ những kiến 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ỏ vào 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 của JNI 1.6 đều được hỗ trợ, ngoại trừ những trường hợp sau:

  • 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 trước đó, bạn có thể cần lưu ý:

  • Tra cứu động các hàm gốc

    Trước phiên bả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 quy trình đă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.

  • Xoá luồng

    Cho đến Android 2.0 (Eclair), bạn không thể sử dụng hàm khởi tạo pthread_key_create để tránh việc kiểm tra "luồng phải 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, vì vậy, đây sẽ là một cuộc đua để xem cái nào được gọi đầu tiên.)

  • Tệp đối chiếu chung yếu

    Cho đến Android 2.2 (Froyo), các tham chiếu toàn cục yếu không được triển khai. Phiên bản cũ sẽ từ chối nghiêm ngặt những nỗ lực sử dụng chúng. Bạn có thể 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 Kem Sandwich), các tệp tham chiếu yếu toàn cục chỉ có thể được chuyển đến NewLocalRef, NewGlobalRefDeleteWeakGlobalRef. (Quy cách này đặc biệt khuyến khích các lập trình viên tạo tham chiếu cứng đến các tập lệnh toàn cục yếu trước khi làm bất cứ việc gì với các tập lệnh đó. Vì vậy, việc này không hề hạn chế.)

    Từ Android 4.0 (Ice Kem Sandwich) trở lên, các tệp tham chiếu toàn cục yếu có thể được dùng như mọi tham chiếu JNI khác.

  • Nội dung tham khảo tại địa phương

    Cho đến Android 4.0 (Ice Kem Sandwich), các tệp tham chiếu cục bộ thực sự là con trỏ trực tiếp. Ice Kem 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à rất nhiều lỗi JNI không phát hiện được trên các bản phát hành cũ. Hãy xem bài viết Các thay đổi về tệp đối chiếu cục bộ của 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ộ được giới hạn ở giới hạn dành riêng cho từng phiên bản. Kể từ Android 8.0, Android hỗ trợ không giới hạn tệp tham chiếu cục bộ.

  • Xác định loại tham chiếu bằng GetObjectRefType

    Cho đến Android 4.0 (Ice Kem Sandwich), do việc sử dụng con trỏ trực tiếp (xem ở trên), bạn không thể triển khai GetObjectRefType chính xác. Thay vào đó, chúng tôi sử dụng phương pháp phỏng đoán thông qua 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, mã tham chiếu sẽ báo cáo rằng tệp đối chiếu của bạn thuộc loại cần kiểm tra. Ví dụ: nếu bạn gọi GetObjectRefType trên một jclass toàn cục giống với jclass được truyền dưới dạng một đối số ngầm ẩn cho phương thức gốc tĩnh của bạn, thì bạn sẽ nhận được JNILocalRefType thay vì JNIGlobalRefType.

  • @FastNative@CriticalNative

    Kể từ Android 7, các chú thích tối ưu hoá này đã bị bỏ qua. Việc ABI không khớp với @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@CriticalNative chưa được triển khai trong Android 8-10 và có chứa các lỗi đã biết trong Android 11. Việc sử dụng những hoạt động tối ưu hoá này mà không đăng ký rõ ràng bằng JNI RegisterNatives có thể dẫn đến sự cố trên Android 8-11.

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 gặp lỗi như sau:

java.lang.UnsatisfiedLinkError: Library foo not found

Trong một số trường hợp, điều này có nghĩa là như vậy — 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 dlopen(3) không thể mở, 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ệ.

Những lý do thường gặp khiến bạn có thể gặp phải trường hợp ngoại lệ "không tìm thấy thư viện":

  • Thư viện không tồn tại hoặc không thể truy cập vào ứng dụng. Hãy sử 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 này.
  • 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 các hàm hoặc thư viện không tồn tại trên thiết bị.

Một lớp khác của lỗi UnsatisfiedLinkError 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

Điều này có nghĩa là thời gian chạy đã cố tìm một phương thức so khớp nhưng không thành công. Một số lý do phổ biến dẫn đến điều này là:

  • Không tải được thư viện. Kiểm tra đầu ra logcat để tìm 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 thao tác tra cứu phương thức tải từng phần, việc không khai báo các hàm C++ bằng extern "C" và chế độ hiển thị thích hợp (JNIEXPORT). Hãy 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 chúng có vẻ như 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 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 phần khai báo.
    • Đối với trường hợp đăng ký rõ ràng, các lỗi nhỏ khi nhập chữ ký phương thức. Hãy đả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ớ 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 ";", dùng "/" để phân tách các tên gói/lớp và dùng "$" để phân tách các 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 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 cũng áp dụng như nhau cho các lỗi tìm phương thức bằng GetMethodID hoặc GetStaticMethodID, hoặc các trường có GetFieldID hoặc GetStaticFieldID.)

Đảm bảo 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 tìm kiếm 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, đồng thời phải gói lớp đó bằng "L" và ";", tức là mảng một chiều của String sẽ là [Ljava/lang/String;. Nếu bạn đang tìm kiếm một lớp bên trong, hãy dùng "$" thay vì ".". Nhìn chung, việc sử dụng javap trên tệp .class là cách hay để tìm 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 lưu giữ đúng cách 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 sử dụng từ JNI.

Nếu tên lớp có vẻ đúng, bạn có thể đang gặp sự cố về trình tải lớp. FindClass muốn bắt đầu quá trình tìm kiếm lớp trong trình tải lớp liên kết với mã của bạn. Công 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 liên kết với lớp Foo và sử dụng đối tượng đó.

Công cụ này thường đáp ứng mong muốn của 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 luồng đó với AttachCurrentThread). Hiện 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ẽ khởi động trong trình tải lớp "system" thay vì trình tải được liên kết với ứng dụng của bạn, vì vậy, sẽ không thể tìm các lớp dành riêng cho ứng dụng.

Dưới đây là một số cách để xử lý vấn đề này:

  • Thực hiện tra cứu FindClass một lần, trong JNI_OnLoad và lưu các tham chiếu lớp vào bộ nhớ đệm để sử dụng sau này. 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 hàm đã gọi System.loadLibrary (đây là quy tắc đặc biệt, được cung cấp để giúp quá trình 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, thì 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 thiết, bằng cách khai báo phương thức gốc để lấy đối số Lớp, sau đó truyền Foo.class vào.
  • Hãy lưu nội dung tham chiếu đến đối tượng ClassLoader vào bộ nhớ đệm ở nơi thuận tiện và trực tiếp thực hiện lệnh gọi loadClass. Bạn cần 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ể rơi vào trường hợp cần truy cập vào một vùng đệm lớn dữ liệu thô từ cả mã gốc và mã được quản lý. Một số ví dụ phổ biến bao gồm thao tác với bitmap hoặc mẫu âm thanh. Có 2 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, về phía gốc, không thể đảm bảo bạn sẽ có thể truy cập vào dữ liệu mà không phải sao chép dữ liệu đó. Trong một số phương thức triển khai, GetByteArrayElementsGetPrimitiveArrayCritical sẽ trả về con trỏ thực tế cho dữ liệu thô trong vùng nhớ khối xếp được quản lý, nhưng trong một số trường hợp khác, phương thức này sẽ phân bổ một vùng đệm trên vùng nhớ khối xếp gốc và 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. 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ư vùng đệm byte thông thường, dung lượng lưu trữ không được phân bổ trên vùng nhớ khối xếp được quản lý và luôn có thể truy cập trực tiếp qua 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 vào vùng đệm byte trực tiếp, việc truy cập dữ liệu qua mã được quản lý có thể rất chậm.

Việc lựa chọn cách dùng ứng dụng phụ thuộc vào 2 yếu tố:

  1. Hầu hết các lượt truy cập dữ liệu có xảy ra qua mã được viết bằng Java hoặc bằng C/C++ không?
  2. 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 cuối cùng dữ liệu đượ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ó quảng cáo giành chiến thắng rõ ràng, hãy sử dụng vùng đệm byte trực tiếp. Dịch vụ hỗ trợ dành cho các tính năng 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.