攻略大全
1. 粘贴攻略
1.1 JNI
JNI是Java Native Interface的缩写,译为Java本地接口,是Java与其他语言通信的桥梁。当出现一些用Java无法处理的任务时,开发人员就可以使用JNI技术来完成。一般来说主要有以下情况需要用到JNI技术:
-
需要调用Java语言不支持的依赖于操作系统平台特性的一些功能。例如:需要调用当前的UNIX系统的某个功能,而Java不支持这个功能,就需要用到JNI技术来实现。
-
为了节省程序的运行时间,必须采用其他语言(比如C/C++语言)来提升运行效率。例如:游戏、音视频开发涉及的音视频编解码和图像绘制需要更快的处理速度。
JNI在Android中的应用场景也十分广泛,主要有音视频开发、热修复和插件化、逆向开发、系统源码调用等。为了更方便地使用JNI技术,Android还提供了NDK这个工具集合,NDK开发是基于JNI的,它和JNI开发本质上并没有区别,理解了JNI原理,NDK开发也会很容易掌握。
1.2 NDK
NDK是Android所提供的一个工具集合,通过NDK可以在Android中更加方便地通过JNI来访问本地代码,比如C或者C++。NDK还提供了交叉编译器,开发人员只需要简单地修改mk文件就可以生成特定CPU平台的动态库。使用NDK有如下好处:
- (1)提高代码的安全性。由于so库反编译比较困难,因此NDK提高了Android程序的安全性。
- (2)可以很方便地使用目前已有的C/C++开源库。
- (3)便于平台间的移植。通过C/C++实现的动态库可以很方便地在其他平台上使用。
- (4)提高程序在某些特定情形下的执行效率,但是并不能明显提升Android程序的性能。
2. 造火箭攻略
3. 拧螺丝攻略
3.1 系统源码中的JNI
通过JNI,Java世界的代码就可以访问Native世界的代码,同样地,Native世界的代码也可以访问Java世界的代码。
3.2 MediaRecorder框架中的JNI
如上图所示,Java Framework层对应的是MediaRecorder.java,也就是我们在应用开发中直接调用的类。JNI层对应的是libmedia_jni.so,可以看到这个动态库的名称含有“_jni”,这说明它是一个JNI的动态库。Native层对应的是libmedia.so,这个动态库完成了实际的调用的功能。
3.2.1 Java Framework层的MediaRecorder
查看MediaRecorder.java的源码,截取部分和JNI有关的源码如下所示:
- 在静态代码块中首先调用了注释1处的代码,用来加载名为media_jni的动态库,也就是libmedia_jni.so。
- 接着调用注释2处的native_init方法,其内部会调用Native方法,用来完成JNI的注册。
- 注释3处的native_init方法用native来修饰,说明它是一个native方法,表示由JNI 来实现。 MediaRecorder的start 方法同样也是一个native方法。对于Java Framework层来说只需要加载对应的JNI库,接着声明native方法就可以了,剩下的工作由JNI层来完成。
3.2.2 JNI层的MediaRecorder
MediaRecorder的JNI层由android_media_recorder.cpp实现,native方法native_init和start的JNI层实现如下所示:
android_media_MediaRecorder_native_init 方法是native_init 方法在JNI层的实现,android_media_MediaRecorder_start方法则是start方法在JNI层的实现。
那么native_init方法是如何找到对应的android_media_MediaRecorder_native_init方法的呢?这就需要了解JNI方法注册的知识。
3.2.3 Native方法注册
Native方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。
3.2.3.1 静态注册
在Android Studio中新建一个Java Library,命名为media,这里仿照系统的MediaRecorder.java,写一个简单的MediaRecorder.java,如下所示:
对MediaRecorder.java进行编译和生成JNI方法,进入项目的media/src/main/java目录中执行如下命令:
第二个命令会在当前目录(media/src/main/java)中生成com_example_MediaRecorder.h文件,如下所示:
-
native_init()方法被声明为注释1处的方法,也就是Java_com_example_MediaRecorder_native_1init。
-
以“Java”开头说明是在Java平台中调用JNI方法的
-
com_example_MediaRecorder_native_1init指的是包名+类名+方法名的格式,原本在Java中应该是以“.”来进行分割,这里却用了“_”,这是因为在Native 语言中“.”有特殊的含义。
-
注释1处的方法名还多了一个“1”,这是因为Java的native_init方法中包含了“_”,转换成JNI方法后会变成“_1”。
-
JNIEnv是Native世界中Java环境的代表,通过JNIEnv*指针就可以在Native世界中访问Java世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递。
-
jclass是JNI的数据类型,对应Java的java.lang.Class实例。jobject同样也是JNI的数据类型,对应于Java的Object。
当我们在Java中调用native_init()方法时,就会从JNI中寻找Java_com_example_MediaRecorder_native_1init 函数,如果没有就会报错,如果找到就会为native_init和Java_com_example_MediaRecorder_native_1init建立关联,其实是保存JNI的函数指针,这样再次调用native_init方法时直接使用这个函数指针就可以了。静态注册就是根据方法名,将Java方法和JNI函数建立关联,但是它有一些缺点:
- JNI层的函数名称过长。
- 声明Native方法的类需要用javah生成头文件。
- 初次调用Native方法时需要建立关联,影响效率。
现已知静态注册就是Java的Native方法通过方法指针来与JNI进行关联,如果Java的Native方法知道它在JNI中对应的函数指针,就可以避免上述的缺点,而这就是动态注册。
3.2.3.2 动态注册
JNI 中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:
系统的MediaRecorder采用的就是动态注册,我们来查看它的JNI层是怎么做的:
上面定义了一个JNINativeMethod 类型的gMethods 数组,里面存储的就是MediaRecorder的Native方法与JNI层函数的对应关系:
-
注释1处“start”是Java层的Native方法,它对应的JNI层的函数为android_media_MediaRecorder_start。
-
“()V”是start方法的签名信息。
只定义JNINativeMethod类型的数组是没有用的,还需要注册它,注册的函数为register_android_media_MediaRecorder,这个函数会在哪里调用呢?答案是在register_android_media_MediaRecorder 函数的英文注释上:JNI_OnLoad in android_media_MediaPlayer.cpp。这个JNI_OnLoad函数会在调用System.loadLibrary 函数后调用,因为多媒体框架中的很多框架都要进行JNINativeMethod数组注册,因此,注册函数就被统一定义在android_media_MediaPlayer.cpp的JNI_OnLoad函数中,如下所示:
在JNI_OnLoad 函数中调用了整个多媒体框架的注册JNINativeMethod 数组的函数,在注释2处调用了register_android_media_MediaRecorder函数,同样地,在注释1处MediaPlayer框架的注册JNINativeMethod数组的函数register_android_media_MediaPlayer也被调用了。
register_android_media_MediaRecorder函数如下所示:
在register_android_media_MediaRecorder方法中返回了AndroidRuntime 的registerNativeMethods函数,如下所示:
在registerNativeMethods 函数中又返回了jniRegisterNativeMethods 函数,它被定义在JNI帮助类JNIHelp.cpp中:
从注释1处可以看出,最终通过调用的JNIEnv的RegisterNatives函数来完成JNI的注册,可以看出动态注册要比静态注册复杂一些,但是一劳永逸。
3.3 数据类型的转换
3.3.1 基本数据类型的转换
3.3.1 引用数据类型的转换
引用数据类型的继承关系:
3.4 方法签名
gMethods数组中存储的是MediaRecorder的Native方法与JNI层函数的对应关系,其中“()V”和“(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V”就是方法签名。Java是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在JNI 中仅仅通过方法名是无法找到Java中对应的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。JNI的方法签名的格式为:
以上面gMethods数组的native_setup函数举例,它在Java中是如下定义的:
它在JNI中的方法签名为:
native_setup 函数的第一个参数的签名为“Ljava/lang/Object;”,后两个参数的签名为“Ljava/lang/String;”,返回值类型void的签名为“V”,组合起来就是上面的方法签名。
如果每次编写JNI时都要组织方法签名,会是一件很繁琐的事,而且也容易出错,因此Java提供了javap命令(javap)来自动生成方法签名。
3.5 解析JNIEnv
JNIEnv是Native世界中Java环境的代表,通过JNIEnv*指针就可以在Native世界中访问Java世界的代码进行操作,它只在创建它的线程中有效,不能跨线程传递,因此不同线程的JNIEnv是彼此独立的,JNIEnv的主要作用有以下两点:
- 调用Java的方法。
- 操作Java(操作Java中的变量和对象等)。
JNIEnv的定义,如下所示:
这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,就是C++代码中的定义,否则就是C代码中的定义。在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。通过JavaVM的AttachCurrentThread 函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。
从注释2处可以看出在C中,JNIEnv类型是JNINativeInterface*,从注释1处可以看出在C++中JNIEnv的类型是JNIEnv,JNIEnv是如何定义的呢?如下所示:
_JNIEnv是一个结构体,其内部又包含了JNINativeInterface。在_JNIEnv中定义了很多函数,这里列举了3个比较常用的函数,FindClass 用来找到Java 中指定名称的类,GetMethodID用来得到Java中的方法,GetFieldID用来得到Java中的成员变量,这里可以发现这三个函数都调用了JNINativeInterface中定义的函数,因此可以得出结论,无论是C还是C++,JNIEnv的类型都和JNINativeInterface结构有关,JNINativeInterface的定义如下所示:
在JNINativeInterface 结构中定义了很多和JNIEnv 结构体对应的函数指针,这里只给出了上面JNIEnv结构体中对应的三个函数指针定义。通过这些函数指针的定义,就能够定位到虚拟机中的JNI函数表,从而实现了JNI层在虚拟机中的函数调用,这样JNI层就可以调用Java世界的方法了。
3.5.1 jfieldID和jmethodID
在_JNIEnv结构体中定义了很多函数,这些函数都会有不同的返回值,如下所示:
这里我们列举了两个函数,这两个函数的返回值分别为jmethodID和jfieldID,当然还有其他很多返回值,比如jobject、jbooleanArray和jboolean等,这里我们拿jmethodID和jfieldID来进行举例,来查看这些返回值在JNI层的应用。jfieldID和jmethodID分别用来代表Java类中的成员变量和方法。在注释1处jclass代表Java类,name代表成员方法或者成员变量的名字,sig为这个方法和变量的签名。我们来查看MediaRecorder框架的JNI层是如何使用GetMethodID和GetFieldID这两个方法的,如下所示:
在注释1处,通过FindClass来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,因此,clazz就是Java层的MediaRecorder在JNI层的代表。在注释2和注释3处的代码用来找到Java层的MediaRecorder中名为mNativeContext和mSurface的成员变量,并分别赋值给context和surface。在注释4处获取Java层的MediaRecorder中名为postEventFromNative的静态方法,并赋值给fields.post_event,其中fields的定义为:
将这些成员变量和方法赋值给jfieldID和jmethodID类型的变量有两个原因:第一是为了效率考虑,如果每次调用相关方法时都要查询方法和变量,显然会效率很低;第二是这些成员变量和方法都是本地引用,在android_media_MediaRecorder_native_init函数返回时这些本地引用会被自动释放,因此用fields 来进行保存,以便后续使用。结合上面两方面原因,在MediaRecorder框架JNI 层的初始化方法android_media_MediaRecorder_native_init中将这些jfieldID和jmethodID类型的变量保存起来,是为了更高效率地供后续使用。
3.5.2 使用jfieldID和jmethodID
保存了jfieldID和jmethodID类型的变量,接着怎么使用它们呢?如下所示:
在注释1处调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了fields.post_event,从9.5.1节中我们得知,它其实是保存了Java层MediaRecorder的静态方法postEventFromNative:
在注释1处会创建一个消息,在注释2处将这个消息发送给MediaRecorder 内部类mEventHandler 来处理,这样做的目的是将代码逻辑运行在应用程序的主线程中。JNIEnv的CallStaticVoidMethod函数会调用Java层MediaRecorder的静态方法postEventFromNative,也就是说JNIEnv的CallStaticVoidMethod函数可以访问Java的静态方法,同理如果想要访问Java的方法则可以使用JNIEnv的CallVoidMethod函数。上面的例子是使用了jmethodID,接着来查看jfieldID是如何应用的:
在注释1处调用了JNIEnv的GetObjectField函数,参数中的fields.surface是jfieldID类型的变量,用来保存Java层MediaRecorde中的成员变量mSurface,mSurface的类型为Surface,这样通过GetObjectField函数就得到了mSurface在JNI层中对应的jobject类型变量surface。
3.6 引用类型
和Java的引用类型一样,JNI也有引用类型,它们分别是本地引用(LocalReferences)、全局引用(Global References)和弱全局引用(Weak GlobalReferences)。
3.6.1 本地引用
JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用也是JNI中最常见的引用类型。本地引用的特点主要有以下几点:
- 当Native函数返回时,这个本地引用就会被自动释放。
- 只在创建它的线程中有效,不能够跨线程使用。
- 局部引用是JVM负责的引用类型,受JVM管理。
以android_media_MediaRecorder_native_init函数来举例,如下所示:
注释1处的FindClass会返回clazz,这个clazz就是本地引用,它会在android_media_MediaRecorder_native_init 函数调用返回后被自动释放。我们也可以使用JNIEnv的DeleteLocalRef函数来手动删除本地引用,DeleteLocalRef函数的使用场景主要是在native函数返回前占用了大量内存,需要调用DeleteLocalRef函数立即删除本地引用。
3.6.2 全局引用
全局引用和本地引用几乎是相反的,它主要有以下特点:
- 在native函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不会被GC回收。
- 全局引用是可以跨线程使用的。
- 全局引用不受到JVM管理。
JNIEnv的NewGlobalRef函数用来创建全局引用,调用JNIEnv的DeleteGlobalRef函数来释放全局引用:
在注释1处返回的clazz是本地引用,并传入到注释2处,在注释2处调用JNIEnv的NewGlobalRef 函数将clazz 转变为全局引用mClass。那什么时候将全局引用mClass释放呢?我们来查看JNIMediaRecorderListener的析构函数:
从上面的析构函数的注释就可以知道,这个析构函数用来释放全局引用,在注释1处释放了全局引用mClass。
3.6.3 弱全局引用
弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,不同的是弱全局是可以被GC回收的,弱全局引用被GC回收之后会指向NULL。JNIEnv的NewWeakGlobalRef函数用来创建弱全局引用,调用JNIEnv的DeleteWeakGlobalRef函数来释放弱全局引用。由于弱全局引用可能被GC 回收,因此在使用它之前要先判断它是否被回收了,方法就是使用JNIEnv的IsSameObject函数来判断:
weakGlobalRef是一个弱全局引用,在使用它之前,调用JNIEnv的IsSameObject函数,这个函数会判断传入的两个引用是否相等,如果相等返回JNI_TRUE,不相等返回JNI_FALSE。注释1处如果返回JNI_TRUE说明weakGlobalRef等于NULL并被GC回收了,就会调用return false不执行后面的代码逻辑,否则就使用弱全局引用weakGlobalRef。