JNI概述

一、概念

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层来完成,例如setDataSourceprepare等等,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方法可以按照如下命名规范:

  1. java_附加到它的开头
  2. 按照顶级源目录文件相关的文件路径命名,使用_取代/
  3. 删掉java扩展名
  4. 在最后一个下划线后附件函数名

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层数据类型转换

基本数据类型的转换关系

javaNative字长
booleanjbooleanunsigned 8
bytejbytesigned 8
charjcharunsigned 16
shortjshortsigned 16
intjintsigned 32
longjlongsigned 64
floatjfloat32 IEEE 754
doublejdouble64 IEEE 754

这里主要要关注Java层的类型到了Native层之后大小就发生了变化,例如char在java中占8位,但是在Native中占16位

引用数据类型的转换关系

javaNative
All objectjobject
java.lang.classjbyte
java.lang.stringjstring
object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
java.lang.Throwablejthrowable
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的AttachCurrentThreadAttachCurrentThreadAsDaemon来将该线程附加到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,静态变量则是在类型之前加上StaticGetStatic<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>MethodCallStatic<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_TYPECALL_TYPE中有三种宏分别是CALL_TYPE_METHODCALL_TYPE_METHODVCALL_TYPE_METHODA,这里我们只看CALL_TYPE_METHOD,其中定义了对应的Call<Type>Method方法。最后除了Void其他类型的方法都是由这个宏来定义(可以把这些宏展开验证)。Call<Type>Method的调用主要包括三类参数,第一个是jobject也就调用该方法的对象,通常是对应的对象引用,第二个是对应的MethodId,后面是对应方法中的参数。

2.7.3、变量的操作

前面我们知道变量的获取格式一般是Get<Type>FieldGetStatic<Type>Field,那么相对应地,变量赋值地一般格式是Set<Type>FieldSetStatic<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类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L/java/lang/Stringstring
[Iint[]
[L/java/lang/objectObject[]

上面是一些常用的类型标识,如果是数组类型,标识中会有[,二维数组签名两个[,以此类推。

对于这部分信息我们可以利用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 个局部引用,因此如果您需要更多局部引用,则应该按需删除,或使用 EnsureLocalCapacityPushLocalFrame 保留更多局部引用。

注意,jfieldID 和 jmethodID 属于不透明类型,不是对象引用,且不应传递给 NewGlobalRef。函数返回的 GetStringUTFCharsGetByteArrayElements 等原始数据指针也不属于对象。(这些指针可以在线程之间传递,并且在匹配的 Release 调用完成之前一直有效。)

还有一种不寻常的情况值得单独提及。如果使用 AttachCurrentThread 来attach原生线程,那么在线程分离之前,您运行的代码不会自动释放局部引用,我们创建的任何局部引用都必须手动释放。一般来说,在循环中创建局部引用的任何Native代码可能需要执行手动释放。

要谨慎使用全局引用。全局引用不可避免,但它们很难调试,并且可能会导致难以诊断的内存(不良)行为。在所有其他条件相同的情况下,全局引用越少,解决方案的效果可能越好。

2.10、jstring的使用

Java中的String也是引用类型,但是该类型使用频率较高,因此JNI中单独创建了一个jstring类型来表示Java中的String(UTF-16)类型。在JNI中有两种方法来获取jstring对象,分别是NewStringNewStringUTF

  • 使用NewString从传入的Unicode字符串得到一个jstring对象
  • 使用NewStringUTF从传入的UTF-8字符串中得到一个jstring对象。
  • 除了上述的两种还有GetStringCharsGetStringUTFChars分别将传入的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 ThrowThrowNew指令只是在当前线程设置了异常指针,直到返回Java层后,虚拟机才会抛出这个异常并进行相应的处理。异常发生后,我们只能进行通知、返回或者资源清理的操作,Android开发者手册明确异常发生时只能调用以下函数:

  • DeleteGlobalRef
  • DeleteGlobalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

Native代码可以通过调用 ExceptionCheckExceptionOccurred 来“捕获”异常,然后使用 ExceptionClear 进行清除。像往常一样,在未经处理的情况下舍弃异常可能会出现问题。

因为没有可用于操控 Throwable 对象本身的内置函数,所以如果想要获取异常字符串,则需要找到 Throwable 类、查找 getMessage "()Ljava/lang/String;" 的方法 ID 并调用该方法;如果结果为非 NULL 值,则使用 GetStringUTFChars 获取可以传递给 printf(3) 或等效函数的内容。

参考文献

  1. https://developer.android.com/training/articles/perf-jni
  2. 《深入理解Android(卷Ⅰ)》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值