开启生长之旅!这是我参加「日新方案 2 月更文挑战」的第 2 天,点击检查活动概况

jni介绍

jni全称java native interface,我把它分为三部分,java代表java言语,native代表当前程序运转的本地环境,一般指windows/linux,而这些操作体系都是经过C/C++完结的,所以native一般也指C/C++言语,interface代表java跟native两者之间的通讯接口,jni能够完结java和C/C++通讯。它是java生态的特征,所以界说在jdk规范傍边。

运用场景和优势
  • java虽然跨平台,但仍然运转在详细平台(windows,linux)之上,对于需求操作硬件的功用,有必要经过体系的C/C++办法对硬件进行直接操作,比方翻开文件,java层有必要调用体系的open办法(linux是open,windows是openFile)才干翻开文件,这个时分就涉及到java代码怎么调用C/C++代码的问题

  • 在一些具有复杂算法的场景(音视频编解码,图像绘制等),java的履行功率远低于C/C++的履行功率,运用jni技术,在java层调用C/C++代码,能够提高程序的履行功率,最大化运用机器的硬件资源。

  • native层的代码往往更加安全,反编译so文件比反编译jar文件要难得多,所以,咱们往往把涉及到密码密钥相关的功用用C/C++完结,然后java层经过jni调用

通讯原理

java运转在jvm,jvm自身便是运用C/C++编写的,因而jni只需求在java代码、jvm、C/C++代码之间做切换即可

一篇文章教你完全掌握jni技术

运用进程

基于windows,为了便利,我运用了idea+clion,读者需求能把握这两个东西的根本运用,跟Android Studio差不多的。整个进程我分为了十步:

1.运用idea创立一个java工程,并创立JNIDemo.java文件

一篇文章教你完全掌握jni技术

2.在JNIDemo.java文件中声明native办法helloJni()

public class JNIDemo {
    public static native helloJni();
}

3.运用javac命令编译JNIDemo.java,生成JNIDemo.class文件

一篇文章教你完全掌握jni技术

4.运用javah命令生成JNIDemo.h文件

一篇文章教你完全掌握jni技术

5.运用clion创立C++ library项目,并仿制刚刚生成的com_jason_jni_JNIDemo.h头文件到项目根目录

一篇文章教你完全掌握jni技术

库类型挑选shared,表明编译生成动态库,static为静态库,动态库和静态库的最大差异就在于静态库会将方针代码以及一切需求依靠的库文件进行整体打包,履行时不再依靠外部环境。动态库则只会将方针代码打包,运转时需求依靠外部环境,所以一般来说,静态库往往比动态库要大。windows上的动态库为.dll文件,静态库为.lib文件。linux上的动态库为.so文件,静态库为.a文件。

一篇文章教你完全掌握jni技术

6.创立JNIDemo.cpp文件,完结helloJni()办法

一篇文章教你完全掌握jni技术

这儿我直接返回了I am from c++字符串,同时要将JNIDemo.cpp文件增加到CMakeList.txt

一篇文章教你完全掌握jni技术

这个时分咱们看到com_jason_jni_JNIDemo.h文件中有报错

一篇文章教你完全掌握jni技术

这是因为无法从体系中找到jni.h头文件,这儿咱们能够手动导入jni.h到项目中,开头说了,jni是java的特征,所以jni.h文件在jdk傍边,去本地jdk装置目中找<jdk装置目录>/include/jni.h<jdk装置目录>/include/win32/jni_md.h,将这两个文件拷贝到项目根目录中,然后将#include <jni.h>改为#include "jni.h",尖括号表明从体系中查找,双引号表明从当前项目中查找。

一篇文章教你完全掌握jni技术

7.编译本地代码,生成libjnitest.dll文件,因为我是在windows上运转的,所以生成的是.dll

一篇文章教你完全掌握jni技术

7.在刚刚的java项目的根目录中创立libs文件夹,并将其设置为资源文件夹,然后将生成的libjnitest.dll文件拷贝到该目录中

一篇文章教你完全掌握jni技术

留意libs目录的图标一定要是资源文件夹的款式,不是普通文件夹的款式,然后将libjnitest.dll文件拷贝到该目录下

一篇文章教你完全掌握jni技术

8.在java代码中经过System.loadLibrary()加载dll文件

public class JNIDemo {
    static {
        System.loadLibrary("libjnitest");
    }
    public static native String helloJni();
    public static void main(String[] args) {
        System.out.println(helloJni());
    }
}

9.将该libjnitest.dll库增加到虚拟机运转环境

一篇文章教你完全掌握jni技术

一篇文章教你完全掌握jni技术

