jni学习总结

一、jni是什么

java代码要使用native的代码,需要一个桥梁将他们连接起来,这个桥梁就是jni。


JNI桥梁

二、JNI的举例

1、新建一个Android项目,在根目录下创建 jni文件夹,用于存放 C源码。
2、在java代码中,创建一个本地方法 getStringFromC 本地方法没有方法体。

private native String getStringFromC();

3、在jni中创建一个C文件,定义一个函数实现本地方法,函数名必须用使用 本地方法的全类名,点改为下划线。

#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
//方法名必须为本地方法的全类名点改为下划线,传入的两个参数必须这样写,
//第一个参数为java虚拟机的内存地址的二级指针,用于本地方法与java虚拟机在内存中交互
//第二个参数为一个java对象,即是哪个对象调用了这个 c方法
jstring Java_com_mwp_jnihelloworld_MainActivity_getStringFromC(JNIEnv* env,jobject obj){
    //定义一个C语言字符串
    char* cstr = "hello form c";
    //返回值是java字符串,所以要将C语言的字符串转换成java的字符串
    //在jni.h 中定义了字符串转换函数的函数指针
    //jstring   (*NewStringUTF)(JNIEnv*, const char*);
    jstring jstr2 = (*env) -> NewStringUTF(env, cstr);
    return jstr2;
}

4、在jni中创建 Android.mk文件,用于配置 本地方法

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#编译生成的文件的类库叫什么名字
LOCAL_MODULE    := hello
#要编译的c文件
LOCAL_SRC_FILES := Hello.c
include $(BUILD_SHARED_LIBRARY)

5、在jni目录下执行 ndk-build.cmd指令,编译c文件
6、在java代码中加载编译后生成的so类库,调用本地方法,将项目部署到虚拟机上之后就会发现toast弹出的C代码定义的字符串
7、jni打包的C语言类库默认仅支持 arm架构,需要在jni目录下创建 Android.mk 文件添加如下代码可以支持x86架构

APP_ABI := armeabi armeabi-v7a x86

三、native方法注册

native方法注册包括静态注册和动态注册,静态注册多用于NDK开发,上述的"二、JNI的举例"就是用的静态注册,而动态注册多用于Fremework开发,下面我们分别了解一下这两种注册方式

1、静态注册

在Android Studio中新建一个java library,命名为media,写一个简单的MediaRecorder.java(仿照系统的MediaRecorder.java)

package com.example;
public class MediaRecorder {
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    private static native final void native_init();
    public native void start() throws IllegalStateException;
}

然后进入项目的media/src/main/java/com/example目录执行如下命令:

javac -h ./ MediaRecorder.java

说明: javah从java10开始被移除掉,取而代之的是javac -h命令

然后在当前目录会生成com_example_MediaRecorder.h文件,此文件的内容为

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_MediaRecorder */

#ifndef _Included_com_example_MediaRecorder
#define _Included_com_example_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_MediaRecorder
 * Method:    native_init
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init
  (JNIEnv *, jclass);

