码字辛苦!转载请注明出处!
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是不是非常简单呢~
最后,如果觉得有帮助的话,就给博主发个红包吧~