JNI(一) JNI - NDK-交叉编译

JNI

1.1 JNI介绍

Java Nativie interface

Java 本地接口,JNI是Java调用本地语言的一种特性。通过Jni 可以使java与本地语言之间相互调用

  • 如java 与c/c++ 相互调用

1.2 实现步骤

    1. java中声明Native方法

      1. public native String stringFromJNI();
        
    2. javac 命令编译1中的java源文件得到class文件

    3. javah -jni命令导出JNI的头(.h)文件

    4. 使用java 需要交互的本地代码,实现在java中声明的native方法

      1. extern "C"
        JNIEXPORT jstring JNICALL
        Java_com_example_ndktest_NdkManager_stringFromJNI(JNIEnv *env, jobject thiz) {
            std::string hello = "Hello from C++";
            return env->NewStringUTF(hello.c_str());
        }
        
    5. 将本地代码编译成动态库

      1. windows下是.dll,linux下是.so ,Mac系统是.jnilib
    6. 通过java 命令执行java程序,实现java调用本地代码

1.3 native层通过jni 可以干什么

  • 创建,更新java对象(包括数组字符串)
  • 调用java方法
  • 加载类并获取类信息

1.3.1 对类的操作

  • GetObjectClass获取类class文件 一般获取参数中传递的自定义类型的class文件

  • FindClass(类的全路径) 获取class 文件 一般获取没在参数列表中 但需要使用的类的class 文件 参数是 目标类的路径

  • GetMethodID获取方法名

  • GetFieldID获取字段

  • 具体请看如下示例

    • extern "C"
      JNIEXPORT jobject JNICALL
      Java_com_example_ndktest_NdkManager_changName(JNIEnv *env, jobject thiz, jobject st, jstring name) {
          //反射java方法
          //1 获取java 对应的class 文件
          jclass stClass = env->GetObjectClass(st);
          // 2 找到要调用的方法 getName
          jmethodID setAge = env->GetMethodID(stClass, "setAge", "(I)V");
          jmethodID printInfo = env->GetStaticMethodID(stClass, "printInfo", "(Ljava/lang/String;)V");
          jmethodID getName = env->GetMethodID(stClass, "getName", "()Ljava/lang/String;");
          //3 调用 getName
          jstring sName = static_cast<jstring> (env->CallObjectMethod(st, getName));
         	// 将返回值jstring 转换为c可用的char
          const char *c = env->GetStringUTFChars(sName, 0);
          LOGE("获取到的名字是:%s", c);
      
          //释放局部引用
          env->ReleaseStringUTFChars(sName, c);
          // 调用setAge
          env->CallVoidMethod(st, setAge, 25);
          // 调用静态方法
          jstring str = env->NewStringUTF("printInfo");
          env->CallStaticVoidMethod(stClass, printInfo, str);
          //释放局部引用
          env->DeleteLocalRef(str);
          
      	//反射类属性
          //反射获取属性id
          jfieldID nameId = env->GetFieldID(stClass, "name", "Ljava/lang/String;");
          jfieldID ageId = env->GetFieldID(stClass, "age", "I");
          //设置属性id
          //  name
          env->SetObjectField(st, nameId, name);
          //  age
          env->SetIntField(st, ageId, 30);
      
          //传递参数object的方法 获取methodid
          jmethodID toString=env->GetMethodID(stClass,"toSting", "(Lcom/example/ndktest/Student;)V");
          //创建对象
          //1 首先获取class 在上面我们获取了Student的class文件这里就不再重复获取
          //2 获取构造方法
          jmethodID constour= env->GetMethodID(stClass,"<init>", "(Ljava/lang/String;I)V");
          //3 创建对象
          jobject st1=env->NewObject(stClass,constour,env->NewStringUTF("jesse"),20);
          // 4 调用 参数为 Studnet的方法
          env->CallVoidMethod(st,toString,st1);
          // 5 释放局部引用
          env->DeleteLocalRef(st1);
          env->DeleteLocalRef(stClass);
          return st;
      }
      

