Android JNI编程入门

来源:互联网 发布:我的世界方块数据值 编辑:程序博客网 时间:2024/06/03 02:27

JNI概述

JNI的全称是Java Native Interface,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)

使用Java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如:

  1. 使用一些旧的库

  2. 与硬件、操作系统进行交互

  3. 提高程序的性能

  4. 提高应用的安全性

那么怎么使用JNI呢,一般情况下我们首先是将写好的C/C++代码编译成对应平台的动态库(windows一般是dll文件,linux一般是so文件等),这里我们是针对Android平台,所以只讨论so库

  • 打包成so库会更安全一点,但肯定不是完全安全,只是相对反编译Java的class字节码文件来说,反汇编so动态库来分析程序的逻辑要复杂得多,没那么容易被破解。很多SDK,都使用了so库,例如百度SDK,微信SDK。

开始JNI编程之前,肯定要配置所支持的环境,请移步通过CMake在AndroidStudio项目中引入JNI编程。

下面就开始介绍Android中JNI编程的入门知识。

JNI函数的注册

其实就是Java的native方法与C/C++中的函数的连接,使二者能够识别彼此。

先看两个文件。

MainActivity.java

package com.tsnt.jni.androidjnidemo;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        TextView tv = (TextView) findViewById(R.id.sample_text);        TextView tv1 = (TextView) findViewById(R.id.sample_text1);        tv.setText(stringFromJNI());        tv1.setText(getStringFromJNI());    }    //声明两个本地方法    public native String stringFromJNI();    public native String getStringFromJNI();    //加载本地库native-lib    static {        System.loadLibrary("native-lib");    }}

native-lib.cpp

//类似Java中的导包#include <jni.h>#include <string>//extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码//加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的extern "C"//静态注册的方法//JNIEnv:它指向一个函数表,该函数表指向一系列的JNI函数,我们通过调用这些JNI函数可以实现与Java层的交互jstring Java_com_tsnt_jni_androidjnidemo_MainActivity_getStringFromJNIStatically(        JNIEnv *env,        jobject obj) {    //声明一个string类型变量    std::string hello = "Hello from native -- registered statically";    return env->NewStringUTF(hello.c_str());}//动态注册的方法jstring nativeGetStringFromJNIDynamically(JNIEnv *env, jobject obj) {    std::string hello = "Hello from native -- registered dynamically";    return env->NewStringUTF(hello.c_str());}//用来保存方法信息的数组JNINativeMethod nativeMethod[] = {{"getStringFromJNIDynamically", "()Ljava/lang/String;", (void *) nativeGetStringFromJNIDynamically},};//当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个函数并调用该函数//因此可以在该函数中做一些初始化的动作//其实这个函数就是相当于Activity中的onCreate()方法JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {    JNIEnv *env;    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {        return -1;    }    //获取MainActivity对象    jclass clz = env->FindClass("com/tsnt/jni/androidjnidemo/MainActivity");    //动态注册方法    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));    //返回JNI版本    return JNI_VERSION_1_4;}

最后程序成功运行起来,就是这样的:

这里写图片描述

其实通过代码中的注释,大家已经可以理解个大概了,下面进行进一步分析。

静态注册

我们在MainActivity中声明了getStringFromJNIStatically()为native方法,他对应的JNI函数就是Java_com_tsnt_jni_androidjnidemo_MainActivity_getStringFromJNIStatically()

在Java虚拟机加载so库时,会去寻找对应Java层的native方法。那它们两个究竟是怎么关联的呢?

我们仔细观察JNI函数名的构成形式是:Java_PkgName_ClassName_NativeMethodName,以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数了

这里简单介绍一下生成的JNI函数包含两个固定的参数变量,分别是JNIEnvjobject

  • jobject就是当前与之链接的native方法隶属的类对象(类似于Java中的this)

  • JNIEnv是个结构体,它指向一个函数表,该函数表指向一系列的JNI函数,我们通过调用这些JNI函数可以实现与Java层的交互。

