十分钟学会安卓JNI

码字辛苦!转载请注明出处!

0.前言

记得第一次接触安卓JNI的时候,那叫一个苦啊,MK文件?不会写,JNI?不会写,Gradle配置?也不会写。

时间一晃就过去3年了,Android Studio已经由当时的1.3到了现在的3.1,最新版本的Android Studio,再也不用手写MK文件,手写JNI,手写Gradle配置了~

只要你熟练掌握JAVA和C语言基础,十分钟拿下JNI,完全不是问题!

那些上来就叫你写MK文件,叫你编译SO库的。身为一个安卓工程师,这些东西……

1.创建工程

首先要确认一下你的Android Studio版本是3.+,如果低于这个版本,那么你仍旧需要手写MK文件,手动编译SO库……

因此赶紧去升级到最新版本的Android Studio吧~

在新版的Android Studio中,只要在创建工程的时候勾选【Include C++ Support】,它就会自动为你创建好JNI的所有开发环境。

创建之后,它会自动下载Android NDK。保持网络通畅,去刷刷段子聊聊天,等待它Build完成就好~

在Build完成之后,它会给你来一段DEMO,用JAVA获取来自C++的字符串,并显示在TextView上。

我们来分析一下这个DEMO,以此来了解一下JNI的工作流程。

2.初始化链接库

想要在JAVA上调用C/C++语言,必须先把被编译成链接库的资源Load上来:

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

这个链接库的源码在工程的cpp文件夹里:

它通过Android Studio创建的环境,在Build时被编译在这里:

其中arm64啦,x86什么的是CPU架构,x86就是32位电脑所使用的CPU架构,x86_64即64位的x86架构CPU。

Android Studio默认是会编译全平台的链接库的,如果你不需要兼容某些架构,你可以在app的Gradle脚本中指定需要编译的架构:

android {

    ...

    defaultConfig {
        
        ...

        ndk {
            //设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
            abiFilters "armeabi-v7a", "arm64-v8a"
        }
    }
}

如此一来,AS在编译的时候,就不会再编译没有被声明的平台了:

 那么问题来了,如果不Load进来就调用其中的方法,会发生什么呢?

如果不Load进来就调用其中的方法,将会爆出UnsatisfiedLinkError:

所以,在使用C语言的代码前,一定要记得loadLibrary!

3.JAVA调用C语言代码

接下来进入使用JNI的正题,如何使用JAVA调用C语言的代码呢?

首先我们需要在JAVA上声明一个native方法:

不不不不是naive是native,你们这些人啊,too young , too naive!

native方法,就是在JAVA方法的前面加上native,这种方法是专门给C\C++调用的。

    public native void helloJNI();

接下来,AS会告诉你,嗨呀,在JNI上找不到这个方法,别慌,快使用万能键ALT+ENTER!

对,AS3.+再也不要什么javah啊,创建头文件这些麻烦事儿了!

直接就特么的给你把JNI代码写好了:

这里的extern "C"的作用是让C++支持调用C语言的方法,如果你不需要,可以去掉;

JNIEXPORT xxx JNICALL代表这是一个JNI方法,其中xxx是返回值类型,如果是空类型,这里就是void;

Java_代表这是一个Java方法;

com_eternity_jnilab_MainActivity_helloJNI这段是你方法所在的包名以及它的方法名,在Java中相当于:com.eternity.jnilab.MainActivity.helloJNI

那么接下来,我们返回一个String回去~

4.JNI数据类型

这里注意一下,JAVA对应的JNI数据类型(记笔记记笔记!):

等等,String型在哪?

C语言并没有String型,如果需要使用它,需要借助C++的string工具包,把它转换成字符指针(char*):

...
//导入string工具包
#include <string>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_eternity_jnilab_MainActivity_helloJNI(JNIEnv *env, jobject instance) {
    std::string hello = "丢雷楼某";
    return env->NewStringUTF(hello.c_str());
}

运行一下~

我们再来看看其他类型的传递方式,这里以int型作为示例:

JAVA部分:

    TextView tv = findViewById(R.id.sample_text);
    tv.setText(add(6, 6) + "");

    //JAVA原生方法声明
    public native int add(int a, int b);

JNI部分:

extern "C"
JNIEXPORT jint JNICALL
Java_com_eternity_jnilab_MainActivity_add(JNIEnv *env, jobject instance, jint a, jint b) {
    int result = a + b;
    return result;
}

可以看到,表格中的基础数据类型是可以无缝转换的~

5.C语言调用JAVA代码

在日常开发中,我们经常会遇到需要把C语言的数据回传给JAVA的情况,这时候怎么办呢?

5.1同步调用

如果是在同一条线程里调用JAVA代码,非常简单:

JAVA部分:

    public void callMeBaby(String msg) {
        tv.setText(msg);
    }