1.3.2 获取从java层传递过来的参数

  • 参数为数组

    • 引用类型数组都为jobjectArray

      • java

      • // 引用数据类型为参数
            public native int Test(int[] list, String[] str);
        
      • c++

      • extern "C"
        JNIEXPORT jint JNICALL
        Java_com_example_ndktest_NdkManager_Test(JNIEnv *env, jobject thiz, jintArray i_Array,
                                                 jobjectArray str) {
            // 获取数组首元素地址
            // 第二个参数
            // jboolean* isCopy
            // true 表示新申请内存 拷贝一个新数组
            // false 就是使用java数组
            jint *p = env->GetIntArrayElements(i_Array, NULL);
            // 如果是c环境 就需要如下使用 env-> 替换成(*env)->
            //jint *p1 =(*env).GetIntArrayElements(i_Array, NULL);
            // 因为 c环境下 JNIEnv 本身就是个指针,这里再次声明为指针就成了二级指针,所以要解一次引用拿到一级指针来操作
            //获取数组的长度
            jint length = env->GetArrayLength(i_Array);
            for (int i = 0; i < length; i++) {
                if (i == 3) {
                    *(p + i) = 10;
                }
                LOGE("获取的java 数组值为: %d", *(p + i));
            }
            // 释放数组
            // 参数1
            // 参数2
            // 参数3: mode 模式 有三个值
            // 0  刷新java数组 并释放c/c++ 数组
            // 1 = JNI_COMMIT:只刷新JAVA数组
            // 2 = JNI_ABORT : 只释放c/c++ 数组
            env->ReleaseIntArrayElements(i_Array, p, 0);
        	//类型数组通过如下方式遍历  首先获取数组长度 遍历 按下下标获取
            int count = env->GetArrayLength(str);
            for (int i = 0; i < count; i++) {
                // 获取jstring
                jstring s1 = static_cast<jstring>(env->GetObjectArrayElement(str, i));
                // 转为c/c++ 中可操作的char
                const char *c = env->GetStringUTFChars(s1, 0);
                LOGE("获取的java str数组值为: %s", c);
                // 释放
                env->ReleaseStringUTFChars(s1, c);
            }
            return count;
        }
        
  • 参数为基本数据类型

    • 则可直接使用

      • java

      • // 基本数据类型为参数
           public native int Test1(int i);
        
      • c++

      • extern "C"
        JNIEXPORT jint JNICALL
        Java_com_example_ndktest_NdkManager_Test1(JNIEnv *env, jobject thiz, jint i) {
            return i + 1;
        }
        
  • 参数为自定义类型

    • 则需通过操作类的方法操作
    • 请参考上面1.3.1中的例子

1.4 JNI 语法

  • java中声明 native方法

    • public native String stringFromJNI();
      
  • Native中实现被声明的native方法

    • extern "C"
      JNIEXPORT jstring JNICALL
      Java_com_example_ndktest_NdkManager_stringFromJNI(JNIEnv *env, jobject thiz) {
          std::string hello = "Hello from C++";
          return env->NewStringUTF(hello.c_str());
      }
      

1.4.1 Native中代码解释

1.4.1.1 extern “C”

1.4.1.2 JNIEXPORT

宏定义:#define JNIEXPORT __attribute__ ((visibility ("default"))) 在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,定义为__attribute__ ((visibility ("default")))

  • 表示函数对外界可见

  • GCC 有个visibility属性, 该属性是说, 启用这个属性:

    • 当-fvisibility=hidden时,动态库中的函数默认是被隐藏的即 hidden。
    • 当-fvisibility=default时,动态库中的函数默认是可见的。

1.4.1.3 JNICALL

宏定义,在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,它是个空的宏定义: #define JNICALL,所以在 android 上删除它也可以

1.4.1.4 thiz

  • thiz 就是java中声明native方法的类 即调用native方法的类

1.4.1.5JNIEnv

  • JNIEnv类型实际上代表了Java环境,通过这个 JNIEnv* 指针,就可以对 Java 端的代码进行操作:
    • 调用 Java 函数
    • 操作 Java 对象
  • JNIEnv 的本质是一个与线程相关的结构体,每个线程存在一个JNIEnv

1.4.1.6 JavaVM

  • JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个
  • JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv,同时 JNIEnv 具有线程相关性,也就是 B 线程无法使用 A 线程的 JNIEnv