值设置为-Djava.library.path=E:\Idea_projects\JNITestDemo\libs,等号后边为libjnitest.dll文件地点的途径

10.在main()函数处右键,运转该程序

一篇文章教你完全掌握jni技术

成功输出I am from c++

上面经过一个简单的事例讲解了jni的运用流程,从中不难看出,大部分进程都是固定的,仅有不固定的是JNIDemo.cpp的内容,这个取决于实践的需求。而在新版的Android Studio傍边现已把这些固定流程封装成了模板操作,咱们能够一键生成头文件和源文件,开发者只需求关注源文件的功用完结即可。

一篇文章教你完全掌握jni技术

只需求在新建项目时挑选Native C++即可,这儿我就不做详细演示了,有爱好的读者能够自行尝试。

API详解

刚刚我只是简单的返回了一个字符串,实践上咱们还能够做许多工作,jni.h都给咱们界说好了规范,咱们依照它的规范来即可。

开头说到,java和C/C++通讯是经过jni来完结的,那么在jni办法中就涉及到对java变量的拜访(变量类型包括根本数据类型和引证数据类型),对java办法的调用,java目标的创立等,而java语法跟jni语法纷歧定是一 一对应的,比方,java中叫boolean,jni中叫jboolean,那怎么处理这个问题呢,jni给咱们供给了若干个映射表,将java中的类型与jni中的类型进行了一 一映射,其中包括根本数据类型映射,引证数据类型映射,办法签名(包含参数和返回值)映射,以下是这三个映射表:

表1-根本数据类型映射表

表2-引证数据类型映射表

表3-办法签名

一篇文章教你完全掌握jni技术

以上面Demo来剖析

//Java办法
public static native String helloJni();
public static native float helloJni2(int age, boolean isChild);
//jni办法
extern "C"
JNIEXPORT jstring JNICALL Java_com_jason_jni_JNIDemo_helloJni
        (JNIEnv *env, jclass clazz){
    return env->NewStringUTF("I am from c++");
}
extern "C"
JNIEXPORT jfloat JNICALL Java_com_jason_jni_JNIDemo_helloJni2
        (JNIEnv *env, jclass clazz, jint age, jboolean isChild){
}

java办法helloJni()的返回值为String,映射到jni办法中的返回值即为jstring,咱们新增一个办法helloJni2(int age, boolean isChild),增加了两个参数intboolean,对应的映射为jintjboolean,同时返回值float映射为jfloat

处理了数据类型纷歧致的问题之后,接下来就能够在jni办法中拜访java成员了,相同的,jni给咱们供给了一系列拜访java成员的API,详细如下:

jni拜访调用目标

办法名 效果
GetObjectClass 获取调用目标的类,咱们称其为target
FindClass 依据类名获取某个类,咱们称其为target
IsInstanceOf 判别一个类是否为某个类型
IsSamObject 是否指向同一个目标

jni拜访java成员变量的值

办法名 效果
GetFieldId 依据变量名获取target中成员变量的ID
GetIntField 依据变量ID获取int变量的值,对应的还有byte,boolean,long等
SetIntField 修正int变量的值,对应的还有byte,boolean,long等

jni拜访java静态变量的值

办法名 效果
GetStaticFieldId 依据变量名获取target中静态变量的ID
GetStaticIntField 依据变量ID获取int静态变量的值,对应的还有byte,boolean,long等
SetStaticIntField 修正int静态变量的值,对应的还有byte,boolean,long等

jni拜访java成员办法

办法名 效果
GetMethodID 依据办法名获取target中成员办法的ID
CallVoidMethod 履行无返回值成员办法
CallIntMethod 履行int返回值成员办法,对应的还有byte,boolean,long等

jni拜访java静态办法

办法名 效果
GetStaticMethodID 依据办法名获取target中静态办法的ID
CallStaticVoidMethod 履行无返回值静态办法
CallStaticIntMethod 履行int返回值静态办法,对应的还有byte,boolean,long等

jni拜访java结构办法

办法名 效果
GetMethodID 依据办法名获取target中结构办法的ID,留意,办法名传<init>
NewObject 创立目标

jni创立引证

办法名 效果
NewGlobalRef 创立全局引证
NewWeakGlobalRef 创立弱全局引证
NewLocalRef 创立局部引证
DeleteGlobalRef 开释全局目标,引证不主动开释会导致内存走漏
DeleteLocalRef 开释局部目标,引证不主动开释会导致内存走漏

除此之外,jni还供给了反常处理机制,处理办法跟java相同有两种,要么往上(java层)抛,要么自己捕获处理

办法名 效果
ExceptionOccurred 判别是否有反常发生
ExceptionClear 铲除反常
Throw 往上(java层)抛出反常
ThrowNew 往上(java层)抛出自界说反常

