转载

Android进阶知识树——JNI和So库开发

JNI是Java Native Interface的缩写,意思是Java的本地接口,这个本地接口主要指Java可以通过本地接口去和其他的编程语言通信,有时在开发某个功能时想使用之前的技术积累或封装好的模块,但不幸的是之前不是用Java开发的,那对于此中情况该如何处理呢?对于经过时间验证的可靠程序不可能轻易重写和修改,所以就需要JNI作为程序的中转枢纽;

  • Jni使用场景
  1. 需要调用Java语言不支持的依赖时,
  2. 整合非Java语言开发的系统,如C、C++
  3. 节省运行时间提高运行效率,如:音视频等
  • Jni类型和Java类型的映射关系

既然Jni是Java和其他语言的沟通桥梁,那么它既必须有一套基础协议作为与Java代码沟通的基础,这个基础就是类型的映射和签名,类型映射就是在Java类型和Jni中的类型建立一一对应关系,从而实现二者的类型可读性和唯一性,签名指Java中类型、方法、属性等在Java中的展现形式,根据最终的签名查找方法的对应关系;

  1. 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);
复制代码
  1. 基本数据映射关系
    Android进阶知识树——JNI和So库开发
  2. 引用类型关系映射表
    Android进阶知识树——JNI和So库开发
  • 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文件中重写并实现即可,方法的命名规则:

  1. Java:表示C++实现Java方法的前缀
  2. com_alex_kotlin_jni_JniTest:JniTest的类名全路径
  3. test:native方法名称
  • 参数规则
  1. JNIEnv *:每个native函数的入口参数,执行JVM函数表的指针,JNIEnv即可在Native环境中使用Java资源
  2. jobject:调用java中native方法的实例或class对象,如果native方法是普通方法,则该参数是jobject,如果是静态方法,则是jclass
  3. 剩余参数为native方法的传入参数,此处为JNI中的映射类型(参照介绍映射关系)
  • Jni签名
  1. 数据类型签名:见上面对照表
  2. 方法签名:将参数签名和返回值类型签名组合一起作为方法签名
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指针建立方法间的联系,下次调用时直接使用函数指针就可以,不过静态注册在初次调用时需要查找再建立关联,影响效率,与静态注册对应的就是动态注册,不需要编译和查找关联;

  • 动态注册
  1. 在C++文件中实现native方法,此时方法名并没有严格要求
JNIEXPORT jstring JNICALL native_method(JNIEnv *env, jobject) {
    return env->NewStringUTF("Register method in Jni");
};
复制代码
  1. 创建注册的方法数组,在数组中建立Java方法和Jni方法的对应关系
static JNINativeMethod methods[] = {
    //参数:1、Java声明的native方法名;2、方法签名;3、c中实现的方法名
    {"method", "()Ljava/lang/String;", (void *) native_method},
};
复制代码
  1. 在重写的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, &copy)
  1. j_str:访问的本地字符串,这里为参数传入
  2. copy:表示引用是否拷贝,如果设置true则拷贝一份使用,false指向源字符串指针
  3. 对字符串的拷贝可能会因为内存问题而失败,在读取本地字符串之后,一定要检查字符串是否为NULL再使用,为空则返回
  4. ReleaseStringUTFChars释放字符串资源,GetStringUTFChars/ReleaseStringUTFChars处理UTF编码的字符串
  • 释放引用的资源:ReleaseStringUTFChars(j_str, c_str);
  1. 在C语言中获取字符串,再使用完毕后需要释放资源,否则造成内存溢出
  2. 一般Get***和Release***成对使用,针对不容的编码选择不同的方法
  • 创建字符串:NewStringUTF()
env->NewStringUTF(“this is string !");
复制代码
  • 其余字符串方法
  1. 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);
复制代码
  1. GetStringLength:获取Unicode编码的字符串的长度
jsize lenUtf = env->GetStringLength(j_str);
复制代码
  1. GetStringUTFLength:获取UTF编码的字符串的长度
jsize lenUtf = env->GetStringUTFLength(j_str);
复制代码
  1. 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);
复制代码
  1. 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)
复制代码
  1. 参数isCopy表示是否拷贝返回副本,如果返回的指针指向Java数组地址而非副本,此时会阻止Java对数组的回收,但创建副本时可能因内存问题创建失败,使用前应检查NULL
  • 释放数组使用:ReleaseArrayElements
  1. 获取数组和释放数组必须配对使用
  2. 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);
复制代码
  • 访问对象数组
  1. FindClass(env,"[I”):获取Int数据引用类型
  2. NewObjectArray(env,size,clsIntArray,NULL):创建一个数组对象
  3. NewIntArray(env,size):创建一个Int数组

3、C/C++与Java的访问

  • C访问Java中静态方法,实现步骤
  1. 根据访问的类路径调用FindClass()获取Class对象
  2. 根据class、方法名、参数、返回值等条件获取Java方法MethodId
  3. 跟据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成员方法,访问步骤:
  1. 根据类的全路径查找class
  2. 根据方法签名,查找构造函数的方法methodId
  3. 执行构造函数方,创建类的实例
  4. 根据方法名、参数、返回值查找调用方法的id
  5. 使用创建的实例调用相应的方法
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静态实例,访问步骤:
  1. 查找类的class
  2. 根据属性名称、属性的数据类型获取fieldId
  3. 根据fieldId访问属性
public class Action {
   private static int number = 100;
   public  int getNumber() {
     return number;
  }
  public  void setNumber(int number) {
     Action.number = number;
  }
}
复制代码
  1. 声明访问变量的native方法
public native void getStaticInstance();
复制代码
  1. 在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); //为静态变量赋值
