1、Jni基础知识
JNI是Java Native Interface的缩写,意思是Java的本地接口,这个本地接口主要指Java可以通过本地接口去和其他的编程语言通信,有时在开发某个功能时想使用之前的技术积累或封装好的模块,但不幸的是之前不是用Java开发的,那对于此中情况该如何处理呢?对于经过时间验证的可靠程序不可能轻易重写和修改,所以就需要JNI作为程序的中转枢纽;
- Jni使用场景
- 需要调用Java语言不支持的依赖时,
- 整合非Java语言开发的系统,如C、C++
- 节省运行时间提高运行效率,如:音视频等
- Jni类型和Java类型的映射关系
既然Jni是Java和其他语言的沟通桥梁,那么它既必须有一套基础协议作为与Java代码沟通的基础,这个基础就是类型的映射和签名,类型映射就是在Java类型和Jni中的类型建立一一对应关系,从而实现二者的类型可读性和唯一性,签名指Java中类型、方法、属性等在Java中的展现形式,根据最终的签名查找方法的对应关系;
- native方法与Jni映射实例
public static native String action(short s , int i, long l, float f, double d, char c,
boolean z, byte b, String str, Object obj, ArrayList<String> array,
int[] arr, Action action);
//生成的Jni对应的代码
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_action
(JNIEnv *, jclass, jshort, jint, jlong, jfloat, jdouble, jchar, jboolean, jbyte, jstring, jobject, jobject, jintArray, jobject);
- 基本数据映射关系
- 引用类型关系映射表
- Jni方法名:Java_类全路径_方法名
//native
public native void test();
//jni
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_test (JNIEnv *, jobject);
上面是Java代码中声明的test()转换后的Jni方法,此方法名称在编译javah文件时生成,在实现的C文件中重写并实现即可,方法的命名规则:
- Java:表示C++实现Java方法的前缀
- com_alex_kotlin_jni_JniTest:JniTest的类名全路径
- test:native方法名称
- 参数规则
- JNIEnv *:每个native函数的入口参数,执行JVM函数表的指针,JNIEnv即可在Native环境中使用Java资源
- jobject:调用java中native方法的实例或class对象,如果native方法是普通方法,则该参数是jobject,如果是静态方法,则是jclass
- 剩余参数为native方法的传入参数,此处为JNI中的映射类型(参照介绍映射关系)
- Jni签名
- 数据类型签名:见上面对照表
- 方法签名:将参数签名和返回值类型签名组合一起作为方法签名
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_setTest
(JNIEnv *env, jobject cls, jstring j_str) {
}
方法签名:(Ljava/lang/Object,Ljava/lang/String)Lava/lang/String
1.1、 JNI 函数注册
- 静态注册
静态注册JNI方法很简单,我们在Java中声明native方法后,会执行Java命令编译和生成Jni方法:
javac ***
javah ***
在执行javah的命令后,系统会在之间文件处创建.h文件,当我们在Java层调用native方法时,系统就会根据JNI方法命名规则,按照JNI方法名寻找对应的方法,在找到JNI方法后保存JNI指针建立方法间的联系,下次调用时直接使用函数指针就可以,不过静态注册在初次调用时需要查找再建立关联,影响效率,与静态注册对应的就是动态注册,不需要编译和查找关联;
- 动态注册
- 在C++文件中实现native方法,此时方法名并没有严格要求
JNIEXPORT jstring JNICALL native_method(JNIEnv *env, jobject) {
return env->NewStringUTF("Register method in Jni");
};
- 创建注册的方法数组,在数组中建立Java方法和Jni方法的对应关系
static JNINativeMethod methods[] = {
//参数:1、Java声明的native方法名;2、方法签名;3、c中实现的方法名
{"method", "()Ljava/lang/String;", (void *) native_method},
};
- 在重写的JNI_OnLoad()中调用注册方法实现注册
// 声明动态注册对应的Java类的路径
static const char *const PACKAGE_NAME = "com/alex/kotlin/jni/JniTest";
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env; //获取JNIEnv
if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
return JNI_ERR;
}
jclass jclass1 = env->FindClass(PACKAGE_NAME); //根据类名获取jclass
if (jclass1 == NULL) {
return JNI_ERR;
}
jclassGlobal = static_cast<jclass>(env->NewWeakGlobalRef(jclass1)); //创建全局缓存jclass
env->DeleteLocalRef(jclass1); //释放局部变量
if (JNI_OK != env->RegisterNatives(jclassGlobal, method, 1)) { //注册方法
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
在创建的C++文件中重写Jni.h中的JNI_OnLoad()方法,在JNI_OnLoad中首先根据报名获取Java类的jclass对象,然后全局缓存jclass对象,最后调用RegisterNatives()方法传入jclass和关系数组实现方法的注册
- 在UnLoad()中解除方法注解
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return;
}
env->UnregisterNatives(jclassGlobal); //解除注册
env->DeleteGlobalRef(jclassGlobal); //释放全局变量
}
2、Jni基本使用
在介绍完JNI基础知识后,一起来学习下JNI在开发中的基本使用,也就是Jni的基本语法,其实在上面动态注册时已经使用到了一些,这里根据使用频率介绍下最常用的方法;
2.1、字符串使用
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_setTest
(JNIEnv *env, jobject cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {};
jboolean copy;
c_str = env->GetStringUTFChars(j_str, ©); // 字符串访问
if(c_str == NULL){
return NULL;
}
sprintf(buff, "Jni %s", c_str); //字符串输出
env->ReleaseStringUTFChars(j_str, c_str); //字符串释放
return env->NewStringUTF(buff); // 字符串创建
}
- 访问字符串:GetStringUTFChars(j_str, ©)
- j_str:访问的本地字符串,这里为参数传入
- copy:表示引用是否拷贝,如果设置true则拷贝一份使用,false指向源字符串指针
- 对字符串的拷贝可能会因为内存问题而失败,在读取本地字符串之后,一定要检查字符串是否为NULL再使用,为空则返回
- ReleaseStringUTFChars释放字符串资源,GetStringUTFChars/ReleaseStringUTFChars处理UTF编码的字符串
- 释放引用的资源:ReleaseStringUTFChars(j_str, c_str);
- 在C语言中获取字符串,再使用完毕后需要释放资源,否则造成内存溢出
- 一般Get***和Release***成对使用,针对不容的编码选择不同的方法
- 创建字符串:NewStringUTF()
env->NewStringUTF(“this is string !");
- 其余字符串方法
- GetStringChars和ReleaseStringChars:用于创建和释放Unicode格式编码的字符串
const jchar *c_str = NULL;
c_str = env->GetStringChars(j_str, ©);
if (c_str == NULL) {
return NULL;
}
sprintf(buff,"Jni %s",c_str); //将字符串缓存到buff中
env->ReleaseStringChars(j_str, c_str);
- GetStringLength:获取Unicode编码的字符串的长度
jsize lenUtf = env->GetStringLength(j_str);
- GetStringUTFLength:获取UTF编码的字符串的长度
jsize lenUtf = env->GetStringUTFLength(j_str);
- GetStringCritical和ReleaseStringCritical:提高JVM返回源字符串直接指针的可能性,前面的获取字符串会拷贝并分配内存,此方法直接读取字符串无需分配内存,但不能在中间临界区间调用Jni方法和线程阻塞程序
const jchar *c_str = NULL;
char buff[128] = {};
jboolean copy;
c_str = env->GetStringCritical(j_str, ©); //读取字符串
if (c_str == NULL) {
return NULL;
}
sprintf(buff,"Jni %s",c_str);
env->ReleaseStringCritical(j_str, c_str); //释放字符串
return env->NewStringUTF(buff);
- GetStringRegion和GetStringUTFRegion:截取UTF和Unicode格式字符串的部分内容,会将源字符串拷贝到缓存区中
JNIEXPORT jstring JNICALL Java_com_alex_kotlin_jni_JniTest_setTest(JNIEnv *env, jobject cls, jstring j_str) {
char buff[128] = {};
jsize lenUtf = env->GetStringUTFLength(j_str); //读取字符串长度
env->GetStringUTFRegion(j_str, 0, 4, buff); //截取字符串0~4缓存到buff中
return env->NewStringUTF(buff); //创建字符串
}
//输入 "From Native !” ,输出结果:“From”
2.2、数组操作
- 访问Java传入的数组
Get<PrimitiveType>ArrayElements(ArrayType array, jboolean *isCopy)
- 参数isCopy表示是否拷贝返回副本,如果返回的指针指向Java数组地址而非副本,此时会阻止Java对数组的回收,但创建副本时可能因内存问题创建失败,使用前应检查NULL
- 释放数组使用:ReleaseArrayElements
- 获取数组和释放数组必须配对使用
- Release中的最后参数mode针对拷贝副本时可设置三种形式,可根据是否需要回写数组进行选择
* 0:将 elems 内容回写到 Java 数组并释放 elems 占用的空间,回写的意思是会去修改原Java中的数组
* JNI_COMMIT:将 elems 内容回写到 Java 数组,但不释放 elems 的空间;
* JNI_ABORT:不回写 elems 内容到 Java 数组,释放 elems 的空间。
- 使用实例
jint *array ;
jboolean jboolean1;
array = env->GetIntArrayElements(jintArray1, &jboolean1); //获取集合
if (array == NULL){
return;
}
array[2] = 5; //修改集合中的参数
env->ReleaseIntArrayElements(jintArray1,array,0); //释放array集合并写入Java集合
//在Java中调用Jni
int[] number = new int[]{1,2,3,4,5};
Log.e("Before Jni=====",number[2] + "");
test.getAction(number); //调用Jni方法
Log.e("After Jni=====",number[2] + "");
2019-04-29 22:09:07.698 15989-15989/com.alex.kotlin.jni E/Before Jni=====: 3
2019-04-29 22:09:07.698 15989-15989/com.alex.kotlin.jni E/After Jni=====: 5 //3改变程5
//使用JNI_ABORT
2019-04-29 22:11:55.485 16108-16108/com.alex.kotlin.jni E/Before Jni=====: 3
2019-04-29 22:11:55.485 16108-16108/com.alex.kotlin.jni E/After Jni=====: 3 //不变
由上面的运行结果知道,在ReleaseIntArrayElements()传入0时,Java层传入的数组在执行方法后被改变,从而验证了将数据回写到Java的结论,在使用JNI_ABORT时,Java中数据并未发生改变;
- GetArrayLength(j_array):获取Array的长度
- malloc(len):申请len长度的缓存区
- memset (c_array,0,len):初始化长度为len的array集合
- memcpy (buffer, data, len):将数组中指定长度的数据拷贝到buff数组中
jbyte* data = env->GetByteArrayElements(javaArray, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(javaArray, data, JNI_ABORT);
}
- GetIntArrayRegion(env,j_array,0,arr_len,c_array):复制源集合中指定长度的数据到目标集合中,和上面块拷贝功能一致
env->GetByteArrayRegion(javaArray, 0, len, buffer);
- 访问对象数组
- FindClass(env,"[I”):获取Int数据引用类型
- NewObjectArray(env,size,clsIntArray,NULL):创建一个数组对象
- NewIntArray(env,size):创建一个Int数组
3、C/C++与Java的访问
- C访问Java中静态方法,实现步骤
- 根据访问的类路径调用FindClass()获取Class对象
- 根据class、方法名、参数、返回值等条件获取Java方法MethodId
- 跟据class、methodId执行Java方法
public class Action {
public static void actionStatic(){
Log.e("=======","Static method from action in Java ") ;
}
}
//Jni方法
JNIEXPORT void JNICALL Java_com_alex_kotlin_jni_JniTest_test
(JNIEnv *env, jobject) {
jclass cls = NULL;
jmethodID method_Id = NULL;
cls = env->FindClass("com/alex/kotlin/jni/Action”); //查找Action类文件
method_Id = env->GetStaticMethodID(cls, "actionStatic", "()V”); //根据类、方法名、参数条件获取MethodId
env->CallStaticVoidMethod(cls,method_Id); //调用静态方法
env->DeleteGlobalRef(cls); //释放class
}
2019-04-27 17:25:27.580 10961-10961/com.alex.kotlin.jni E/=======: Static method from action in Java
- 访问Java成员方法,访问步骤:
- 根据类的全路径查找class
- 根据方法签名,查找构造函数的方法methodId
- 执行构造函数方,创建类的实例
- 根据方法名、参数、返回值查找调用方法的id
- 使用创建的实例调用相应的方法
public class Action {
public void action(){
Log.e("=======","Method from action in Java ") ;
}
}
//Jni中代码
jclass clss = NULL;
jmethodID method_Id = NULL;
jmethodID construct_Id = NULL;
jobject obj = NULL;
clss = env->FindClass("com/alex/kotlin/jni/Action”); //查找Action类
construct_Id = env->GetMethodID(clss, "<init>", "()V”); //获取构造函数方法的Id
obj = env->NewObject(clss, construct_Id); //创建构造Action实例
method_Id = env->GetMethodID(clss, "action", "()V”); //创建action()方法的Id
env->CallVoidMethod(obj,method_Id); //调用action()方法
2019-04-27 17:42:31.774 11880-11880/com.alex.kotlin.jni E/=======: Method from action in Java
- 访问Java静态实例,访问步骤:
- 查找类的class
- 根据属性名称、属性的数据类型获取fieldId
- 根据fieldId访问属性
public class Action {
private static int number = 100;
public int getNumber() {
return number;
}
public void setNumber(int number) {
Action.number = number;
}
}
- 声明访问变量的native方法
public native void getStaticInstance();
- 在Jni方法中访问静态变量
cls = env->FindClass("com/alex/kotlin/jni/Action”); //获取功能类
field_id = env->GetStaticFieldID(cls, "number", "I”); //获取静态变量Id
number = env->GetStaticIntField(cls, field_id); //从类中获取静态变量
jint num = 555;
env->SetStaticIntField(cls, field_id, num); //为静态变量赋值
- 调用和执行方法
Action action = new Action();
action.setMessage("Message in Java");
action.setNumber(123);
Log.e("Before======", action.getNumber() + "");
test.getStaticInstance();
Log.e("After Jni======", action.getNumber() + "");
2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/Before======: 123
2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/After Jni======: 555
- 访问Java成员实例(需要传入实例)
- 根据传入的object获取类的class
- 根据参数名、参数类型获取fieldId
- 根据fieldId和object访问属性
public class Action {
private String message = null;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
- 声明访问变量的native方法
public native void getInstance(Action action);
- Jni中读取成员变量
cls = env->GetObjectClass(obj); //从参数object中获取class
field_id = env->GetFieldID(cls, "message", "Ljava/lang/String;”); //根据参数名和类型获取Field_Id
str = static_cast<jstring>(env->GetObjectField(obj, field_id)); //根据field_Id从obj中获取实例
new_str = env->NewStringUTF("Message in Jni”); //创建新的String
env->SetObjectField(obj, field_id, new_str); //设置Obj中的数值
- 访问静态实例
Log.e("Before======", action.getMessage() + "");
test.getInstance(action);
Log.e("After Jni======", action.getMessage() + "");
2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/Before======: Message in Java
2019-04-27 21:32:51.592 15022-15022/com.alex.kotlin.jni E/After Jni======: Message in Jni
- 访问构造函数:由上面的实例知道访问构造函数分三步
- FindClass()查找类的jclass
- GetMethodID(clss, “”, "()V”):获取构造函数的MethosId
- NewObject(clss, construct_Id):创建实例
- 访问父类的方法
- FindClass()查找类的jclass
- GetMethodID(clss, “”, "()V”):获取构造函数的MethosId
- NewObject(clss, construct_Id):创建类的实例
- 使用FindClas s查找父类的jclass对象
- 获取父类方法的MethodId
- 调用父类方法,此处需要同时传入子类和父类的对象
public class Action {
public void action(){
Log.e("==========","Action in Action");
}
}
public class ChildAction extends Action {
@Override
public void action() {
Log.e("==========","Action in ChildAction");
}
}
cls = env->FindClass("com.alex.kotlin.jni.Action”); //1、
jmethodID1 = env->GetMethodID(cls, "<init>", "()V”); //2、
jobject1 = env->NewObject(cls, jmethodID1); //3、
cls_parent = env->FindClass("com.alex.kotlin.jni.ChildAction”); //4、
jmethodID2 = env->GetMethodID(cls_parent, "action()", "()V”); //5、
env->CallNonvirtualVoidMethod(jobject1, cls_parent, jmethodID2); //6、
4、引用类型
- 局部引用
- 局部引用不能跨方法和跨线程使用,相当于Java中的局部变量
- 会阻止GC的回收,在方法结束返回Java后对象没被引用会自动释放,如果被引用则阻止回收
- 在方法中使用基本方法创建的都是局部变量,可以使用DeleteLocalRef释放
- 局部变量使用完后要及时释放资源,否则当变量个数超过512个时抛出异常
- 推荐使用Push/PopLocalFrame(创建一个局部变量的引用栈),在方法的入口使用PushLocalFrame,在每个返回的地方调用PopLocalFrame,这样在函数中创建的任何变量都会被释放;
- 全局引用
- 可以跨方法、跨线程使用,相当于Java中的成员属性
- 只能通过NewGlobalRef函数创建
- 会组织对象被GC回收,只有手动调用DeleteGlobalRef释放
- 弱全局引用
- 使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放
- 可以跨方法、跨线程使用
- 不会阻止GC回收,当内存不足时会被回收,相当于Java中的若引用
- 在使用时需要检查引用的对象是否被回收,所以每次引用之前需要做非空判断
- 引用比较
- IsSameObject(env, obj1, obj2):如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)
- 局部引用和全局引用使用IsSameObject与NULL比较判断是否为null
- IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不同于局部引用和全局引用,此时判断对象是否被回收
- 管理引用规则
- 不要造成全局引用和弱引用的增加
- 不要在函数轨迹上遗漏任何的局部引用
- 在方法对外返回实例时,要注释清楚返回的是全局、局部变量
5、其他知识
5.1、缓存
- 使用时缓存
- 使用静态字段缓存数据,在第一次加载初始化,之后直接使用缓存数据
- 不能缓存局部引用,局部引用释放后容易造成缓存的空指针
- 静态初始化时缓存
- 在引入库时直接调用native方法initIDs
- 在c文件中实现initIds方法,在其中缓存资源
static {
System.loadLibrary("AccessCache");
initIDs();
}
5.2、Jni异常处理
- Jni中没有像try…catch()异常处理机制
- Jni中抛出异常程序不会立刻执行,此时为了停止程序调用ThrowNew()手动抛出异常后调用return结束函数
- Jni中捕获异常的方法
- ExceptionCheck():检查Java是否抛出异常
- ExceptionDescribe():打印Java异常堆栈信息
- ExceptionClear():清除异常堆栈信息
- ThrowNew():手动抛出一个java.lang.Exception异常
//Java 抛出异常代码
public void actionException() throws Exception {
throw new Exception("Java 抛出异常");
}
//C文件中调用
jmethodID1 = env->GetMethodID(cls, "actionException", "()V");
env->CallVoidMethod(obj, jmethodID1);
if (env->ExceptionCheck()) { //检查并抛出异常
env->ExceptionDescribe();
env->ExceptionClear();
env->ThrowNew(env->FindClass("java/lang/Exception"), "Jni 抛出异常");
}
//运行结果
2019-04-29 19:12:54.919 32350-32350/com.alex.kotlin.jni W/System.err: java.lang.Exception: Java 抛出异常
2019-04-29 19:12:54.920 32350-32350/com.alex.kotlin.jni E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.alex.kotlin.jni, PID: 32350
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.alex.kotlin.jni/com.alex.kotlin.jni.MainActivity}: java.lang.Exception: Jni 抛出异常
- 使用ExceptionOccurred捕获异常
env->CallVoidMethod(obj, jmethodID1);
if (env->ExceptionOccurred()) {
env->ExceptionDescribe();
env->ExceptionClear();
env->ThrowNew(env->FindClass("java/lang/Exception"), "Jni 抛出异常");
}
- 异常发生后释放资源
if ((*env)->ExceptionCheck(env)) { /* 异常检查 */
(*env)->ReleaseStringChars(env, jstr, cstr); // 发生异常后释放前面所分配的内存
return;
}
6、使用Jni生成So库文件
对于我们正常开发来说,直接使用Jni的场景很少,一般Jni的方法都会封装在So库中供Java层调用,现在就根据上面的Jni知识利用AS生成一个So库。
6.1、Android Studio 配置Jni
- 下载NDK和构建工具
- 在Android Studio的SDK Manager中选择LLDB、CMake、NDK下载
- 配置ndk-build
- 在local.properties文件中配置ndk文件路径
ndk.dir=/Users/wuliangliang/Library/Android/sdk/ndk-bundle
- 创建C++项目
- 创建新的项目选择C++工程
- 创建完成后,项目中首先会有一个cpp文件夹,其中已配置好Jni开发的Demo(可以作为参考)
- 在main文件夹下创建jni文件夹
- 配置CMakeList.txt文件,C代码生成so库的配置文件
- 创建text或复制cpp文件夹下的CMakeList.txt
cmake_minimum_required(VERSION 3.4.1) :配置So库的最小版本
add_library( //每个C文件具备一个add_library
jni-lib // 配置So库名称
SHARED //设置So库设置为Shared共享库
test.cpp) // 源C文件的相对路径
find_library( //,
log-lib
log)
target_link_libraries(
jni-lib // 指定目标库
${log-lib}) // 将目标库连接到NDK的日志库
上面配置中:
- add_library()作用是创建并命名一个So库,“jni-lib”就是最终生成So库的名程,最终生成的So库名称为“linjni-lib.so”文件
- test.cpp为对应So库的C代码文件,系统在编译时会自动定位和关联此文件,
- 由于NDK已经是CMake搜索的一部分,所以在使用时只需要向NDK设置要使用库的名称,在配置文件中使用find_library()定位Ndk,并将其路径存储为变量设,在之后构建脚本时使用此变量待指NDK,此处设置的log-lib就是NDK的变量
- 为了保证原生库可以在log中调用函数,需要使用target_link_libraries()命令关联库
- build.gradle中配置:关联CMakeList的配置文件路径和Jni文件路径
android{
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt” //配置CMakeList的文件路径
}
}
sourceSets { main { jni.srcDirs = ['src/main/jni', 'src/main/jni/'] } } //配置Jni文件输出路径
}
6.2、创建Native代码,生成并使用So库
- 创建Java文件并声明native方法
public class JniTest {
static {
System.loadLibrary("jni-lib”); //引入so库
}
public static native String test(); //配置native方法
}
- 在jni文件夹下生成编译后的.h文件
- 创建cpp文件实现native方法
JNIEXPORT void JNICALL Java_com_alex_kotlin_jni_JniTest_test
(JNIEnv *env, jobject) {
jclass cls = NULL;
jmethodID method_Id = NULL;
cls = env->FindClass("com/alex/kotlin/jni/Action");
if (cls == NULL) {
return;
}
method_Id = env->GetStaticMethodID(cls, "test", "()V");
env->CallStaticVoidMethod(cls, method_Id);
env->DeleteGlobalRef(cls);
}
这里调用FindClass()根据类名获取Class对象,然后使用全局变量保存Class对象,然后查找并调用actionStatic()
- 根据so库名称配置CMakeList.txt后执行Make Project,系统会自动在build文件夹下创建so库
- 调用native方法
JniTest test = new JniTest();
tv.setText(test.test());
关于Jni的基本知识点和用法基本介绍完了,在一般的开发中可能使用的不多,但想做进一步的功能或优化时就经常会使用到,所以Jni也成为Android高级开发这必备基础知识,希望此篇文章的总结对需要的同学有所帮助;