C++部分:

    //获取JAVA类
    jclass clazz = env->FindClass("com/eternity/jnilab/MainActivity");
    //获取方法ID
    jmethodID methodID = env->GetMethodID(clazz, "callMeBaby", "(Ljava/lang/String;)V");
    std::string msg = "I Love U";
    env->CallVoidMethod(instance, methodID, env->NewStringUTF(msg.c_str()));

FindClass的入参是包名+类名的路径

GetMethodID的入参是JAVA类,方法名,方法签名

方法签名包含两个部分:

括号里的内容是回传的数据内容,(Ljava/lang/String;)代表需要回传一个String型数据;

括号外面的V代表这是一个void方法;

来看看GetMethodID第三个参数是怎么表示的:

类型 签名
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V
Object Ljava/lang/Object;
数组 [

 有点难理解?举个栗子!

JAVA方法:

    public int add(boolean excuted, int result) {
        return result;
    }

 C++获取:

    jmethodID methodID = env->GetMethodID(clazz, "add", "(ZI)I");

签名中的ZI代表回传一个boolean类型,一个int类型,括号外的I代表调用后,会返回一个int类型。

JAVA方法:

    public boolean getData(byte[] data) {
        return true;
    }

 C++获取:

    jmethodID methodID = env->GetMethodID(clazz, "getData", "([B)Z");

签名中的[B代表回传一个byte[]数组,括号外的Z代表调用后,会返回一个boolean类型。 

之所以boolean类型是Z,是因为B已经被byte给占了呀,不要觉得奇怪!

5.2异步调用 

从上面的代码,可以看到,C调用JAVA方法离不开JNIEnv。

我们先来认识一下JNIEnv:

JNIEnv是JAVA与C沟通的桥梁,他弥补了JAVA与C有差异的部分,可以视作为外交官一样的存在。

JNIEnv一般是是由虚拟机传入,而且与线程相关的变量,也就说线程A不能使用线程B的 JNIEnv,因此,我们需要一个方法来获取当前线程的JNIEnv:

在这之前,我们需要拿到JAVA虚拟机对象,

JAVA虚拟机对象只能从JAVA线程中获取到,因此多数的SO库都会要求在Load之后,调用初始化方法。

现在我们就来写一个初始化的方法:

JAVA部分:

    //初始化JNI库
    public native void init();

C++部分:

//声明一个静态变量
static JavaVM *JVM

extern "C"
JNIEXPORT void JNICALL
Java_com_eternity_jnilab_MainActivity_init(JNIEnv *env, jobject instance) {
    //获取Java虚拟机,赋值给静态变量
    env->GetJavaVM(&JVM);
}

然后通过Java虚拟机获取到当前线程的JNIEnv:

JNIEnv *getCurrentJNIEnv() {
    if (JVM != NULL) {
        JNIEnv *env_new;
        JVM->AttachCurrentThread(&env_new, NULL);
        return env_new;
    } else {
        return NULL;
    }
}

在JAVA上创建给C语言用的回调:

    TextView tv;

    //提供给C语言回调的方法
    public void callMeBaby(final String msg){

        //在主线程运行
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                tv.setText(msg);
            }
        });
    }

因为回调线程是C语言的线程,他们是没有办法获取JAVA方法的,

因此我们要把JAVA的回调方法保存起来:

static JavaVM *JVM;
static jobject objectMainActivity;
static jmethodID methodCallMeBaby;

//还是上面的初始化方法
extern "C"
JNIEXPORT void JNICALL
Java_com_eternity_jnilab_MainActivity_init(JNIEnv *env, jobject instance) {
    //获取Java虚拟机,赋值给静态变量
    env->GetJavaVM(&JVM);
    //获取Java对象并做static强引用
    objectMainActivity = env->NewGlobalRef(instance);
    //获取该对象的Java类
    jclass clazz = env->GetObjectClass(objectMainActivity);
    methodCallMeBaby = env->GetMethodID(clazz, "callMeBaby", "(Ljava/lang/String;)V");
}

OK,现在我们在C语言的线程里调用JAVA:

JAVA部分:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        init();
        startCThread();
    }

C++部分:

void *callingJava(void *arg) {
    JNIEnv *jniEnv = getCurrentJNIEnv();
    if (jniEnv != NULL) {
        std::string msg = "I'm fucking love u!";
        jmethodID callJavaMethod = jniEnv->GetMethodID(jniEnv->GetObjectClass(objectMainActivity),
                                                       "callMeBaby", "(Ljava/lang/String;)V");
        jniEnv->CallVoidMethod(objectMainActivity, callJavaMethod,
                               jniEnv->NewStringUTF(msg.c_str()));
    }
    return NULL;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_eternity_jnilab_MainActivity_startCThread(JNIEnv *env, jobject instance) {
    //创建一个C语言的线程,执行上面的callingJava方法
    pthread_t pthread;
    pthread_create(&pthread, NULL, callingJava, NULL);
}

运行结果:

怎么样,JNI是不是非常简单呢~

最后,如果觉得有帮助的话,就给博主发个红包吧~

猜你喜欢

转载自blog.csdn.net/u014653815/article/details/81092213