Android JNI编程入门

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值