复制代码
  1. 调用和执行方法
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成员实例(需要传入实例)
  1. 根据传入的object获取类的class
  2. 根据参数名、参数类型获取fieldId
  3. 根据fieldId和object访问属性
public class Action {
   private String message = null;
   public String getMessage() {
      return message;
   }
   public void setMessage(String message) {
      this.message = message;
   }
}
复制代码
  1. 声明访问变量的native方法
public native void getInstance(Action action);
复制代码
  1. 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中的数值
复制代码
  1. 访问静态实例
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
复制代码
  • 访问构造函数:由上面的实例知道访问构造函数分三步
  1. FindClass()查找类的jclass
  2. GetMethodID(clss, "", "()V”):获取构造函数的MethosId
  3. NewObject(clss, construct_Id):创建实例
  • 访问父类的方法
  1. FindClass()查找类的jclass
  2. GetMethodID(clss, "", "()V”):获取构造函数的MethosId
  3. NewObject(clss, construct_Id):创建类的实例
  4. 使用FindClas s查找父类的jclass对象
  5. 获取父类方法的MethodId
  6. 调用父类方法,此处需要同时传入子类和父类的对象
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、引用类型

  • 局部引用
  1. 局部引用不能跨方法和跨线程使用,相当于Java中的局部变量
  2. 会阻止GC的回收,在方法结束返回Java后对象没被引用会自动释放,如果被引用则阻止回收
  3. 在方法中使用基本方法创建的都是局部变量,可以使用DeleteLocalRef释放
  4. 局部变量使用完后要及时释放资源,否则当变量个数超过512个时抛出异常
  5. 推荐使用Push/PopLocalFrame(创建一个局部变量的引用栈),在方法的入口使用PushLocalFrame,在每个返回的地方调用PopLocalFrame,这样在函数中创建的任何变量都会被释放;
  • 全局引用
  1. 可以跨方法、跨线程使用,相当于Java中的成员属性
  2. 只能通过NewGlobalRef函数创建
  3. 会组织对象被GC回收,只有手动调用DeleteGlobalRef释放
  • 弱全局引用
  1. 使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放
  2. 可以跨方法、跨线程使用
  3. 不会阻止GC回收,当内存不足时会被回收,相当于Java中的若引用
  4. 在使用时需要检查引用的对象是否被回收,所以每次引用之前需要做非空判断
  • 引用比较
  1. IsSameObject(env, obj1, obj2):如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)
  2. 局部引用和全局引用使用IsSameObject与NULL比较判断是否为null
  3. IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不同于局部引用和全局引用,此时判断对象是否被回收
  • 管理引用规则
  1. 不要造成全局引用和弱引用的增加
  2. 不要在函数轨迹上遗漏任何的局部引用
  3. 在方法对外返回实例时,要注释清楚返回的是全局、局部变量

5、其他知识

5.1、缓存

  • 使用时缓存
  1. 使用静态字段缓存数据,在第一次加载初始化,之后直接使用缓存数据
  2. 不能缓存局部引用,局部引用释放后容易造成缓存的空指针
  • 静态初始化时缓存
  1. 在引入库时直接调用native方法initIDs
  2. 在c文件中实现initIds方法,在其中缓存资源
static { 
    System.loadLibrary("AccessCache"); 
    initIDs(); 
}
复制代码

5.2、Jni异常处理

  • Jni中没有像try…catch()异常处理机制
  • Jni中抛出异常程序不会立刻执行,此时为了停止程序调用ThrowNew()手动抛出异常后调用return结束函数
  • Jni中捕获异常的方法
  1. ExceptionCheck():检查Java是否抛出异常
  2. ExceptionDescribe():打印Java异常堆栈信息
  3. ExceptionClear():清除异常堆栈信息
  4. 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和构建工具
  1. 在Android Studio的SDK Manager中选择LLDB、CMake、NDK下载
    Android进阶知识树——JNI和So库开发
  • 配置ndk-build
  1. 在local.properties文件中配置ndk文件路径
ndk.dir=/Users/wuliangliang/Library/Android/sdk/ndk-bundle
复制代码
  • 创建C++项目
  1. 创建新的项目选择C++工程
  2. 创建完成后,项目中首先会有一个cpp文件夹,其中已配置好Jni开发的Demo(可以作为参考)
  3. 在main文件夹下创建jni文件夹
  • 配置CMakeList.txt文件,C代码生成so库的配置文件
  1. 创建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的日志库
复制代码

上面配置中:

  1. add_library()作用是创建并命名一个So库,“jni-lib”就是最终生成So库的名程,最终生成的So库名称为“linjni-lib.so”文件
  2. test.cpp为对应So库的C代码文件,系统在编译时会自动定位和关联此文件,
  3. 由于NDK已经是CMake搜索的一部分,所以在使用时只需要向NDK设置要使用库的名称,在配置文件中使用find_library()定位Ndk,并将其路径存储为变量设,在之后构建脚本时使用此变量待指NDK,此处设置的log-lib就是NDK的变量
  4. 为了保证原生库可以在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库
    Android进阶知识树——JNI和So库开发
  • 调用native方法
JniTest test = new JniTest();
tv.setText(test.test());
复制代码

关于Jni的基本知识点和用法基本介绍完了,在一般的开发中可能使用的不多,但想做进一步的功能或优化时就经常会使用到,所以Jni也成为Android高级开发这必备基础知识,希望此篇文章的总结对需要的同学有所帮助;

原文  https://juejin.im/post/5d7a0e015188255e577e0efc
正文到此结束
Loading...