API有许多,上述只是列出了一些常用的,其他的能够自行到jni.h文件里去检查。

事例实战

以一个完整的demo来进行综合实战,在实战中感触jni的运用姿态,为了便利,我直接在Android Studio里边创立了一个Native工程。

需求:计算按钮的点击次数

代码如下:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "jasonwan";
    private TextView tv;
    static {
        System.loadLibrary("jni");
    }
    private int num = 1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.sample_text);
        tv.setOnClickListener(v -> {
            jnitTest()
        });
    }
    //jni测验代码主要在这个办法里边
    public native void jniTest();
}
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG    "jasonwan" // 这个是自界说的LOG的标识
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__) // 界说LOGD类型
/**
 * 尽管java中的jniTest()办法没有参数,但cpp中仍然有两个参数,
 * 参数一:JNIEnv* env表明指向可用JNI函数表的接口指针,一切跟jni相关的操作都需求经过env来完结
 * 参数二:jobject是调用该办法的java目标,这儿是MainActivity调用的,所以thiz代表MainActivity
 * 办法名:Java_包名_类名_办法名
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_jason_jni_MainActivity_jniTest(JNIEnv *env, jobject thiz) {
    //获取MainActivity的class目标
    jclass clazz = env->GetObjectClass(thiz);
    //获取MainActivity中num变量id
    /**
    参数1:MainActivity的class目标
    参数2:变量名称
    参数3:变量类型,详细见上《表3-办法签名》
    **/
    jfieldID numFieldId = env->GetFieldID(clazz, "num", "I");
    //依据变量id获取num的值
    jint oldValue = env->GetIntField(thiz, numFieldId);
    //将num变量的值+1
    env->SetIntField(thiz, numFieldId, oldValue + 1);
    //从头获取num的值
    jint num = env->GetIntField(thiz, numFieldId);
    //先获取tv变量id
    jfieldID tvFieldId = env->GetFieldID(clazz, "tv", "Landroid/widget/TextView;");
    //依据变量id获取textview目标
    jobject tvObject = env->GetObjectField(thiz, tvFieldId);
    //获取textview的class目标
    jclass tvClass = env->GetObjectClass(tvObject);
    //获取setText办法ID
    /**
    参数1:textview的class目标
    参数2:办法名称
    参数3:办法参数类型和返回值类型,详细见上《表3-办法签名》
    **/
    jmethodID methodId = env->GetMethodID(tvClass, "setText", "([CII)V");
    //获取setText所需的参数
    //先将num转化为jstring
    char buf[64];
    sprintf(buf, "%d", num);
    jstring pJstring = env->NewStringUTF(buf);
    const char *value = env->GetStringUTFChars(pJstring, JNI_FALSE);
    //创立char数组,长度为字符串num的长度
    jcharArray charArray = env->NewCharArray(strlen(value));
    //拓荒jchar内存空间
    jchar *pArray = (jchar *) calloc(strlen(value), sizeof(jchar));
    //将num字符串缓冲到内存空间中
    for (int i = 0; i < strlen(value); ++i) {
        *(pArray + i) = *(value + i);
    }
    //将缓冲的值写入到上面创立的char数组中
    env->SetCharArrayRegion(charArray, 0, strlen(value), pArray);
    //调用setText办法
    env->CallVoidMethod(tvObject, methodId, charArray, 0, env->GetArrayLength(charArray));
    //开释资源
    env->ReleaseCharArrayElements(charArray, env->GetCharArrayElements(charArray, JNI_FALSE), 0);
    free(pArray);
    pArray = NULL;
}

最后的效果是这样的

一篇文章教你完全掌握jni技术

经过这样一个简单的事例,将大部分jni相关的API都操练了一遍,不难看出,java层能完结的功用,在native层相同能够完结,但这儿仅仅是为了操练jni,实践项目中不会把一些无关紧要的功用写在native层,比方UI操作,因为相同的功用,java代码要简洁得太多。

上面咱们在完结jniTest()时,能够看到c++里边的办法名很长Java_com_jason_jni_MainActivity_jniTest,这是jni静态注册的办法,依照jni规范的命名规则进行查找,格式为Java_类途径_办法名,这种办法在应用层开发用的比较广泛,因为Android Studio默许便是用这种办法,而在framework傍边几乎都是选用动态注册的办法来完结java和c/c++的通讯。比方之前研究过的《Android MediaPlayer源码剖析》,里边便是选用的动态注册的办法。

