文章目录
一、概念
JNI(Java Native Interface),Java本地调用,它是一种技术主要做到以下两点:
1、Java程序中的函数可以调用Native代码编写的函数(C/C++)
2、Native程序中的函数可以调用Java程序中的函数
对于Android平台来说,JNI提供了一种将Android编译器通过Java/Kotlin编写的字节码与Native代码(C/C++)交互的方式。
二、JNI实例
这里我们从Audio模块的Media实例来说明
frameworks/base/media/java/android/media/MediaPlayer.java
frameworks/base/media/jni/android_media_MediaPlayer.cpp
frameworks/base/core/jni/AndroidRuntime.cpp
libnativehelper/include/nativehelper/JNIHelp.h
2.1、 概述
MediaPlayer中有些函数需要Native层来完成,例如setDataSource
、prepare
等等,MediaPlayer在JNI层对应的是libmedia_jni.so,其中libmedia_jni是JNI库的名字,Android平台上的JNI库基本上都采用lib+模块名_jni.so的命名方式。(此部分信息可以在frameworks/base/media/jni/Android.bp中看到)
上述可知,当前有三部分,分别是Java层的MediaPlayer,JNI层的libmeidia_jni.so,Native层的libmedia.so,MediaPlayer通过libmedia_jni.so与libmedia.so进行交互,三部分共同合作来完成MediaPlayer的功能。
同时JNI层必须由动态库来实现运行时加载。
2.2、MediaPlayer
->MediaPlayer.java
public class MediaPlayer extends PlayerBase
implements SubtitleController.Listener
, VolumeAutomation
, AudioRouting
{
...
static {
/*
加载对应的JNI库,media_jni是JNI库的名字,在实际加载的时候会拓展成libmeidia_jni.so,在windows平台会拓展成media_jni.dll.
*/
System.loadLibrary("media_jni");
native_init();
}
...
// native关键字,表示这个函数由Native层来实现
private static native final void native_init();
private native void _setDateSource(FileDescriptor fd, long offset, long length)
throws IOException, IllegalArgumentException, IllegalStateException;
}
这里利用java中的静态初始化块逻辑(即在类第一次加载的时候,就会将静态字段初始化)来加载JNI库,这里我们可以看出,要使用JNI技术,主要分为两步:1、加载对应的JNI库;2、对于要调用Native层的函数,将其用native关键字声明
2.3、JNI层的MediaPlayer分析
MediaPlayer的JNI层代码在android_media_MediaPlayer.cpp中
->android_media_MediaPlayer.cpp
static void
android_media_MeidaPlayer_native_init(JNIEnv *env){
...
}
static void android_media_MediaPlayer_setDataSourceFD(JNIEnv *env, jobject thiz, jobject fileDescription, jlong offset, jlong length){
...
}
2.4、JNI方法的注册
JNI方法名实际上就是JNI方法的全路径名,例如native_init函数的全路径名是android.meida.MediaPlayer.native_init,于是".“被替换为”_"(如果原函数中有“_”,则会被替换成“_l”),其对应的Native函数名为android_media_MediaPlayer_native_init
,这是安卓JNI方法的命名规范,对于我们自己定义的JNI方法可以按照如下命名规范:
- 将
java_
附加到它的开头 - 按照顶级源目录文件相关的文件路径命名,使用
_
取代/
- 删掉
java
扩展名 - 在最后一个下划线后附件函数名
JNI方法的注册主要分为两种:1、静态注册和动态注册。
2.4.1、静态注册
静态注册实际上就是按照我们上述的命名规则来查找对应的JNI方法,即当Java层调用native_init函数时,他会在对应的JNI库中查找android_media_MediaPlayer_native_init
函数,如果没有就会报错;如果找到就会为native_init
函数与android_media_MediaPlayer_native_init
之间建立关联关系,具体就是虚拟机保存这个函数的函数指针,后续调用该函数就直接使用这个函数指针。
优点:
缺点:
1、对于每个包含native函数Java类都要生成对应的头文件
2、编写麻烦
3、首次建立关联关系需要遍历JNI层函数,耗时较长
2.4.1、动态注册
利用JNINativeMethod结构体实现,通过定义一个JNINativeMethod数组,数组成员就是Java层和Native函数的一一对应关系
->android_media_MediaPlayer.cpp
typedef struct {
const char* name; // java中native函数的函数名
const char* signature; // 对应的函数签名
void* fnPtr; // JNI层对应的函数指针
}JNINativeMethod;
static const JNINativeMethod gMethods[] = {
{"_setDataSource",
"(Ljava/io/FileDescriptor;JJ)V",
(void*)android_media_MediaPlayer_setDataSourceFD},
{"_setDataSource", "(Landroid/media/MediaDataSource;)V", (void*)android_media_MediaPlayer_setDataSourceCallback},
{"native_init", "()V", (void*)android_media_MediaPlayer_native_init}
}
// This function only registers the native methods
static int register_android_media_MediaPlayer(JNIEnv* env) {
// 调用AndroidRuntime::registerNativeMethods方法,其中第二个参数指明了Java层所属的类
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
->AndroidRuntime.cpp
/*
* Register native methods using JNI
*/
/*static*/ int AndroidRuntime::registerNativeMethosd(JNIEnv *env, const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
->JNIHelp.h
/*
* Register ont or more native methods with a particular class, "className" look like
* "java/lang/String". Abort on failure, return 0 on success
*/
[[maybe_unused]] static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* methods,
int numMethods){
jclass clazz = env->findClass(className);
// 调用JNIEnv的RegisterNatives完成注册,返回0以外的数字即失败
int result = env->RegisterNatives(clazz, methods, numMethods);
env->DeleteLocalRef(clazz);
if(result == 0) {
return 0;
}
return result;
}
由上述可知,这里主要实现函数其实就是jniRegisterNativeMethods
,其中JNINativeMethod
中使用的是函数名,所以className指明了是哪个类。
那么是在什么时候/哪里完成这个注册步骤的呢,实际上java层通过System.loadLibrary
加载完JNI动态库之后,紧接着会查找该库中的JNI_OnLoad
函数,如果有就调用它完成动态注册。所以如果想完成动态注册就必须要实现JNI_OnLoad
函数。
->android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVm* vm, void* /*reserved*/){
JNIEnv* env = NULL;
jint result = -1;
if(vm->GetEnv((void*) &env, JNI_VERSION_1_4) != JNI_OK){
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
...
if(register_android_media_mediaPlayer(env) < 0){
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
...
/*success -- return valid version number*/
result = JNI_VERSION_1_4;
bail:
return result;
}
从这个文件中我们可以看到很多其他类的注册方法,看来media的模块的JNI注册方法大多在此处完成注册加载。如果注册成功则返回合法的版本号(JNI_VERSION_1_4),如果失败就返回-1;
2.5、JNI层和Java层数据类型转换
基本数据类型的转换关系
java | Native | 字长 |
---|---|---|
boolean | jboolean | unsigned 8 |
byte | jbyte | signed 8 |
char | jchar | unsigned 16 |
short | jshort | signed 16 |
int | jint | signed 32 |
long | jlong | signed 64 |
float | jfloat | 32 IEEE 754 |
double | jdouble | 64 IEEE 754 |
这里主要要关注Java层的类型到了Native层之后大小就发生了变化,例如char在java中占8位,但是在Native中占16位
引用数据类型的转换关系
java | Native |
---|---|
All object | jobject |
java.lang.class | jbyte |
java.lang.string | jstring |
object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
java.lang.Throwable | jthrowable |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
除了基本数据类似,其他对象数据类型在JNI中都用jobject
表示,在使用的时候再进行强制类型转换。
看一下setDataSource
在Java层和Native层的定义差异
/*
Native层相较于Java层多出了两个参数,分别是JNIEnv和jobject
其中jobject表示MediaPlayer对象,如果这个函数是static函数,那么这个参数类型将是jclass
*/
private native void _setDateSource(FileDescriptor fd, long offset, long length)
throws IOException, IllegalArgumentException, IllegalStateException;
static void android_media_MediaPlayer_setDataSourceFD(JNIEnv *env, jobject thiz, jobject fileDescription, jlong offset, jlong length){
2.6、JavaVM和JNIEnv
JavaVM和JNIEnv是JNI层的两个关键数据结构,其本质上都是指向函数表指针的指针,理论上来说一个进程可以拥有多个JavaVM,但是在Android中一个进程只允许拥有一个JavaVM。
JNIEnv是一个与线程相关的代表JNI环境的结构体,所有的JNI函数均将其作为第一个参数,其中提供了一系列JNI系统函数,通过这些函数我们可以调用Java的函数,操作object对象。
一个线程只有一个JNIEnv,存储在对应线程的本地存储中,不同线程之间不可以互相访问彼此的JNIEnv结构体。我们知道当Java层访问JNI层的时候JNI_OnLoad会传入对应的JNIEnv,那么当Native层回调JNI层如何获得本线程的JNIEnv呢,这里就需要使用JavaVM,通过调用JavaVM的AttachCurrentThread
或AttachCurrentThreadAsDaemon
来将该线程附加到JavaVM。在此之前,线程不包含任何JNIEnv,也无法调用JNI层),此附加操作会创建java.lang.Thread
对象并将其添加到“主”ThreadGroup
,最后通过JavaVM的GetEnv
方法来得到JNIEnv。注意通过此方式附加的线程,在退出之前需要调用DetachCUrrentThread
来释放对应的资源。
2.7、通过JNIEnv操作jobject
2.7.1、操作的一般步骤
一个对象主要包括两个部分:成员变量、成员方法,所以通过JNIEnv操作jobject的本质就是如何通过JNIEnv操作jobject的成员变量和成员方法,主要有以下三个步骤:
- 使用
FindClass
获取类对象引用 - 使用
GetFieldID
获取字段的字段ID - 使用适当函数获取字段的内容,
Get<Type>Field
例如GetIntField
,静态变量则是在类型之前加上Static
,GetStatic<Type>Field
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
/*
*clazz:对应的java类
*name:成员函数或成员变量的名字
*sig:对应的成员函数和成员变量的签名信息
*/
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
调用方法的一般步骤是先获取类引用对象,再获取方法ID,方法ID通常只是指向内部运行时数据结构的指针。由于获取方法ID需要若干次的字符串匹配,每次查找方法都要经过这步显然是不合理的,我们查找到这部分值可以将其存储在静态本地数据结构中。这部分获取的类引用、字段ID和方法ID在取消类引用之前保证有效,在取消类加载之后重新加载类时我们需要重新获取这部分值。这部分使用方法我们可以再回到android_media_MediaPlayer_native_init
函数中
struct fields_t {
jfieldID context;
jfieldID surface_texture;
jmethodID post_event;
jmethodID proxyConfigGetHost;
jmethodID proxyConfigGetPort;
jmethodID proxyConfigGetExclusionList;
};
static fields_t fields;
static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaPlayer");
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
fields.surface_texture = env->GetFieldID(clazz, "mNativeSurfaceTexture", "J");
env->DeleteLocalRef(clazz);
...
}
这里使用了一个静态结构体fields_t
来保存这部分信息。这里主要是获取,接下来看如何使用这部分信息。
2.7.2、函数的调用
函数调用的一般格式是Call<Type>Method
或CallStatic<Type>Method
,这里Type为对应的返回值类型
->jni.h
#define CALL_TYPE_METHOD(_jtype, _jname) \
_jtype Call##_jname##Method(jobject obj, jmethodID methodID, ...) \
{ \
_jtype result; \
va_list args; \
va_start(args, methodID); \
result = functions->Call##_jname##MethodV(this, obj, methodID, \
args); \
va_end(args); \
return result; \
}
#define CALL_TYPE(_jtype, _jname) \
CALL_TYPE_METHOD(_jtype, _jname) \
CALL_TYPE_METHODV(_jtype, _jname) \
CALL_TYPE_METHODA(_jtype, _jname)
CALL_TYPE(jobject, Object)
CALL_TYPE(jboolean, Boolean)
CALL_TYPE(jbyte, Byte)
CALL_TYPE(jchar, Char)
CALL_TYPE(jshort, Short)
CALL_TYPE(jint, Int)
CALL_TYPE(jlong, Long)
CALL_TYPE(jfloat, Float)
CALL_TYPE(jdouble, Double)
void CallVoidMethod(jobject obj, jmethodID methodID, ...)
{
va_list args;
va_start(args, methodID);
functions->CallVoidMethodV(this, obj, methodID, args);
va_end(args);
}
void CallVoidMethodV(jobject obj, jmethodID methodID, va_list args)
{ functions->CallVoidMethodV(this, obj, methodID, args); }
void CallVoidMethodA(jobject obj, jmethodID methodID, const jvalue* args)
{ functions->CallVoidMethodA(this, obj, methodID, args); }
jni.h
中定义了一个宏CALL_TYPE
,CALL_TYPE
中有三种宏分别是CALL_TYPE_METHOD
、CALL_TYPE_METHODV
、CALL_TYPE_METHODA
,这里我们只看CALL_TYPE_METHOD
,其中定义了对应的Call<Type>Method
方法。最后除了Void
其他类型的方法都是由这个宏来定义(可以把这些宏展开验证)。Call<Type>Method
的调用主要包括三类参数,第一个是jobject
也就调用该方法的对象,通常是对应的对象引用,第二个是对应的MethodId,后面是对应方法中的参数。
2.7.3、变量的操作
前面我们知道变量的获取格式一般是Get<Type>Field
或GetStatic<Type>Field
,那么相对应地,变量赋值地一般格式是Set<Type>Field
或SetStatic<Type>Field
->jni.h
void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
{ functions->SetObjectField(this, obj, fieldID, value); }
void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)
{ functions->SetObjectArrayElement(this, array, index, value); }
与方法调用不同,变量的获取都是写明的,具体可以自行查看jni.h
,其中除了基本类型的获取还有一部分引用数据类型的获取。
2.8、JNI类型签名
前面我们可以看到方法注册的时候需要传入对应的类型签名,例如(Ljava/lang/Object;IIILjava/lang/Object;)V
,签名由对应函数的参数类型和返回类型信息组成,这是由于Java的方法重载机制,需要我们传入这部分信息来确定具体函数。这个签名通常的格式是
(参数1类型标识参数2类型标识…参数n类型标识)返回值类型标识
其中当参数类型为引用类型时,其格式是L包名
,其中包名中的“.”需要替换成“/”
类型标识 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L/java/lang/String | string |
[I | int[] |
[L/java/lang/object | Object[] |
上面是一些常用的类型标识,如果是数组类型,标识中会有[
,二维数组签名两个[
,以此类推。
对于这部分信息我们可以利用javap工具来帮助生成签名信息
javap -s -p xxx
其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息。
2.9、JNI中的引用
Java层传递给Native层的每个参数,包括JNI方法返回的每个对象,都是局部引用,这就意味着该引用只在当前Native方法运行期间有效,当Native方法返回之后,即使对象本身继续存在,该应用也无效。这就需要使用到JNI中的三种引用:
- Local Reference:本地引用,使用
env->NewLocalRef
。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的object和JNI层函数创建的jobject。Local Reference的最大特点在于一旦JNI层函数返回,这些jobject就可能被垃圾回收。 - Global Reference:全局引用,使用
env->NewGlobalRef
。这种对象如果不主动释放,它永远不会被垃圾回收。 - Weak Global Reference:弱全局引用,使用
env->NewWeakGloablRef
。一种特殊的Global Reference,在运行期间可能被垃圾回收,所以在使用之前需要调用JNIEnv的IsSameObject
判断是否被回收。
所有 JNI 方法都接受局部引用和全局引用作为参数。对同一对象的引用可能具有不同的值。例如,对同一对象连续调用 NewGlobalRef
所返回的值可能有所不同。如需了解两个引用是否引用同一对象,必须使用 IsSameObject
函数。切勿在原生代码中使用"==" 比较各个引用。
如果使用引用,我们就不能认为对象引用在Native代码中是不变的。在两次调用同一个方法时,表示某个对象的 32 位值可能有所不同;而在连续调用方法时,两个不同的对象可能具有相同的 32 位值,同时勿将 jobject
值用作键。
我们需要“不过度分配”局部引用,意思就是如果我们要创建大量局部引用,使用完之后,应该使用 DeleteLocalRef
手动释放它们。正常来说会保留为 16 个局部引用,因此如果您需要更多局部引用,则应该按需删除,或使用 EnsureLocalCapacity
或PushLocalFrame
保留更多局部引用。
注意,jfieldID 和 jmethodID 属于不透明类型,不是对象引用,且不应传递给 NewGlobalRef
。函数返回的 GetStringUTFChars
和 GetByteArrayElements
等原始数据指针也不属于对象。(这些指针可以在线程之间传递,并且在匹配的 Release 调用完成之前一直有效。)
还有一种不寻常的情况值得单独提及。如果使用 AttachCurrentThread
来attach原生线程,那么在线程分离之前,您运行的代码不会自动释放局部引用,我们创建的任何局部引用都必须手动释放。一般来说,在循环中创建局部引用的任何Native代码可能需要执行手动释放。
要谨慎使用全局引用。全局引用不可避免,但它们很难调试,并且可能会导致难以诊断的内存(不良)行为。在所有其他条件相同的情况下,全局引用越少,解决方案的效果可能越好。
2.10、jstring的使用
Java中的String也是引用类型,但是该类型使用频率较高,因此JNI中单独创建了一个jstring类型来表示Java中的String(UTF-16)类型。在JNI中有两种方法来获取jstring对象,分别是NewString
和NewStringUTF
。
- 使用
NewString
从传入的Unicode字符串得到一个jstring对象 - 使用
NewStringUTF
从传入的UTF-8字符串中得到一个jstring对象。 - 除了上述的两种还有
GetStringChars
和GetStringUTFChars
分别将传入的jstring对象分别分解成对应的Unicode字符串和UTF-8字符串。 - 与其他引用对象类似,在使用完之后要采用对应的
Release
方法来释放对应的资源。由于这部分函数会返回jchar*
或jbyte*
,它们是指向原始数据而非局部引用的 C 样式指针。这些指针在调用Release
之前保证有效,这意味着在Native方法返回时不会释放这些指针。
UTF-16与UTF-8的主要区别在于,UTF-16不是以0为字符串终止符,所以我们在使用NewString
来构造对应的jstring对象的时候需要传入jchar
指针和对应的字符串长度,使用时要注意二者之间的区别。
2.11、JNI中的异常处理
JNI中的异常处理与其他异常处理不同,当JNI中函数产生异常时,我们不能调用大部分JNI函数来对异常进行处理,此时不能中断本地函数的运行。JNI Throw
和ThrowNew
指令只是在当前线程设置了异常指针,直到返回Java层后,虚拟机才会抛出这个异常并进行相应的处理。异常发生后,我们只能进行通知、返回或者资源清理的操作,Android开发者手册明确异常发生时只能调用以下函数:
- DeleteGlobalRef
- DeleteGlobalRef
- DeleteWeakGlobalRef
- ExceptionCheck
- ExceptionClear
- ExceptionDescribe
- ExceptionOccurred
- MonitorExit
- PopLocalFrame
- PushLocalFrame
- Release<PrimitiveType>ArrayElements
- ReleasePrimitiveArrayCritical
- ReleaseStringChars
- ReleaseStringCritical
- ReleaseStringUTFChars
Native代码可以通过调用 ExceptionCheck
或 ExceptionOccurred
来“捕获”异常,然后使用 ExceptionClear
进行清除。像往常一样,在未经处理的情况下舍弃异常可能会出现问题。
因为没有可用于操控 Throwable 对象本身的内置函数,所以如果想要获取异常字符串,则需要找到 Throwable 类、查找 getMessage "()Ljava/lang/String;"
的方法 ID 并调用该方法;如果结果为非 NULL 值,则使用 GetStringUTFChars
获取可以传递给 printf(3)
或等效函数的内容。
参考文献
- https://developer.android.com/training/articles/perf-jni
- 《深入理解Android(卷Ⅰ)》