记得第一次接触安卓JNI的时候,那叫一个苦啊,MK文件?不会写,JNI?不会写,Gradle配置?也不会写。
时间一晃就过去3年了,Android Studio已经由当时的1.3到了现在的3.1,最新版本的Android Studio,再也不用手写MK文件,手写JNI了~
只要你熟练掌握JAVA和C语言基础,十分钟拿下JNI,完全不是问题!
那些上来就叫你写MK文件,叫你编译SO库的。身为一个安卓工程师,这些东西……
首先要确认一下你的Android Studio版本是3.0以上,如果低于这个版本,那么你仍旧需要手写MK文件,手动编译SO库……
因此赶紧去升级到最新版本的Android Studio吧~
在新版的Android Studio中,只要在创建工程的时候勾选【Include C++ Support】,它就会自动为你创建好JNI的所有开发环境。
在最新的3.3上,则是选择Native C++项目:
创建之后,它会自动下载Android NDK。保持网络通畅,去刷刷段子聊聊天,等待它Build完成就好~
在Build完成之后,它会给你来一段FreeStyle,用JAVA获取来自C++的字符串,并显示在TextView上。
我们来分析一下这个DEMO,以此来了解一下JNI的工作流程。
想要在JAVA上调用SO库,必须先把被编译成SO库的资源Load上来:
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
这个链接库的源码在工程的cpp文件夹里:
它通过Android Studio创建的环境,在Build时被编译在这里:
JAVA只能调用被编译成库的C\C++代码。
其中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:
所以,在使用SO库的代码前,一定要记得loadLibrary!
接下来进入使用JNI的正题,如何使用JAVA调用C语言的代码呢?
首先我们需要在JAVA上声明一个native方法:
不不不不是naive是native,你们这些人啊,too young , too naive!
native方法,就是在JAVA方法的前面加上native,这种方法是专门给JAVA调用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回去~
这里注意一下,JAVA对应的JNI数据类型(记笔记记笔记!):
等等,String型在哪?
C语言并没有String型,如果需要使用它,需要借助C++的string工具包,把它转换成字符指针(char*):
...
//导入string工具包
#include
extern "C"
JNIEXPORT jstring JNICALL
Java_com_eternity_jnilab_MainActivity_helloJNI(JNIEnv *env, jobject instance) {
std::string hello = "丢雷楼某";
return env->NewStringUTF(hello.c_str());
//这里的->相当于Java的 .this. 即指针引用
//如果要强行翻译成Java,就是env.this.NewStringUTF(hello.c_str())
}
然后把这个值设置到TextView上:
TextView tv = findViewById(R.id.sample_text);
tv.setText(helloJNI());
运行一下~
我们再来看看其他类型的传递方式,这里以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;
}
可以看到,表格中的基础数据类型是可以无缝转换的~
更多的类型转换可以参考:https://blog.csdn.net/kgdwbb/article/details/72810251
在日常开发中,我们经常会遇到需要把C语言的数据回传给JAVA的情况,这时候怎么办呢?
如果是在JAVA调用JNI的同一条线程里调用JAVA代码,非常简单:
JAVA部分:
public void callMeBaby(String msg) {
tv.setText(msg);
}
C++部分:
extern "C"
JNIEXPORT jint JNICALL
Java_com_eternity_jnilab_MainActivity_add(JNIEnv *env, jobject instance, jint a, jint b) {
//获取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方法;
最后通过env->CallVoidMethod/CallStaticVoidMethod等等与Java方法对应的JNI方法调用JAVA。
来看看GetMethodID第三个参数是怎么表示的:
类型 | 签名 |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
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给占了呀,不要觉得奇怪!
从上面的代码,可以看到,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");
}
注意:无论是jclass还是jobject都必须做强引用保存!否则会爆出JNI DETECTED ERROR IN APPLICATION: use of invalid jobject / jclass .
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!";
jniEnv->CallVoidMethod(objectMainActivity, methodCallMeBaby,
jniEnv->NewStringUTF(msg.c_str()));
}
//用完后要释放!
javaVM->DetachCurrentThread();
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);
}
运行结果:
我擦,这么麻烦的吗,可不可以用同步调用的方法呢?
OK,满足你的好奇心~
现在我们把线程方法里的调用方式改成同步调用的方式:
void *callingJava(void *arg) {
JNIEnv *jniEnv = getCurrentJNIEnv();
if (jniEnv != NULL) {
jclass clazz = jniEnv->FindClass("com/eternity/jnilab/MainActivity");
jmethodID methodID = jniEnv->GetMethodID(clazz, "callMeBaby", "(Ljava/lang/String;)V");
std::string msg = "I'm fucking love u!";
jniEnv->CallVoidMethod(clazz, methodID,
jniEnv->NewStringUTF(msg.c_str()));
}
return NULL;
}
再来执行一遍:
看到了吧,不属于调用线程的JNIEnv,是连JAVA类都找不到的,
现在知道为什么要把JAVA对象和方法都存起来了吧~
JAVA用习惯了,写C语言代码不回收内存,是我们没错了。
我们来看一段从C语言获取字节流的代码:
int decode(const char *buffer, int size) {
//因为是异步操作,获取当前线程的JNIEnv
JNIEnv *jniEnv = getCurrentJNIEnv();
if (jniEnv != NULL) {
//创建byte[]字节数组
jbyteArray bytes = jniEnv->NewByteArray(size);
//将字符指针*buffer中的数据强转成byte型,填充到数组中
jniEnv->SetByteArrayRegion(bytes, 0, size, reinterpret_cast(buffer));
//回传字节流
jniEnv->CallStaticVoidMethod(jniCallClass, onGetDataMethod, bytes, size);
return 0;
} else {
return 1;
}
}
乍看之下没什么问题,然而运行一段时间之后:
经典的内存溢出崩溃,看到没~
在JNI的世界里,大部分数据类型都是需要手动回收的:
不要手动释放(基本类型): jint , jlong , jchar
需要手动释放(引用类型,数组家族): jstring,jobject ,jobjectArray,jintArray ,jclass ,jmethodID
// 创建 jstring 和 char*
jstring jstr = jniEnv->CallObjectMethod(jniEnv, mPerson, getName);
char* cstr = (char*) jniEnv->GetStringUTFChars(jniEnv,jstr, 0);
// 释放
jniEnv->ReleaseStringUTFChars(jniEnv, jstr, cstr);
jniEnv->DeleteLocalRef(jniEnv, jstr);
jniEnv->DeleteLocalRef(jniEnv, XXX);
jbyteArray audioArray = jnienv->NewByteArray(frameSize);
jnienv->DeleteLocalRef(audioArray);
jbyte* array= jniEnv->GetByteArrayElements(env,jarray,&isCopy);
jniEnv->ReleaseByteArrayElements(env,jarray,array,0);
jobject ref= env->NewGlobalRef(customObj);
env->DeleteGlobalRef(customObj);
参考资料:https://blog.csdn.net/c1481118216/article/details/77727573
记笔记记笔记!
好了,项目搞完,你想把C\C++中的方法保存起来下次使用,或者做成SDK?OJBK!
在本文第二章就讲到了SO文件的编译位置,那么是不是拷出来随便改个名字就行了呢?
告诉你:
很明显,拷出来直接改名字是不行的,和JAVA类一样,SO库也是有态度的,行不更名,坐不改姓!
但是,肯定不能让别人拿到的SO库就叫native-lib吧,这样LOW爆了,
那我们来看看正确的改名方式:
首先我们要找到C\C++文件的“户口簿”,它在Extenal Build Files\CMakeLists.txt这里:
打开它,我们会看到和Gradle脚本格式非常相似的脚本:
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )
find_library( log-lib
log )
target_link_libraries( native-lib
${log-lib} )
其中:
add_library的作用是添加需要编译成SO文件的源文件;
find_library的作用是查找SO库
target_link_libraries的作用是连接SO库
他们的语法是:
add_library(生成库的名称
#生成后的库名字前会加上lib
#如:此处配置native-lib
#生成的就是libnative-lib,安卓只能加载名字前面带lib的SO库
库类型
#SHARED共享/STATIC静态
#静态库,名称以.a结尾,运行进程时就会载入,因此运行耗时较长,且安卓不能直接使用
#共享库,名称以.so结尾,使用时才会挂载
C\CPP文件
#支持多条,换行分割
)
find_library(别名
SO/A库
#这里不要写上库名前的lib和扩展名.so/.a
#但是库名前面必须带lib
#比如liblog.so,这里就写作log
)
target_link_libraries(被连接的库
${别名})
也就是说,只要把add_library后面的名称改掉,就会按照新的名称生成SO库了。
Android Studio默认会在find_library引入log(输出日志的库),这是NDK自带的库,它的位置在你的SDK目录\ndk-bundle\platforms\android-28\arch-arm\usr\lib里,如果不用可以去掉:
拿到了SO库,我们在新项目里如何使用呢?
非常简单,在项目的src/main目录下创建一个jniLibs目录,然后把生成的库丢进去:
使用和常规方式是一样的,注意调用的JAVA类和方法名要保持和库中一致就行。
static {
System.loadLibrary("sub");
}
//SO库中的方法
public native int sub(int a, int b);
有些场景,我们需要调用已经写好的C\CPP文件,比如博主最近的一个项目,需要使用做视频编码的同事给的C代码。
首先直接把所有的c\cpp\h文件扔进CPP文件夹,不需要拷贝MK文件:
在native-lib中调用其中的方法:
//导入头文件
#include "stream.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_eternity_mediacodeclab_jni_JniCall_streamStartConnect(JNIEnv *env, jclass type,
jstring server_ip_,
jshort server_port,
jshort input_port) {
const char *server_ip = env->GetStringUTFChars(server_ip_, 0);
//这个方法来自stream.c
stream_start_connect(server_ip, server_port, input_port, decode, log);
env->ReleaseStringUTFChars(server_ip_, server_ip);
}
在编译的时候,在CMakeLists.txt里把这些文件合并到native-lib:
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp
#新增的文件
src/main/cpp/stream.c
src/main/cpp/reed_solomon.c)
这样就可以调用C\CPP文件中的方法了~
还记不记得AS默认导入的log-lib库呢?
它的作用就是让你在JNI中使用Log.e啦Log.d打印日志的,使用起来非常简单:
首先导入库,定义方法,这里以导入Log.e为例:
#include
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "CLogTag", __VA_ARGS__)
这里的CLogTag就是Log.e(TAG,MSG)中的TAG哟~
使用方法:
LOGE("C语言并不想理你并向你扔出了一个Log");
怎么样,JNI是不是非常简单呢~
最后,如果觉得有帮助的话,就给博主发个红包吧~