4.2 Android NDK 基础概念
1 JavaVM
和JNIEnv
JNI 定义了两个关键数据结构,JavaVM
和JNIEnv
。这两者本质上都是指向函数表指针的指针。(在 C++ 版本中,它们是具有指向函数表的指针的类,以及指向该表的每个 JNI 函数的成员函数。)JavaVM
提供了“调用接口”函数,允许您创建和销毁JavaVM
。理论上,每个进程可以有多个JavaVM
,但 Android 只允许一个。
JNIEnv
提供了大部分 JNI 功能。除了@CriticalNative
方法外,您的原生函数都会收到JNIEnv
作为第一个参数。
JNIEnv
用于线程本地存储。因此,您不能在线程之间共享JNIEnv
。如果一段代码没有其他方法获取其JNIEnv
,则应共享JavaVM
,并使用GetEnv
发现线程的JNIEnv
。(假设它有一个)
JNIEnv
和JavaVM
的 C 声明与 C++ 声明不同。jni.h
包含文件提供不同的typedef
,具体取决于它是包含在 C 还是 C++ 中。因此,在两种语言都包含的头文件中包含JNIEnv
参数是一个坏主意。(换句话说:如果你的头文件需要#ifdef __cplusplus
,如果该头文件中的任何内容引用JNIEnv
,你可能需要做一些额外的工作。)
2 jclass
、jmethodID
和jfieldID
如果要从原生代码访问对象的字段,可以执行以下操作:
- 使用
FindClass
获取类的类对象引用 - 使用
GetFieldID
获取字段的字段 ID - 使用适当的东西获取字段的内容,例如
GetIntField
同样,要调用一个方法,您首先会得到一个类对象引用,然后是一个方法 ID。这些 ID 通常只是指向内部运行时数据结构的指针。查找它们可能需要几个字符串比较,但一旦你有了它们,获取字段或调用方法的实际调用就非常快了。
如果性能很重要,那么查找一次值并将结果缓存在原生代码中是有用的。因为每个进程只能有一个JavaVM
,所以将这些数据存储在静态本地结构中是合理的。
类引用、字段 ID 和方法 ID 保证有效,直到类被卸载。只有当与ClassLoader
关联的所有类都可以被垃圾回收时,类才会被卸载,这在 Android 中很少见,但并非不可能。但是请注意,jclass
是一个类引用,必须通过调用NewGlobalRef
来保护它。
如果你想在加载类时缓存 ID,并在卸载和重新加载类时自动重新缓存它们,初始化 ID 的正确方法是在相应的类中添加一段看起来像这样的代码:
/*
* 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();
}
在 C/C++ 代码中创建一个执行 ID 查找的nativeClassInit
方法。代码将在类初始化时执行一次。如果类被卸载然后重新加载,它将再次执行。
3 局部和全局引用
传递给原生方法的每个参数以及 JNI 函数返回的几乎每个对象都是“局部引用”。这意味着它在当前线程中当前原生方法的持续时间内有效。即使对象本身在原生方法返回后继续存在,引用也是无效的。
这适用于jobject
的所有子类,包括jclass
、jstring
和jarray
。(启用扩展 JNI 检查时,运行时将警告您大多数引用错误使用。)
获取非局部引用的唯一方法是通过函数NewGlobalRef
和NewWeakGlobalRef
。
如果你想在更长的时间内保留一个引用,你必须使用“全局”引用。NewGlobalRef
函数将局部引用作为参数并返回全局引用。在您调用DeleteGlobalRef
之前,全局引用保证有效。
此模式通常用于缓存从FindClass
返回的jclass
,例如:
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
。GetStringUTFChar
和GetByteArrayElements
等函数返回的原始数据指针也不是对象。(它们可以在线程之间传递,并且在匹配的Release调用之前有效。)
一个不寻常的案例值得单独提及。如果使用AttachCurrentThread
附加原生线程,则在线程分离之前,您正在运行的代码将永远不会自动释放局部引用。您创建的任何局部引用都必须手动删除。一般来说,任何在循环中创建局部引用的原生代码都可能需要手动删除。
使用全局引用时要小心。全局引用可能是不可避免的,但它们很难调试,并可能导致难以诊断的内存(错误)行为。在其他条件相同的情况下,全局引用较少的解决方案可能更好。
4 原生库
您可以使用标准System.loadLibrary
从共享库加载原生代码。
从静态类初始化器调用System.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
。这会生成更快、更小的代码,并避免与加载到应用程序中的其他库发生潜在冲突(但如果应用程序在原生代码中崩溃,它会创建不太有用的堆栈跟踪)。
静态初始化器应该如下所示:
static {
System.loadLibrary("fubar");
}
如果用 C++ 编写,JNI_OnLoad
函数应该看起来像这样:
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_OnLoad
进行的任何FindClass
调用都将解析用于加载共享库的类加载器上下文中的类。当从其他上下文调用时,FindClass
使用与 Java 堆栈顶部的方法关联的类加载器,或者如果没有(因为调用来自刚刚附加的原生线程),则使用“系统”类加载器。系统类加载器不知道应用程序的类,因此您将无法在该上下文中使用FindClass
查找自己的类。这使得JNI_OnLoad
成为查找和缓存类的一个方便的地方:一旦你有了一个有效的jclass
全局引用,你就可以从任何连接的线程中使用它。