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()
* 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();
在执行 ID 查找的 C/C++ 代码中创建 nativeClassInit 方法。初始化类时,该代码会执行一次。如果该类被卸载,并且
然后重新加载,系统会再次执行该函数。
局部引用和全局引用
传递给原生方法的每个参数,以及 JNI 函数返回的几乎每个对象都属于“局部引用”。也就是说,该日期在
当前原生方法在当前线程中的持续时间。
即使对象本身在原生方法之后继续存在,
则引用无效。
这适用于 jobject 的所有子类,包括
jclass、jstring和jarray。
(扩展 JNI 时,运行时会针对大部分引用误用问题向您发出警告
检查是否处于启用状态。)
获取非局部引用的唯一方法是通过函数
NewGlobalRef和NewWeakGlobalRef。
如果您想将某个参考文件保留更长时间,就必须使用
“全局”参考。NewGlobalRef 函数接受
本地引用作为参数,并返回全局引用。
DeleteGlobalRef。
此模式通常在缓存返回的 jclass 时使用
来自 FindClass,例如:
jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
所有 JNI 方法都接受局部引用和全局引用作为参数。
对同一对象的引用可能具有不同的值。
例如,对同一对象连续调用 NewGlobalRef 所返回的值可能有所不同。如需查看两个引用是否引用了同一个对象,
必须使用 IsSameObject 函数。切勿在原生代码中使用 == 比较各个引用。
如果使用此符号,您就不能假设对象引用在原生代码中是常量或唯一值。在两次调用同一个方法时,表示某个对象的值可能有所不同;而在连续调用方法时,两个不同的对象可能具有相同的值。请勿使用
jobject 值作为键。
程序员需要“不过度分配”局部引用。实际上,这意味着
如果您要创建大量局部引用,也许是在运行
您应使用以下代码手动释放它们:
DeleteLocalRef,而不是让 JNI 为您执行此操作。该实现仅需要为 16 个局部引用保留槽位,因此如果您需要更多槽位,则应该按需删除,或使用 EnsureLocalCapacity/PushLocalFrame 保留更多槽位。
请注意,jfieldID 和 jmethodID 是不透明的
类型,而不是对象引用,并且不应将其传递给
NewGlobalRef。原始数据
GetStringUTFChars 等函数返回的指针
和 GetByteArrayElements 也不是对象。(这些指针可以在线程之间传递,并且在匹配的 Release 调用完成之前一直有效。)
还有一种不寻常的情况值得单独提及。如果您将原生
AttachCurrentThread 之间的线程,您运行的代码将
在线程分离之前,绝不会自动释放局部引用。您创建的任何局部引用都必须手动删除。一般来说,
在循环中创建局部引用的代码可能需要手动执行一些操作,
请谨慎使用全局引用。全局引用不可避免,但这样做非常困难
并可能导致难以诊断的内存(不良)行为。在所有其他条件均相同的情况下
全局引用越少,解决方案就越好。
UTF-8 和 UTF-16 字符串
Java 编程语言使用的是 UTF-16。为方便起见,JNI 提供了
还经过修改的 UTF-8。修改后的编码对 C 代码非常有用,因为它将 \u0000 编码为 0xc0 0x80,而不是 0x00。这有个好处,就是你可以使用以零终止的 C 样式的字符串,
且适合与标准 libc 字符串函数搭配使用。但缺点是,您无法将任意 UTF-8 数据传递给 JNI 并期望它能够正常工作。
如需获取 String 的 UTF-16 表示法,请使用 GetStringChars。
请注意,UTF-16 字符串不是以零终止的,并且允许使用 \u0000,因此您需要保留字符串长度和 jchar 指针。
不要忘记 Release 您 Get 的字符串。通过
字符串函数会返回 jchar* 或 jbyte*,
是指向原始数据而非局部引用的 C 样式指针。他们
在调用 Release 之前保证有效,也就是说,
在原生方法返回时释放。
传递给 NewStringUTF 的数据必须采用修改后的 UTF-8 格式。一种常见的错误就是从文件或网络数据流中读取字符数据,并在未过滤的情况下将其传递给 NewStringUTF。除非您确定数据是有效的 MUTF-8(或 7 位 ASCII,这是一个兼容子集),否则您需要剔除无效字符或将它们相应转换为修改后的 UTF-8 格式。如果不这样做,UTF-16 转换可能会产生意外的结果。
CheckJNI(模拟器默认处于启用状态)会扫描字符串
并在收到无效输入时中止虚拟机
在 Android 8 之前,使用 UTF-16 字符串操作的速度通常快于 Android
在 GetStringChars 中不需要副本,而
“GetStringUTFChars”需要分配和转换为 UTF-8。
Android 8 将 String 表示法更改为了每个字符使用 8 位
用于 ASCII 字符串(以节省内存),并开始使用
垃圾回收器。这些功能大大减少了 ART 出现故障的情况
可以提供指向 String 数据的指针,而无需复制,即使
价格为 GetStringCritical。不过,如果代码处理的大多数字符串
因此在大多数情况下,可以通过以下方式避免分配和取消分配
使用堆栈分配的缓冲区和 GetStringRegion 或
GetStringUTFRegion。例如:
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);
JNI 提供访问数组对象内容的函数。虽然访问对象数组时只能一次访问一个条目,但可以直接读写原始类型的数组,就像它们是在 C 语言中声明的一样。
使界面尽可能高效,同时又不会受到任何限制
虚拟机实现,Get<PrimitiveType>ArrayElements
一系列调用允许运行时返回指向实际元素的指针,或者
分配一些内存并创建一个副本。无论哪种情况,返回的原始指针
在相应的 Release 调用之前始终有效
(这意味着,如果未复制数据,则数组对象
将被固定,并且无法在压缩堆的过程中重新定位)。
您必须 Release 自己 Get 的每个数组。此外,如果 Get 调用失败,您必须确保自己的代码稍后不会试图 Release NULL 指针。
您可以通过传入
isCopy 参数的非 NULL 指针。很少出现
Release 调用接受 mode 参数,
具有三个值中的一个。运行时执行的操作取决于
返回指向实际数据还是指向数据副本的指针:
实际数据:数组对象未固定。
数据副本:已复制回数据。释放了包含相应副本的缓冲区。
JNI_COMMIT
实际数据:不执行任何操作。
数据副本:已复制回数据。包含相应副本的缓冲区
未释放。
JNI_ABORT
实际数据:数组对象未固定。较早
写入未中止。
数据副本:释放了包含相应副本的缓冲区;对该副本所做的任何更改都会丢失。
检查isCopy标志的一个原因是了解
你需要使用 JNI_COMMIT 来调用 Release
更改数组之后的效果 - 如果您在更改数组时交替使用
更改并执行使用数组内容的代码,
则跳过无操作提交检查该标志的另一个可能原因是
高效处理 JNI_ABORT。例如,您可能希望
来获取数组,进行适当修改,将部分传递给其他函数,以及
然后舍弃这些更改如果您知道 JNI 正在为
无需创建另一个“可修改”复制。如果 JNI 通过
您需要制作自己的副本。
一种常见的错误是认为您可以跳过 Release 调用(在示例代码中重复出现过这种情况)
*isCopy 为 false。事实并非如此。如果没有分配任何副本缓冲区,则必须固定原始内存,并且不能由垃圾回收器移动。
另请注意,JNI_COMMIT 标记不会释放数组,您最终需要使用其他标记再次调用 Release。
如果您只是想复制数据,使用替代方法进行 Get<Type>ArrayElements 和 GetStringChars 等调用可能非常有用。注意事项:
jbyte* data = env->GetByteArrayElements(array, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(array, data, JNI_ABORT);
这会抓取数组、将第一个 len 字节元素复制到数组之外,然后释放数组。根据
实现时,Get 调用会固定或复制数组
代码复制数据(可能是第二次),然后调用 Release;在这种情况下,JNI_ABORT 确保不会有机会进行第三次复制。
您还能够以更简单的方式完成相同操作:
env->GetByteArrayRegion(array, 0, len, buffer);
这种做法具有诸多优势:
需要一个 JNI 调用而不是两个,从而减少开销。
不需要固定或额外复制数据。
降低程序员出错风险,因为不存在操作失败后忘记调用 Release 的风险。
同样,您可以使用 Set<Type>ArrayRegion 调用将数据复制到数组中,使用 GetStringRegion 或 GetStringUTFRegion 将字符复制到 String 之外。
在异常挂起时,不得调用大多数 JNI 函数。
您的代码应该会注意到异常(通过函数的返回值 ExceptionCheck 或 ExceptionOccurred)并返回,或者清除异常并进行处理。
在异常挂起时,您只能调用以下 JNI 函数:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
许多 JNI 调用都会抛出异常,但通常会提供一种更简单的方法
检查是否失败。例如,如果 NewString 返回非 NULL 值,则无需检查异常。但是,如果要调用方法(使用 CallObjectMethod 等函数),则必须始终检查异常,因为如果系统抛出异常,返回值将无效。
请注意,托管代码抛出的异常不会展开原生堆栈帧。(并且,在 Android 上通常不建议抛出 C++ 异常,也不得在从 C++ 代码到托管代码的 JNI 过渡边界中抛出 C++ 异常。)JNI Throw 和 ThrowNew 指令仅需
在当前线程中设置异常指针。从原生代码返回到受管理代码后,这些指令会注意到异常并进行相应处理。
原生代码可以“捕捉”调用 ExceptionCheck 或
ExceptionOccurred,然后使用
ExceptionClear。像往常一样
如果舍弃异常而不处理异常,可能会导致问题。
没有用于操控 Throwable 对象的内置函数
所以,如果您想获取异常字符串,则需要
找到 Throwable 类,然后查找
getMessage "()Ljava/lang/String;",调用该函数,如果结果
是非 NULL 值,请使用 GetStringUTFChars 来获取一些
交给 printf(3) 或同等机构。
扩展的检查
JNI 很少进行错误检查。错误通常会导致崩溃。Android 还提供了一种名为 CheckJNI 的模式,其中 JavaVM 和 JNIEnv 函数表指针已切换为在调用标准实现之前执行一系列扩展的检查的函数表。
额外检查包括:
数组:尝试分配大小为负值的数组。
错误指针:将错误的 jarray/jclass/jobject/jstring 传递给 JNI 调用,或者将 NULL 指针传递给带有不可设为 null 的参数的 JNI 调用。
类名称:将类名称的“java/lang/String”样式之外的所有内容传递给 JNI 调用。
关键调用:在“关键”get 及其相应 release 之间调用 JNI。
直接字节缓冲区:将错误参数传递给 NewDirectByteBuffer。
异常:在异常挂起时调用 JNI。
JNIEnv*:使用错误线程中的 JNIEnv*。
jfieldID:使用 NULL jfieldID,或者使用 jfieldID 将字段设置为错误类型的值(例如,尝试将 StringBuilder 分配给 String 字段),或者使用静态字段的 jfieldID 设置实例字段(反之亦然),或者将一个类中的 jfieldID 与另一个类的实例搭配使用。
jmethodID:在调用 Call*Method JNI 时使用错误类型的 jmethodID:返回类型不正确、静态/非静态不匹配、“this”类型错误(对于非静态调用)或类错误(对于静态调用)。
引用:对错误类型的引用使用 DeleteGlobalRef/DeleteLocalRef。
Release 模式:将错误的 release 模式传递给 release 调用(除 0、JNI_ABORT 或 JNI_COMMIT 之外的内容)。
类型安全:从原生方法返回不兼容的类型(例如,从声明返回 String 的方法返回 StringBuilder)。
UTF-8:将无效的修改后的 UTF-8 字节序列传递给 JNI 调用。
(仍未检查方法和字段的可访问性:访问限制不适用于原生代码。)
您可以通过以下几种方法启用 CheckJNI。
如果您使用的是模拟器,CheckJNI 默认处于启用状态。
如果您使用的是已取得 root 权限的设备,则可以使用以下命令序列重新启动运行时,并启用 CheckJNI:
adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
在以上任何一种情况下,当运行时启动时,您将在 logcat 输出中看到如下内容:
D AndroidRuntime: CheckJNI is ON
如果您使用的是常规设备,则可以使用以下命令:
adb shell setprop debug.checkjni 1
这不会影响已经运行的应用,但从那时起启动的任何应用都将启用 CheckJNI。(将属性更改为任何其他值,或者只是重新启动应用都将再次停用 CheckJNI。)在这种情况下,当应用下次启动时,您将在 logcat 输出中看到如下内容:
D Late-enabling CheckJNI
您还可以将应用清单中的 android:debuggable 属性设置为
请为您的应用开启 CheckJNI。请注意,Android 构建工具会自动为
特定 build 类型。
您可以使用标准 API 从共享库加载原生代码
System.loadLibrary。
事实上,旧版 Android 的 PackageManager 中存在导致安装和
使原生库更新不可靠。ReLinker
项目提供了解决此问题和其他原生库加载问题的解决方法。
从静态类调用 System.loadLibrary(或 ReLinker.loadLibrary)
初始化函数。参数是“未修饰”的库名称,因此如需加载 libfubar.so,您需要传入 "fubar"。
如果您只有一个类具有原生方法,则调用
System.loadLibrary 位于该类的静态初始化程序中。否则
您希望从 Application 进行该调用,这样您就知道始终会加载该库,
并且始终会提前加载
运行时可以通过两种方式找到您的原生方法。您可以使用 RegisterNatives 显示注册原生方法,也可以让运行时使用 dlsym 进行动态查找。RegisterNatives 的优势在于,您可以提前
还可以检查这些符号是否存在
导出除 JNI_OnLoad 之外的任何内容。让运行时发现
因为它需要编写的代码略少一些。
如需使用 RegisterNatives,请执行以下操作:
提供 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 函数。
在 JNI_OnLoad 中,使用 RegisterNatives 注册所有原生方法。
使用版本脚本构建(首选)或使用
-fvisibility=hidden,以便只有您的 JNI_OnLoad
。这样可以生成更快、更小的代码,并避免
与加载到您的应用中的其他库发生冲突(但创建的堆栈轨迹没有多大用处)
(如果您的应用在原生代码中崩溃)。
静态初始化程序应如下所示:
Kotlin
companion object {
init {
System.loadLibrary("fubar")
static {
System.loadLibrary("fubar");
如果存在以下情况,则 JNI_OnLoad 函数应如下所示:
使用 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;
要改用“发现”您需要以特定方式为其命名(请参阅
JNI 规范
了解详情)。这意味着,如果某个方法签名是错误的,在调用
调用该方法。
从 JNI_OnLoad 进行的任何 FindClass 调用都会在用于加载共享库的类加载器上下文中解析类。从其他模式调用时
上下文,FindClass 会使用与位于
Java 堆栈,或者没有 Java 堆栈(因为调用来自刚刚附加的原生线程)
它使用“系统”类加载器。系统类加载器不知道您应用的
类,因此您无法在其中使用 FindClass 查找自己的类。
上下文。这使得 JNI_OnLoad 成为查找和缓存类的便捷位置:一旦有了有效的 jclass 全局引用,您就可以从任何附加的线程使用它。
使用 @FastNative 和 @CriticalNative 加快原生调用速度
原生方法可以带有
@FastNative 或
@CriticalNative
(但不是两者)来加快托管代码与原生代码之间的转换。不过,这些注释
包含某些行为变化,使用前需要仔细考虑。虽然我们
简要提及这些更改,请参阅相关文档了解详情。
@CriticalNative 注解只能应用于
使用受管对象(在参数或返回值中,或作为隐式 this),并且此
注解会更改 JNI 转换 ABI。原生实现必须排除
函数签名中的 JNIEnv 和 jclass 参数。
执行 @FastNative 或 @CriticalNative 方法时,垃圾信息
集合无法挂起线程以进行基本工作,并且可能会阻塞。请勿使用
为长时间运行的方法添加注解,包括通常很快但一般不受限制的方法。
特别是,代码不应执行重要的 I/O 操作,也不应该获取原生锁,
可能会长时间保留
从 Android 8 开始,这些注解已实现供系统使用,并在 Android 14 中成为经过 CTS 测试的公共 API。这些优化可能也适用于 Android 8-13 设备(尽管
没有强有力的 CTS 保证),但只有
Android 12 及更高版本,必须向 JNI RegisterNatives 明确注册
。在 Android 7 及更高版本中,系统会忽略这些注解;@CriticalNative 的 ABI 不匹配会导致参数封装错误,并且可能会崩溃。
对于需要这些注解且对性能要求苛刻的方法,强烈建议您使用 JNI RegisterNatives 显式注册这些方法,而不是依赖于基于名称的“发现”原生方法。为获得最佳应用启动性能,建议
在@FastNative@CriticalNative
基准配置文件。从 Android 12 开始
从经过编译的托管方法调用 @CriticalNative 原生方法几乎与
只要所有参数都能放入寄存器中(例如,最多
在 arm64 上为 8 个整数和最多 8 个浮点参数)。
有时,最好将原生方法一分为二,
另一个函数处理慢情况。例如:
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)
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);
在 Android 2.0 (Eclair) 之前,无法使用 pthread_key_create
析构函数,以避免“必须先分离线程
退出”检查。(运行时还会使用 pthread key 析构函数,
因此会找出首先调用哪个函数。)
弱全局引用
在 Android 2.2 (Froyo) 之前,没有实现弱全局引用。
旧版本会强烈排斥使用弱全局引用。您可以使用
Android 平台版本常量以测试支持情况。
在 Android 4.0 (Ice Cream Sandwich) 之前,弱全局引用只能
传递给 NewLocalRef、NewGlobalRef 和
DeleteWeakGlobalRef。(该规范强烈建议程序员在处理弱全局之前对其创建硬引用,因此这不应该有任何限制。)
从 Android 4.0 (Ice Cream Sandwich) 开始,弱全局引用可以像任何其他 JNI 引用一样使用。
在 Android 4.0 (Ice Cream Sandwich) 之前,局部引用一直
直接指针Ice Cream Sandwich 添加了间接
但这也意味着
的 JNI bug 在旧版本中无法检测到。请参阅
<ph type="x-smartling-placeholder"></ph>
ICS 中的 JNI 本地引用更改。
在 Android 8.0 之前的 Android 版本中,局部引用的数量上限取决于版本特定的限制。从 Android 8.0 开始,Android 支持无限制的局部引用。
使用 GetObjectRefType 确定引用类型
Android 4.0 (Ice Cream Sandwich) 之前,由于使用
直接指针(见上文),则无法实现
GetObjectRefType正确。我们改为采用启发法,即按顺序查看弱全局表、参数、局部表和全局表。它首次发现您的
直接指针,则系统会报告您的引用属于
都有哪些问题例如,这意味着
您对之前发生的全局 jclass 调用了 GetObjectRefType,
与作为隐式参数传递给静态变量的 jclass 相同
原生方法,会得到 JNILocalRefType,而不是
JNIGlobalRefType。
@FastNative 和 @CriticalNative
在 Android 7 及之前,这些优化注解已被忽略。@CriticalNative 的 ABI 不匹配会导致参数封装错误,并且可能会发生崩溃。
动态查找 @FastNative 和
@CriticalNative 方法在 Android 8-10 中未实现,
包含 Android 11 中的已知 bug。使用这些优化措施
通过 JNI 明确注册 RegisterNatives 可能会导致
会导致崩溃。
FindClass 抛出 ClassNotFoundException
为了向后兼容,Android 会抛出 ClassNotFoundException
而不是 NoClassDefFoundError,因为没有
FindClass。此行为与 Java 反射 API 一致
Class.forName(name)。
其他类的 UnsatisfiedLinkError 失败消息如下所示:
java.lang.UnsatisfiedLinkError: myfunc
at Foo.myfunc(Native Method)
at Foo.main(Foo.java:10)
在 logcat 中,您将看到以下内容:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
这意味着,运行时试图查找匹配方法但未成功。造成此问题的一些常见原因如下:
库未加载。请检查 logcat 输出,以获取有关库加载的消息。
名称或签名不匹配,因此找不到该方法。这个
通常由以下原因引起:
<ph type="x-smartling-placeholder"></ph>
- 对于延迟方法查找,无法声明 C++ 函数
包含
extern "C" 和适当的
可见性 (JNIEXPORT)。请注意,在投放“Ice Cream”之前
Sandwich,JNIEXPORT 宏不正确,因此请使用带有
旧的jni.h将无法使用。
您可以使用 arm-eabi-nm
即可看到符号在库中显示的符号;如果它们看
损坏(诸如_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass之类的内容)
而不是 Java_Foo_myfunc),或者如果符号类型是
小写“t”而不是大写的“T”,则需要
- 对于显式注册,在输入
方法签名。请确保传递给注册调用的内容与日志文件中的签名匹配。请注意,“B”是指
byte,“Z”是指 boolean。
签名中的类名称组成部分以“L”开头,以“;”结尾,
使用“/”分隔软件包/类名称,然后使用“$”来分隔
内部类名称(比如 Ljava/util/Map$Entry;)。
使用 javah 自动生成 JNI 标头可能会有帮助
可以避免一些问题。
常见问题解答:为什么 FindClass 找不到我的类?
(以下建议的大部分内容同样适用于找不到方法的问题
包含 GetMethodID、GetStaticMethodID 或字段
使用 GetFieldID 或 GetStaticFieldID。)
确保类名称字符串的格式正确无误。JNI 类名称以软件包名称开头,并用斜线分隔,例如 java/lang/String。如果您要查找数组类,
您需要以适当数量的方括号开头
还必须使用“L”封装类和“;”这样的一个一维数组,
String 为 [Ljava/lang/String;。
如果您要查找内部类,请使用“$”而不是“.”。通常,在 .class 文件上使用 javap 是查找类的内部名称的好方法。
如果要启用代码缩减,请确保
配置要保留的代码。正在配置
适当的保留规则非常重要,因为代码压缩器可能会从别处移除类、方法
或仅通过 JNI 使用的字段。
如果类名称形式正确,则可能是您遇到了类加载器问题。FindClass想要在
与代码关联的类加载器。它会检查调用堆栈,如下所示:
Foo.myfunc(Native Method)
Foo.main(Foo.java:10)
最顶层的方法是 Foo.myfunc。FindClass
查找与 Foo 关联的 ClassLoader 对象
类并使用该类。
采用这种方法通常会完成您想要执行的操作。如果您
自行创建线程(可能通过调用 pthread_create)
,然后使用 AttachCurrentThread 附加该映像)。现在
不是来自应用的堆栈帧。
如果您从此线程调用 FindClass,
JavaVM 将在“system”下启动类加载器,而不是
与您的应用关联,因此尝试查找特定于应用的类将失败。
您可以通过以下几种方法来解决此问题:
FindClass
JNI_OnLoad,并缓存类引用以供日后使用
。执行过程中进行的任何 FindClass 调用
JNI_OnLoad 将使用与
调用 System.loadLibrary 的函数(这是
特殊规则,以方便执行库初始化)。
如果您的应用代码要加载库,请FindClass
将使用正确的类加载器。
- 将类的实例传递到
方法是声明原生方法接受 Class 参数
然后传入
Foo.class。
- 在某处缓存对
ClassLoader 对象的引用
然后直接发出 loadClass 调用。这需要
不费吹灰之力
另一种方法是将数据存储在直接字节缓冲区中。这些
可使用 java.nio.ByteBuffer.allocateDirect 创建,或
JNI NewDirectByteBuffer 函数。不同于普通
字节缓冲区,因此存储空间不会在托管堆上分配,并且可以
始终直接从原生代码中访问(获取
与 GetDirectBufferAddress 相关联)。取决于
已实现字节缓冲区访问,从托管代码访问数据
可能会非常慢
选择使用哪种方法取决于以下两个因素:
- 大部分数据访问是通过使用 Java 编写的代码进行的吗
还是用 C/C++ 开发?
- 如果数据最终传递到系统 API,以
应该在其中吗?(例如,如果数据最终传递到
函数,它接受一个 byte[] 值,直接
ByteBuffer 可能不太明智。)
如果这两种方法不分伯仲,请使用直接字节缓冲区。为他们提供的支持
直接内置在 JNI 中,性能应该会在未来版本中得到提升。
[[["易于理解","easyToUnderstand","thumb-up"],["解决了我的问题","solvedMyProblem","thumb-up"],["其他","otherUp","thumb-up"]],[["没有我需要的信息","missingTheInformationINeed","thumb-down"],["太复杂/步骤太多","tooComplicatedTooManySteps","thumb-down"],["内容需要更新","outOfDate","thumb-down"],["翻译问题","translationIssue","thumb-down"],["示例/代码问题","samplesCodeIssue","thumb-down"],["其他","otherDown","thumb-down"]],["最后更新时间 (UTC):2025-10-10。"],[],[]]