在Android中,当程序在Java层运转System.loadLibrary("jnitest");这行代码后,程序会去载入libjnitset.so文件。于此同时,发生一个Load事情,这个事情触发后,程序默许会在载入的.so文件的函数列表中查找JNI_OnLoad函数并履行,与Load事情相对,在载入的.so文件被卸载时,Unload事情被触发。此时,程序默许会去载入的.so文件的函数列表中查找JNI_OnLoad函数并履行,然后卸载.so文件。因而开发者常常会在JNI_OnLoad中做一些初始化操作,动态注册便是在这儿进行的,运用env->RegisterNatives(clazz, gMethods, numMethods)

  • 参数1:Java对应的类
  • 参数2:JNINativeMethod数组
  • 参数3:JNINativeMethod数组的长度,也便是要注册的办法的个数

JNINativeMethod是jni中界说的一个结构体

typedef struct {
    const char* name; //java中要注册的native办法名
    const char* signature;//办法签名
    void*       fnPtr;//对应映射到C/C++中的函数指针
} JNINativeMethod;

相比静态注册,动态注册的灵活性更高,如果修正了native函数地点类的包名或类名,仅调整native函数的签名信息即可。上述事例改为动态注册,java代码不需求更改,只需求更改native代码

#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG    "jasonwan" // 这个是自界说的LOG的标识
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__) // 界说LOGD类型
void native_jniTest(JNIEnv *env, jobject thiz) {
    //获取java类的实例目标
    jclass clazz = env->GetObjectClass(thiz);
    //获取MainActivity中num变量
    jfieldID numFieldId = env->GetFieldID(clazz, "num", "I");
    jint oldValue = env->GetIntField(thiz, numFieldId);
    //将num变量的值+1
    env->SetIntField(thiz, numFieldId, oldValue + 1);
    //从头获取num
    jint num = env->GetIntField(thiz, numFieldId);
    //获取tv控件目标
    jfieldID tvFieldId = env->GetFieldID(clazz, "tv", "Landroid/widget/TextView;");
    jobject tvObject = env->GetObjectField(thiz, tvFieldId);
    jclass tvClass = env->GetObjectClass(tvObject);
    //获取setText办法ID
    jmethodID methodId = env->GetMethodID(tvClass, "setText", "([CII)V");
    //获取setText所需的参数
    //先将num转化为jstring
    char buf[64];
    sprintf(buf, "%d", num);
    jstring pJstring = env->NewStringUTF(buf);
    const char *value = env->GetStringUTFChars(pJstring, JNI_FALSE);
    //创立char数组,长度为字符串num的长度
    jcharArray charArray = env->NewCharArray(strlen(value));
    //拓荒jchar内存空间
    jchar *pArray = (jchar *) calloc(strlen(value), sizeof(jchar));
    //将num的值缓冲到内存空间中
    for (int i = 0; i < strlen(value); ++i) {
        *(pArray + i) = *(value + i);
    }
    //将缓冲的值写入到char数组中
    env->SetCharArrayRegion(charArray, 0, strlen(value), pArray);
    //调用setText办法
    env->CallVoidMethod(tvObject, methodId, charArray, 0, env->GetArrayLength(charArray));
    //开释资源
    env->ReleaseCharArrayElements(charArray, env->GetCharArrayElements(charArray, JNI_FALSE), 0);
    free(pArray);
    pArray = NULL;
}
static const JNINativeMethod nativeMethod[] = {
    /*
    参数1:java中要注册的native办法名
    参数2:办法签名
    参数3:对应映射到C/C++中的函数指针
    */
        {"jniTest", "()V;", (void *) native_jniTest},
};
//System.loadLibrary()履行时会调用此办法
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
    JNIEnv *env = NULL;
    // 初始化JNIEnv
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_FALSE;
    }
    // 找到需求动态注册的java类
    jclass jniClass = env->FindClass("com/jason/jni/MainActivity");
    if (nullptr == jniClass) {
        return JNI_FALSE;
    }
    // 动态注册
    if (env->RegisterNatives(jniClass, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0])) != JNI_OK) {
        return JNI_FALSE;
    }
    // 返回JNI运用的版别
    return JNI_VERSION_1_4;
}

留意,在Android工程中要扫除对native办法以及地点类的混杂(java工程不需求),否则要注册的java类和java函数会找不到。proguard-rules.pro中增加

# 设置一切 native 办法不被混杂
-keepclasseswithmembernames class * {
    native <methods>;
}
# 不混杂类
-keep class com.jason.jni.** { *; }

到这儿,你应该了解jni的根本运用姿态了,剩下的便是不断的实践来巩固技能。附上Demo源码:gitee.com/jasonwan/JN…

参阅文章

JNI办法注册源码剖析

NDK 系列(5):JNI 从入门到实践,爆肝万字详解!

根底JNI语法和常见运用