这两个变量都是Java虚拟机生成并在调用时传递进来的。

然而静态注册有很多弊端,例如:

  1. 代码编写不方便,由于JNI层函数的名字必须遵循特定的格式,且名字特别长;

  2. 程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时(静态注册是用到时加载,动态注册一开始就加载好了)。

下面就来说动态注册。

动态注册

动态注册的原理是这样的:JNI 允许我们提供一个函数映射表,注册给 JVM,这样 JVM 就可以用函数映射表来调用相应的函数。

JNI_OnLoad()

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {    JNIEnv *env;    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {        return -1;    }    //获取MainActivity对象    jclass clz = env->FindClass("com/tsnt/jni/androidjnidemo/MainActivity");    //动态注册方法    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));    //返回JNI版本    return JNI_VERSION_1_4;}

当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个函数并调用该函数,因此可以在该函数中做一些初始化的动作,其实这个函数就是相当于Activity中的onCreate()方法

函数返回值表示当前使用的JNI的版本,其实类似于Android系统的API版本一样,不同的JNI版本中定义的一些不同的JNI函数。

该函数有两个参数,其中*jvm代表Java虚拟机实例

其中进行的操作主要就是,获取Java对象,完成动态注册

JNINativeMethod

注册方法的时候利用到了JNINativeMethod这个结构体,来看代码:

//用来保存方法信息的数组JNINativeMethod nativeMethod[] = {{"getStringFromJNIDynamically", "()Ljava/lang/String;", (void *) nativeGetStringFromJNIDynamically},};

nativeMethod其实就是一个J**NINativeMethod的数组**,JNINativeMethod是这样定义的:

typedef struct {    const char* name;//Java层native方法的名字    const char* signature;//Java层native方法的描述符    void*       fnPtr;//对应JNI函数的指针} JNINativeMethod;

JNI数据类型

上面我们提到JNI定义了一些自己的数据类型。这些数据类型是衔接Java层和C/C++层的,如果有一个对象传递下来,那么对于C/C++来说是没办法识别这个对象的,同样的如果C/C++的指针对于Java层来说它也是没办法识别的,那么就需要JNI进行匹配,所以需要定义一些自己的数据类型。

基本数据类型

这里写图片描述

引用数据类型

这里写图片描述

描述符

类描述符

前面为了获取Java的AndroidJNI对象,是通过调用FindClass()函数获取的,该函数参数只有一个字符串参数,我们发现该字符串如下所示:

"com/tsnt/jni/androidjnidemo/MainActivity"

其实这个就是JNI定义了对类的描述符,它的规则就是将"com.tsnt.jni.androidjnidemo.MainActivity"中的“.”用“/”代替

方法描述符

前面我们动态注册native方法的时候结构体JNINativeMethod中含有方法描述符,就是确定native方法的参数和返回值,我们这里定义native方法是这样的:

    public native String getStringFromJNIDynamically();

对应的描述符“()Ljava/lang/String;”

  • 括号中的值表示方法参数,没有参数,所以括号中为空。

  • 括号后的值表示方法返回值,为String类型。

再举个例子:

    public native void Fun(int a, int b)

对应的描述符:“(II)V”

方法描述符中的基本数据类型

这里写图片描述

方法描述符中的引用数据类型

这里写图片描述

  • 对象类型:以”L”开头,以”;”结尾,中间是用”/” 隔开,如上表第1个

  • 数组类型:以”[“开始,如上表第2个(n维数组的话,则是前面多少个”[“而已,如“[[[D”表示“double[][][]”)。

  • 对象数组类型:上述两者结合,如上表第3个。

对象类型与数组类型的举例

这里写图片描述

demo地址:AndroidJNIDemo

参考:

  1. JNI/NDK开发指南(开山篇)
  2. Android JNI编程—JNI基础
  3. Android的NDK开发(4)————JNI数据结构之JNINativeMethod
原创粉丝点击