1.4.1.7 如何在native线程中使用JNIenv

  • 如果想在 native 线程中使用 JNIEnv* 需要使用 JVM 的 AttachCurrentThread 方法进行绑定,在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。

    • JavaVM *_vm;
      
      jint JNI_OnLoad(JavaVM* vm, void* reserved){
          _vm = vm;
          return JNI_VERSION_1_6;
       }
      
      void* threadTask(void* args){
          JNIEnv *env;
          // 通过 javavm 绑定当前线程赋值给env
          jint result = _vm->AttachCurrentThread(&env,0);
          if (result != JNI_OK){
              return 0;
          }
      
          // ...
      
          // 线程 task 执行完后不要忘记分离
          _vm->DetachCurrentThread();
      }
      
      extern "C"
      JNIEXPORT void JNICALL
      Java_com_example_ndktest_NdkManager_nativeThreadTest(JNIEnv *env, jobject thiz) {
          pthread_t pid;
          pthread_create(&pid,0,threadTask,0);
      }
      
    • 这里通过在JNI_OLoad中 保存的全局变量来实现,这个方式涉及到jni的注册方式 后面会详细说

1.5 JNI 注册类型

1.5.1 静态注册

  • 当Java层调用navtie函数时,会在JNI库中根据函数名查找对应的JNI函数。如果没找到,会报错。如果找到了,则会在native函数与JNI函数之间建立关联关系,其实就是保存JNI函数的函数指针。下次再调用native函数,就可以直接使用这个函数指针。

  • 语法

    • JNI函数名格式(包名里面的”.”需要改为”_”)
    • Java_ + 包名(com_example_ndktest)+ 类名(_MainActivity) + 函数名(_stringFromJNI)
  • 静态注册的缺点

    • 要求JNI函数的名字必须遵循JNI规范的命名格式;
    • 名字冗长,容易出错;
    • 初次调用会根据函数名去搜索JNI中对应的函数,会影响执行效率;
    • 需要编译所有声明了native函数的Java类,每个所生成的class文件都要用javah工具生成一个头文件;
  • 例子

    • 包名 com.example.ndktest

    • 类名NdkManager

    • java类

      • package com.example.ndktest;
        
        import java.util.ArrayList;
        import java.util.List;
        
        public class NdkManager {
            static {
                // native实现在ndktest 库内所以需加载该库 后面cmake中会说到
                System.loadLibrary("ndktest");
            }
            public native String stringFromJNI();
        }
        
        
    • native实现

      • #include <jni.h>
        #include <string>
        #include <android/log.h>
        
        // 定义log 宏 输出  __VA_ARGS__表示可变参数 代表了前面括号里的"..."
        #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI", __VA_ARGS__);
        
        extern "C"
        JNIEXPORT jstring JNICALL
        Java_com_example_ndktest_NdkManager_stringFromJNI(JNIEnv *env, jobject thiz) {
            std::string hello = "Hello from C++";
            // 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
            jclass thisclazz = env->GetObjectClass(thiz);
            // 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
            jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass", "()Ljava/lang/Class;");
            // 3. 执行 getClass 方法,获得 Class 对象
            jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
            // 4. 获取 Class 实例
            jclass clazz = env->GetObjectClass(clazz_instance);
            // 5. 根据 class  的 methodID
            jmethodID mid_getName = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;");
            // 6. 调用 getName 方法
            jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName));
            //打印 thiz类名
            LOGE("class name:%s", env->GetStringUTFChars(name, 0));
            
            return env->NewStringUTF(hello.c_str());
        }
        

