Java调用C/C++在Java语言里面本来就有的,并非Android独有的,即JNI。JNI就是Java调用C++的规范。
JNI 概述
JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native语言的一种特性,通过JNI可以使JAVA和 C/C++进行交互。
Java语言是跨平台的语言,而这跨平台的背后都是依靠Java虚拟机,虚拟机采用C/C++编写,适配各个系统,通过JNI为上层Java提供各种服务,保证跨平台性。
在Java语言出现前,就有很多程序和库都是由Native语言写的,如果想重复利用这些库,就可以所使用JNI来实现。在Android平台上,JNI就是一座将Java世界和Native世界联通的一座桥梁。
通过JNI,Java世界和Native世界的代码就可以相互访问了。
JNI实例:Camera
最新有在看系统的Camera相关,所以从系统Camera角度来分析下JNI的应用,下面讲的实例基于Camera2
Android5.0(21)之后android.hardware.Camera就被废弃了,取而代之的是全新的android.hardware.Camera2
相关代码:
1 | frameworks/base/core/jni/AndroidRuntime.cpp |
Camera2 Java层对应的是CameraMetadataNative.java,Native层对应的是android_hardware_camera2_CameraMetadata.cpp
Java层CameraMetadataNative
相关代码在CameraMetadataNative.java
Camera2使用CameraManager(摄像头管理器)进行控制,CameraManager具体的操作会通过CameraMetadataNative来执行。
CameraMetadataNative的初始化
1 | public class CameraMetadataNative implements Parcelable |
静态方法初始化调用了Native层的方法nativeClassInit
,这个方法对应的Native层具体实现,是在android_hardware_camera2_CameraMetadata.cpp
Native层CameraMetadata
Native层相关代码在android_hardware_camera2_CameraMetadata.cpp
Native方法初始化
1 | static const JNINativeMethod gCameraMetadataMethods[] = { |
gCameraMetadataMethods什么时候会被加载?
1 | int register_android_hardware_camera2_CameraMetadata(JNIEnv *env) |
register_android_hardware_camera2_CameraMetadata
何时会被调用到,这个就需要了解下JNI的查找方式。
JNI查找方式
Android系统在启动启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会AndroidRuntime.cpp中的startVm创建虚拟机,VM创建完成后,紧接着调用startReg完成虚拟机中的JNI方法注册。
刚才CameraMetadata中register_android_hardware_camera2_CameraMetadata
方法,在AndroidRuntime.cpp的声明:
1 | extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env); |
然后在gRegJNI中的静态声明
1 | static const RegJNIRec gRegJNI[] = { |
gRegJNI方法在startReg中被调用
1 | /*static*/ int AndroidRuntime::startReg(JNIEnv* env) |
register_jni_procs(gRegJNI, NELEM(gRegJNI), env)会循环调用gRegJNI数组成员所对应的方法
1 | static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env) |
这样android_hardware_camera2_CameraMetadata.cpp中的int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)
就会被调用到。
除了这种Android系统启动时,就注册JNI所对应的方法。还有一种就是程序自定义的JNI方法,以 MediePlay 为例:
相关代码路径
1 | frameworks/base/media/java/android/media/MediaPlayer.java |
MediaPlayer声明:
1 | public class MediaPlayer extends PlayerBase |
静态代码块中使用System.loadLibrary加载动态库,media_jni在Android平台对应的是libmedia_jni.so库。
在jni目录/frameworks/base/media/jni/Android.mk
中有相应的声明:
1 | LOCAL_SRC_FILES:= \ |
在android_media_MediaPlayer.cpp
找到对应的Native(natvie_init)方法:
1 | static void |
JNI注册的方法就是上面描述的两种方法:
- 在Android系统启动时注册,在AndroidRuntime.cpp中的gRegJNI方法中声明
- 使用System.loadLibrary()方式注册
JNI基础
上面一节主要描述了系统中Java层和Native层交互和实现,并没有对JNI的基础理论,流程进行分析
JNI命名规则
JNI方法名规范 :
1 | 返回值 + Java前缀 + 全路径类名 + 方法名 + 参数① JNIEnv + 参数② jobject + 其它参数 |
简单的一个例子,返回一个字符串
1 | extern "C" JNIEXPORT jstring JNICALL |
- 返回值:jstring
- 全路径类名:com_yeungeek_jnisample_NativeHelper
- 方法名:stringFromJNI
JNI开发流程
- 在Java中先声明一个native方法
- 编译Java源文件javac得到.class文件
- 通过javah -jni命令导出JNI的.h头文件
- 使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法。)
- 将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib)
- 通过Java命令执行Java程序,最终实现Java调用本地代码。
数据类型
基本数据类型
Signature | Java | Native |
---|---|---|
B | byte | jbyte |
C | char | jchar |
D | double | jdouble |
F | float | jfloat |
I | int | jint |
S | short | jshort |
J | long | jlong |
Z | boolean | jboolean |
V | void | jvoid |
引用数据类型
Signature | Java | Native |
---|---|---|
L+classname +; | Object | jobject |
Ljava/lang/String; | String | jstring |
[L+classname +; | Object[] | jobjectArray |
Ljava.lang.Class; | Class | jclass |
Ljava.lang.Throwable; | Throwable | jthrowable |
[B | byte[] | jbyteArray |
[C | char[] | jcharArray |
[D | double[] | jdoubleArray |
[F | float[] | jfloatArray |
[I | int[] | jintArray |
[S | short[] | jshortArray |
[J | long[] | jlongArray |
[Z | boolean[] | jbooleanArray |
方法签名
JNI的方法签名的格式:
1 | (参数签名格式...)返回值签名格式 |
demo的native 方法:
1 | public static native java.lang.String stringFromJNI(); |
可以通过javap命令生成方法签名``:
1 | ()Ljava/lang/String; |
JNI原理
Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI环境中创建JVM的函数为JNI_CreateJavaVM。
JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”,两者本质上都是指向函数表的二级指针。
JavaVM
JavaVM是Java虚拟机在JNI层的代表,JavaVM 提供了“调用接口”函数,您可以利用此类函数创建和销毁 JavaVM。理论上,每个进程可以包含多个JavaVM,但AnAndroid只允许每个进程包含一个JavaVM。
JNIEnv
JNIEnv是一个线程相关的结构体,该结构体代表了Java在本线程的执行环境。JNIEnv 提供了大多数 JNI 函数。您的原生函数均会接收 JNIEnv 作为第一个参数。
JNIEnv作用:
- 调用Java函数
- 操作Java代码
JNIEnv定义(jni.h):libnativehelper/include/nativehelper/jni.h
1 |
|
定义中可以看到JavaVM,Android中一个进程只会有一个JavaVM,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构
注册JNI函数
Java世界和Native世界的方法是如何关联的,就是通过JNI函数注册来实现。JNI函数注册有两种方式:
静态注册
这种方法就是通过函数名来找对应的JNI函数,可以通过javah
命令行来生成JNI头文件
1 | javah com.yeungeek.jnisample.NativeHelper |
生成对应的com_yeungeek_jnisample_NativeHelper.h
文件,生成对应的JNI函数,然后在实现这个函数就可以了
1 | /* |
静态注册方法中,Native是如何找到对应的JNI函数,在JNI查找方式中介绍系统的流程,并没有详细说明静态注册的查找。这里简单说明下这个过程(以上面的声明为例子s):
当Java调用native stringFromJNI函数时,会从对应JNI库中查找Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI
函数,如果没有找到,就会报错。
静态注册方法,就是根据函数名来关联Java函数和JNI函数,JNI函数需要遵循特定的格式,这其中就有一些缺点:
- 声明了native方法的Java类,需要通过
javah
来生成头文件 - JNI函数名称非常长
- 第一次调用native函数,需要通过函数名来搜索关联对应的JNI函数,效率比较低
如何解决这些问题,让native函数,提前知道JNI函数,就可以解决这个问题,这个过程就是动态注册。
动态注册
动态注册在前面的Camera例子中,已经有涉及到,JNI函数classInit
的声明。
1 | static const JNINativeMethod gCameraMetadataMethods[] = { |
JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:
1 | typedef struct { |
在JNI查找方式说到,JNI注册的两种时间,第一种已经介绍过了,我们自定义的native函数,基本都是会使用System.loadLibrary(“xxx”)
,来进行JNI函数的关联。
loadLibrary(Android7.0)
1 | public static void loadLibrary(String libname) { |
调用到Runtime(libcore/ojluni/src/main/java/java/lang/Runtime.java)的loadLibrary0方法:
1 | synchronized void loadLibrary0(ClassLoader loader, String libname) { |
doLoad
1 | private String doLoad(String name, ClassLoader loader) { |
nativeLoad
进入到虚拟机代码/libcore/ojluni/src/main/native/Runtime.c
1 | JNIEXPORT jstring JNICALL |
然后调用JVM_NativeLoad
,JVM_NativeLoad方法申明在jvm.h中,实现在OpenjdkJvm.cc(/art/runtime/openjdkjvm/OpenjdkJvm.cc)
1 | JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, |
LoadNativeLibrary
调用JavaVMExt的LoadNativeLibrary方法,方法在(art/runtime/java_vm_ext.cc)中,这个方法代码非常多,选取主要的部分进行分析
1 | bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, |
代码里的主要逻辑:
- 加载so库中查找JNI_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功
- 如果找到JNI_OnLoad就会调用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注册的函数
- 所以如果采用动态注册就必须要实现
JNI_OnLoad
方法,否则调用Java中的native方法时会抛出异常
jclass、jmethodID和jfieldID
如果要通过原生代码访问对象的字段,需要执行以下操作:
- 使用 FindClass 获取类的类对象引用
- 使用 GetFieldID 获取字段的字段 ID
- 使用适当内容获取字段的内容,例如 GetIntField
具体的使用,放在第二篇文章中讲解
JNI的引用
JNI规范中定义了三种引用:
- 局部引用(Local Reference)
- 全局引用(Global Reference)
- 弱全局引用(Weak Global Reference)
局部引用
也叫本地引用,在 JNI层函数使用的非全局引用对象都是Local Reference,最大的特点就是,JNI 函数返回后,这些声明的引用可能就会被垃圾回收
全局引用
这种声明的对象,不会主动释放资源,不会被垃圾回收
弱全局引用
一种特殊的全局引用,在运行过程中可能被回收,使用之前需要判断下是否为空