/*
 * Class:     com_example_MediaRecorder
 * Method:    start
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_start
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

我们可以看到native_init的jni方法对应的是Java_com_example_MediaRecorder_native_1init,以"Java"开头说明是在java平台调用JNI方法的,后面的com_example_MediaRecorder_native_1init指的是包名+类名+方法名,中间用" _ "代替。另外我们注意到,原本"native_init"方法名中就有" _ ",为了消除歧义,这里的" _ "就会替换为" _1 "。
JNIEnv是native世界中java环境的代表,通过JNIEnv*指针就可以在native世界中访问java世界的代码进行操作,它只能在创建它的线程中有效,不能跨进程传递。
jclass是jni的数据类型,对应的是java的java.lang.Class实例。
jobject也是jni的数据类型,对应java的object。

当我们在java中调用natice_init方法时,就会从jni中寻找Java_com_example_MediaRecorder_native_1init,如果找到了该方法名的jni函数,就会建立联系,建立联系的方法就是保存jni的函数指针。通过方法名来建立联系的方式就是静态注册。

2、动态注册

从静态注册我们可以看出,只要java层和native层能够进行关联就能完成注册,静态注册采用的是方法名对应,然后保存jni函数指针的方法。动态注册其实就是不靠方法名来进行关联,而是换一种方式来记录"java的native方法"和"jni中的函数指针"的关系的注册方式。

在jni中有一个专门的结构体来描述这种对应关系:


JNINativeMethod

Android系统的MediaRecorder采用的就是动态注册:


MediaRecorder的JNINativeMethod数组

通过这个数组,我们就能获取到java层的native函数和jni层的函数指针的一一对应关系,但知道了对应关系还不够,这里只是数组的声明,我们还得使用他,即调用注册函数,才能真正建立联系。

注册函数一般流程:jni的register函数--->AndroidRuntime.registerNativeMethods()---->JNIEnv.RegisterNatives()

四、数据类型转换

通过natice方法的注册,我们已经找到了java层的函数和jni的函数指针的关联关系,但是他们之间相互调用,数据类型也要相匹配才行。换句话说,java层是一个int型变量,native层也需要有native所能理解的int才行,这就需要数据类型转换。
数据类型转换我们又分为基本数据类型转换和引用数据类型转换。

1、基本数据类型转换

基本数据类型转换

2、引用数据类型转换

引用数据类型转换

五、方法签名

我们再来回顾下动态注册的JNINativeMethod数组:


JNINativeMethod数组

因为java中有函数的重载,所以只通过函数名,我们无法定位到java所指向的函数,于是我们通过方法签名来表示java层的函数的参数和返回值,从而达到定位。如上图数组的元素的第二个参数"()V"和"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法签名。
jni的方法签名的格式为:
(参数签名格式...)返回值签名格式。

每次去手写签名格式显然是很心累的一件事,好在java提供了javap命令来自动生成签名。
我们接着在刚才的shell窗口输入

javap -s -p ./MediaRecorder.class
javap命令

六、解析JNIEnv

JNIEnv是native世界中java环境的代表,它只在创建它的线程中有效,不能跨线程传递,不同线程的JNIEnv彼此独立。

1、JNIEnv的定义:

JNIEnv定义

这里对c和c++做了区分,实际上我们深入源码可以发现,c++中的_JNIEnv结构体实际上又包含了JNINativeInterface,所以无论是c++还是c,最终都是靠JNINativeInterface来实现的,JNINativeInterface的定义如下:


JNINativeInterface定义

在JNINativeInterface中有很多JNIEnv结构体对应的函数指针,通过这些函数指针的定义,就能定位到虚拟机中的jni函数表,从而实现了jni层在虚拟机中的函数调用,这样jni层就可以调用java世界的方法了。
这里我们又提到了虚拟机,我们再回调"JNIEnv定义"那幅图中,可以看到,除了JNINativeInterface,无论是c++还是c,都还有一个JavaVM变量,它就是虚拟机在jni层的代表,一个虚拟机进程只有一个JavaVM,因此所有线程通能使用这个JavaVM。通过JavaVM的attachCurrentThread函数可以获取到这个线程的JNIEnv,这样就能在不同的线程中调用java方法了。顺便提一下,在线程结束的时候,还需要DetachCurrentThread函数来释放资源。

2、jfieldID和jmethodID

JNIEnv最终都是调用的JNINativeInterface,在JNINativeInterface里面,我们可以看到,函数指针返回的类型就有jfieldID和jmethodID,当然还有其他的,不过都大同小异。jfieldID和jmethodID分别来代表java类中的成员变量和方法。
我们来看一下系统层的MediaRecorder框架的jni层是如何使用GetMethodID
和GetFieldID这两个方法的,如图所示:


GetMethodID 和GetFieldID

可以看到,首先会通过FindClass找到java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,clazz就是java层的mediaRecorder在jni层的代表,在注释2和注释3分别找到jfava层的MediaRecorder中名为mNativeContext和surface并缓存到fields中,注释4获取到java层的MediaRecorder中名为postEventFromNative方法,并缓存到fields中。这里为什么要进行缓存呢,第一个是因为效率问题,不用每次都查询,第二个是因为这些成员变量和方法都是本地引用,在android_media_MediaRecorder_native_init函数返回时这些本地引用会自动释放。本地引用后续会提到。

3、使用jfieldID和jmethodID

上述只是将jfieldID和jmethodID保存了起来,还没有使用到,那要怎么才能使用呢?如下图所示:


使用jfieldID和jmethodID

在注释1出调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了缓存的fields.post_event,它其实是保存了java层MediaRecorder的静态方法postEventFromNative,也就是说JNIEnv的CallStaticVoidMethod函数可以访问java的静态方法,同理如果想要访问java的方法则可以使用JNIEnv的CallVoidMethod函数,如果想要想要访问java的属性,可以使用GetObjectField函数。


GetObjectField

七、jni的引用类型

jni的引用类型分别是本地引用、全局引用、弱全局引用

1、本地引用

本地引用有以下三个特点:

  • 当native函数返回时,这个本地引用就会被自动释放
  • 只在创建它的线程中有效,不能够跨线程使用
  • 局部引用是JVM负责的引用类型,受JVM管理


    本地引用

    注释1处的FindClass会返回clazz,这个clazz就是本地引用,它会在android_media_MediaRecorder_native_init函数调用返回后被自动释放。

2、全局引用

全局引用有以下三个特点:

  • 在native函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不会被GC回收
  • 全局引用是可以跨线程是用的
  • 全局引用不受到JVM管理
    JNIEnv的NewGlobalRef函数用来创建全局引用,JNIEnv的DeleteGlobalRef函数用来释放全局引用


    添加全局引用

    释放全局引用

3、弱全局引用

弱全局引用和全局引用特点差不多,区别是弱全局引用可以被GC回收,回收之后指向NULL,JNIEnv的NewWeakGlobalRef用来创建弱全局引用,JNIEnv的DeleteWeakGlobalRef用来释放弱全局引用。由于弱全局引用可能为NULL,因此使用前要想判断是否为空,使用JNIEnv的sSameObject进行判断


弱全局引用判空

推荐阅读更多精彩内容