1.5.2 动态注册

  • 通过提供一个函数映射表,注册给JVM虚拟机,这样JVM就可以用函数映射表来调用相应的函数,就不必通过函数名来查找需要调用的函数。

  • Java与JNI通过JNINativeMethod的结构来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:

    • typedef struct {
          const char* name; // 对应java中方法名
          const char* signature; // 方法签名
          void*       fnPtr;// /对应交互cpp中方法的 函数指针 (指向对应函数)
      } JNINativeMethod;
      
  • 创建映射表后,调用env->RegisterNatives函数将映射表注册给JVM;

    • 当Java层通过System.loadLibrary加载JNI库时,会在库中查JNI_OnLoad函数。
    • JNI_OnLoad为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他一些初始化工作。
  • 实例

    • Java 类名 NativeManager包名com.example.dynamicndk

      • public class NativeManager {
            // Used to load the 'dynamicndk' library on application startup.
            static {
                System.loadLibrary("dynamicndk");
            }
            // 动态注册
            public native String stringFromJNI();
            public native void Test();
            // native 线程 调用java
            public native void threadTest();
        }
        
    • cpp

      • #include <jni.h>
        #include <string>
        #include <android/log.h>
        
        #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI", __VA_ARGS__);
        
        JavaVM *_vm;//保存JavaVM 声明为全局变量 方便提取JNIEnv 或者又需要的时候用到
        
        
        char *mClassName = "com/example/dynamicndk/NativeManager";
        
        void Test(JNIEnv *env, jobject thiz) {
            LOGE("动态注册");
        }
        
        jstring stringFromJNI(JNIEnv *env, jobject thiz) {
            std::string hello = "Hello from C++";
        
            return env->NewStringUTF(hello.c_str());
        }
        // 创建要绑定的native函数 对应的映射表
        JNINativeMethod jniMethod[] = {
                {"Test",          "()V",                  (void *) Test},
                {"stringFromJNI", "()Ljava/lang/String;", (jstring *) stringFromJNI},
        };
        // System.loadLibrary 后第一个执行的函数
        int JNI_OnLoad(JavaVM *vm, void *unused) {
            LOGE("JniOnLoad");
            //保存为全局变量javaVM
            _vm = vm;
            // 保存全局变量JNIEnv
            JNIEnv *env = nullptr; //nullptr 取代NUll给指针赋值 也可以直接写0
            // 这里要传递env指针的地址进去方便给该地址指向的指针赋值
            int success = vm->GetEnv((void **) &env, JNI_VERSION_1_6);
        	//判断是否成功
            if (success != JNI_OK) {
                LOGE("SUCCESS %d:", success);
                return -1;
            }
            // 获取要绑定的类 即对应的java 交互类
            jclass jcls = env->FindClass(mClassName);
            // 注册要关联的native方法
            env->RegisterNatives(jcls, jniMethod, sizeof(jniMethod) / sizeof(JNINativeMethod));
            return JNI_VERSION_1_6;
        }
        
  • 步骤总结

      1. 创建函数映射表
      2. 获取函数对应java类
      3. 获取JNIEnv,通过JNIEnv注册 函数映射表
  • 优势

    • 动态注册 命名更灵活

1.6 数据类型转换

1.6.1 基本数据类型

java类型 nativie类型 描述
boolean jboolean unsigned 8 bits 整型
byte jbyte signed 8 bits 整型
char jchar unsigned 16 bits 整型
short jshort signed 16 bits 整型
int jint signed 32 bits 整型
long jlong signed 64 bits 整型
float jfloat signed 32 bits 浮点型
double jdouble signed 64 bits 浮点型
void void 无整形

1.6.2 引用数据类型

java native
object jobject
java.lang.Class instance jclass
java.lang.String instance jstring
array jarray
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
java.lang.Throwable objects jthrowable

1.6.3 函数签名

  • 格式

    • [参数1类型字符参数2类型字符...]返回值类型字符

    • 注意引用类型需以L开头后跟类型全路径并以;结尾

      • Ljava/lang/String;
    • 参数

      • 若为多参数也无需间隔直接后面添加类型字符即可

      • java 方法 方法签名
        String getStr(int a,int b) (II)Ljava/lang/String;
        String getS(int a,String b) (ILjava/lang/String;)Ljava/lang/String;
    • 若没参数则括号内不写内容若无返回值则返回值位置用V

      • ()V
  • 注意引用类型需以L开头后跟类型全路径并以;结尾

    • Ljava/lang/String;
  • 对照表

    • Java类型 对应字符
      void V
      boolean Z
      int I
      long J
      double D
      float F
      byte B
      char C
      short S
      int[] [I (数组以[开始后跟对应类型字符)
      String Ljava/lang/String; (引用类型 以L开头后跟类路径以;结尾)
      Object[] [Ljava/lang/object;

1.7 JNI引用

局部引用(Local Reference)

  • 在函数内部创建,声明的变量和对象都属于局部引用

  • 方法调用结束,局部引用自动释放

  • 当然也可以手动释放

    • DeleteLocalRef()  // 创建的对象用Delete
      ReleaseXXX 
      

全局引用(Global Reference)

  • JNI 允许从局部变量创建全局变量

    • //声明全局引用
      jclass  st1Class;
      extern "C"
      JNIEXPORT jobject JNICALL
      Java_com_example_ndktest_NdkManager_changName(JNIEnv *env, jobject thiz, jobject st, jstring name) {
          //查找类
          if(st1Class==NULL){
              jclass  cls = env->FindClass("com/example/ndktest/Student");
              //声明为全局引用
              st1Class= static_cast<jclass>(env->NewGlobalRef(cls));
              env->DeleteLocalRef(cls);//释放局部引用
              //env->DeleteGlobalRef(st1Class);//释放全局引用
          }
      }
      
  • 可以跨方法跨线程

  • 释放需调用DeleteGlobalRef

弱全局引用(Weak Global Reference)

  • 与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象

  • 所以在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象

  • 创建

    • //声明全局引用
      jclass  st1Class;
      extern "C"
      JNIEXPORT jobject JNICALL
      Java_com_example_ndktest_NdkManager_changName(JNIEnv *env, jobject thiz, jobject st, jstring name) {
       	//判断是否指向活动的对象
          jboolean isEqual =env->IsSameObject(st1Class,NULL);
          if(st1Class==NULL||isEqual){
              jclass  cls = env->FindClass("com/example/ndktest/Student");
              //声明为全局引用
              st1Class= static_cast<jclass>(env->NewWeakGlobalRef(cls));
              env->DeleteLocalRef(cls);//释放局部引用
              //env->DeleteWeakGlobalRef(st1Class);//释放弱全局引用
          }
      }
      
  • 释放

    • 调用DeleteWeakGlobalRef来释放

NDK

简介

Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具,NDK描述的是工具集

是通过jni调用的c/c++的原生代码的

重要结构目录

  • 我们可以在 sdk/ndk-bundle 中查看 ndk 的目录结构,下面列举出三个重要的成员:
    • ndk-build: 该 Shell 脚本是 Android NDK 构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了。
    • platforms: 该目录包含支持不同 Android 目标版本的头文件和库文件, NDK 构建系统会根据具体的配置来引用指定平台下的头文件和库文件。
    • toolchains: 该目录包含目前 NDK 所支持的不同平台下的交叉编译器 - ARM 、X86、MIPS ,目前比较常用的是 ARM。 // todo ndk-depends.cmd

交叉编译

  • 在一个平台上编译出另一个平台上可以执行的二级制文件的过程叫做交叉编译
    • 比如在windows上编译出android可用的库文件

库文件格式

  • 静态库 `.a
    • 编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,linux中后缀名为”.a”
  • 动态库.so
    • 在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。linux 中后缀名为”.so”,gcc在编译时默认使用动态库。

makefile (.mk) 编译

简介

makefile 就是“自动化编译”:一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,如何进行链接等等操作。 Android 使用 Android.mk 文件来配置 makefile

1.1 Android.mk

  • # 源文件在的位置。宏函数 my-dir 返回当前目录(包含 Android.mk 文件本身的目录)的路径。
    LOCAL_PATH := $(call my-dir)
    
    # 引入其他makefile文件。CLEAR_VARS 变量指向特殊 GNU Makefile,可为您清除许多 LOCAL_XXX 变量
    # 不会清理 LOCAL_PATH 变量
    include $(CLEAR_VARS)
    
    # 指定库名称,如果模块名称的开头已是 lib,则构建系统不会附加额外的前缀 lib;而是按原样采用模块名称,并添加 .so 扩展名。
    LOCAL_MODULE := hello
    # 包含要构建到模块中的 C 和/或 C++ 源文件列表 以空格分开
    LOCAL_SRC_FILES := hello.c
    # 构建动态库
    include $(BUILD_SHARED_LIBRARY)
    
    

1.2 对应Gradle的设置

  • app/gradle

    • apply plugin: 'com.android.application'
      
      android {
          compileSdkVersion 29
      
          defaultConfig {
            ...
              // 应该将源文件编译成几个 CPU so
              externalNativeBuild{
                  ndkBuild{
                      abiFilters 'x86','armeabi-v7a'
                  }
              }
              // 需要打包进 apk 几种 so
              ndk {
                  abiFilters 'x86','armeabi-v7a'
              }
          }
          // 配置 native 构建脚本位置
          externalNativeBuild{
              ndkBuild{
                  path "src/main/jni/Android.mk"
              }
          }
          // 指定 ndk 版本
          ndkVersion "20.0.5594570"
      
          ...
      }
      
      dependencies {
          implementation fileTree(dir: "libs", include: ["*.jar"])
          ...
      }
      
  • Google 推荐开发者使用 cmake 来代替 makefile 进行交叉编译了,makefile 在引入第三方预编译好的 so 的时候会在 android 6.0 版本前后有些差异,比如在 6.0 之前需要手动 System.loadLibrary 第三方 so,在之后则不需要。 关于 makefile 还有很多配置参数,这里不在讲解,更多参考官方文档

    • 在 6.0 以下,System.loadLibrary 不会自动加载 so 内部依赖的 so 在 6.0 以下,System.loadLibrary 会自动加载 so 内部依赖的 so 所以使用 mk 的话需要做版本兼容

Cmake 编译

  • 是一个构建工具

1.1 CMakeLists.txt

  • # cmake版本
    cmake_minimum_required(VERSION 3.10.2)
    
    #声明并命名项目
    project("ndktest")
    
    # 声明并命名库
    # 将其设置 是否共享 此处有三个值 
    #	SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;
    # 	STATIC: 表示静态库,集成到代码中会在编译时调用;
    #	MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;
    #	EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建;
    # 添加库中源文件的路径
    add_library( # 设置库的名字 比如现在会生成 ndktest.so
            ndktest
    
            # 设置库为共享库
            SHARED
    
            # 提供源文件相对路径
            native-lib.cpp)
    # 搜索并指定预构建库并将路径存储为变量(这里是log-lib)。
    # NDK中已经有一部分预构建库(比如 log),并且ndk库已经是被配置为cmake搜索路径的一部分
    # 可以不写 直接在 target_link_libraries 写上log
    # 比如上文中我们引入的 android/log.h 就是从调用的ndk 目录下的 liblog.so  前面的lib可省略直接写log 即可找到
    find_library( # 设置路径变量名称
            log-lib
    
            # 从系统路径中查找指定名字的so库路径 赋值给上面的log-lib
            log)
    
    # 指定CMake应链接到目标库的库。可以链接多个库,例如在此生成脚本中定义的库、预构建的第三方库或系统库。
    target_link_libraries( # 指定目标库
            ndktest
    
            # 链接目标库的路径 这里是log的路径 即上面 find_library 中 的log-lib 
            # 这种是以变量形式链接目标库路径 当然我们也可以省区find_library这一步直接指定目标库
            # 就比如这里的ndktest 直接写log 也可以找到
            # 如果需要引入三方so 就得指定目录来查找了
            ${log-lib})
    

1.2 Gradle 中的配置

  • android {
        //......
        defaultConfig {
            applicationId "com.example.ndktest"
            minSdk 21
            targetSdk 31
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
            externalNativeBuild {
                cmake {
                    // 设置c++ 标准
                    cppFlags '-std=c++11'
                    //需要生成几种cpu架构下的so,不写默认都生成
                    abiFilters "armeabi-v7a","x86"
                }
            }
            // 打包支持几种架构的apk包 比如这里就会生成支持x86 和armeabi-v7a两种的架构的apk
            ndk {
                abiFilters 'x86','armeabi-v7a'
            }
        }
        // 分架构打包出不同架构的apk
         splits {
            abi {
                enable true
                reset()
                include 'armeabi-v7a', 'x86'
                universalApk true
            }
        }
        //......
        // 配置native构建脚本的文件路径
        externalNativeBuild {
            cmake {
                path file('src/main/cpp/CMakeLists.txt')
                version '3.10.2'
            }
        }
        //.....
    }
    
  • externalNativeBuilddefaultConfig 中为指导源文件编译 在defaultConfig 外为配置native的构建脚本路径

添加多个源文件

引用三方动态库.so

  • 假如引用一个c 编写的so -> libtest.so 内只含有一个hc 文件

  • 这里将CMakeList.txt从cpp目录移动到app目录下 方便后面拼接路径

  • 将对应架构的so 放到 jinLibs对应架构目录下 这里以 armeabi-v7a 为例

方式一

  • 增加 CMake 查找路径 直接给 cmake 在添加一个查找路径,在这个路径下可以找到 external
  • CMakeList.txt中添加如下代码 代码级别与 target_link_libraries、find_library同级别

    • set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
      
    • CMakeList.txt中设置一个变量

      • CMAKE_CXX_FLAGS 使用c++编译为c++环境下的变量名
      • CMAKE_C_FLAGS 使用c编译 为C环境下的变量名
    • 该变量会传递给编译器

    • 重新定义该变量 通过-L 指定 目标路径 可以通过如下变量 动态识别目录 从而查找指定的so

      • CMAKE_SOURCE_DIR 当前文件路径
      • ANDROID_ABIabi 目录
    • test写到target_link_libraries 表示我们要链接libtest.so

      • so 以lib开头时 写到target_link_librariesfind_library时 lib 可以省略只写名字即可

      • target_link_libraries( # Specifies the target library.        ndktest		test        # Links the target library to the log library        # included in the NDK.        ${log-lib})
        

方式二

  • CMakeList.txt中添加如下代码 代码级别与 target_link_libraries、find_library同级别

  • 且需要在systemload中 load 该so

  • 这种方式在android 6.0 以下还能用 6.0 以上就会出现目录不对的问题 所以cmake中 我们一般采用第一种方式

    • # test 代表第三方 so - libtest.so# SHARED 代表动态库,静态库是 STATIC;# IMPORTED: 表示是以导入的形式添加进来(预编译库)add_library(test SHARED IMPORTED)#设置 test 的 导入路径(IMPORTED_LOCATION) 属性,不可以使用相对路径# CMAKE_SOURCE_DIR: 当前cmakelists.txt的路径 (cmake工具内置的)# android cmake 内置的 ANDROID_ABI :  当前需要编译的cpu架构set_target_properties(external PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libtest.so)
      
    • test写到target_link_libraries 表示我们要链接libtest.so

      • target_link_libraries( # Specifies the target library.        ndktest		test        # Links the target library to the log library        # included in the NDK.        ${log-lib})
        

引入so后使用

libtest.so 内存在 test() 函数

情况一 存在test.h

  • 直接#include<test.h>

    • test函数是用c 编写的

      • 我们的环境又是c++环境 则需要提前标记extern "C" 才能正常调用

        • extern "C" {    extern void test();}
          
      • 不是c++ 环境 则直接调用test函数即可

情况二 不存在 test.h

  • 则要将函数声明为 extern 即 外部定义在这里引用

    • extern void test();
      
    • test函数是用c 编写的

      • 我们的环境又是c++环境 则需要提前标记extern "C" 才能正常调用

        • extern "C" {    extern void test();}
          
      • 不是c++ 环境 则直接调用test函数即可

引入静态库 .a

  • 不用必须放在jnilibs 目录下 只需要像引用so一样设置好库的查找路径即可

  • # test1 代表第三方.a文件 全名是libtest1.a
    # SHARED 代表动态库,静态库是 STATIC;# IMPORTED: 表示是以导入的形式添加进来(预编译库)
    add_library(test1 STATIC IMPORTED)
    #设置 test 的 导入路径(IMPORTED_LOCATION) 属性,不可以使用相对路径
    # CMAKE_SOURCE_DIR: 当前cmakelists.txt的路径 (cmake工具内置的)
    # android cmake 内置的 ANDROID_ABI :  当前需要编译的cpu架构 AS3.2后, ${ANDROID_ABI} 改成 ${CMAKE_ANDROID_ARCH_ABI}
    set_target_properties(external PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libtest1.a)
    
  • 使用方式 和上面so 使用方式相同

上一篇:rails binding的应用


下一篇:Babel基础入门