Android jni 使用说明(cmake)

项目中使用 jni

1. 添加 jni 文件

首先在 main 文件夹下新增一个 cpp 文件夹,然后在文件夹中创建一个 CMakeList.txt 文件,用于 ndk 相关的配置,基本如下 首先使用 add_library 添加一个库,指定了库的名字(native-lib)、类型(动态库)、源文件(native-lib.cpp),如果 jni 中不需要使用其他库,只需要写这么多就够了。不过在使用 android studio 创建一个默认 jni 的时候,会同时带上 log 库,就是下面两个命令,首先使用 find_library 将 log 库的路径赋值给 log-lib,然后使用 target_link_libraries 将 lib-log 链接到 native-lib 库上,这样在 jni 中就可以使用 log 库中的函数。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

在调用 add_library 时有指定 native-lib 这个库的源文件,也就是 native-lib.cpp 这个文件,待添加到 c++ 函数将被写在这个文件里,如果需要添加多个文件,直接加在这里就行。

2. java 中声明 native 接口

然后就是需要在 java 中加载 jni 库,然后是声明对应的 naitve 方法,比如下面这个:

public class EffectSdk {
    static {
        System.loadLibrary("native-lib");
    }

    public native String getSdkVersion();
}

首先使用 System.loadLibrary 加载库,这个方法会自动给 native-lib 加上前后缀使其成为 libnative-lib.so,然后在 apk 文件中查找这么一个文件加载进来,然后使用 native 关键字声明一个 native 函数,这个函数在被调用时会转而调用对应的 C++ 函数,java 方法与 C++ 函数映射有两种方式,一种是使用默认的方法,系统会根据被调用的 java 方法名,找到一个对应的 C++ 函数名,比如上面这个函数的名字是 com.wuruoye.effect_sdk.EffectSdk.getSdkVersion,那么它就会直接找名为 Java_com_wuruoye_effct_1sdk_EffectSdk_getSdkVersion 的函数,如果找不到就会报函数没有实现,当然也可以自己执行函数名的对应关系,使用 JNIEnv.RegisterNatives 函数,指定 java 中的函数名与 C++ 的函数名,如下:

static const char *CLASS = "com/wuruoye/effct_sdk/EffectSdk";
static JNINativeMethod gMethods[] = {
        {"getSdkVersion2",       "()Ljava/lang/String;",           (void *)nativeGetSdkVersion}
};
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reversed) {
    jint result = -1;
    JNIEnv *env = NULL;

    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }

    jclass clazz = env->FindClass(CLASS);
    if (env->RegisterNatives(clazz, gMethods, ((int) sizeof(gMethods) / sizeof(gMethods[0]))) < 0) {
        return result;
    }

    result = JNI_VERSION_1_4;

    return result;
}

通过在 JNI_OnLoad 中调用 RegisterNatives 注册 native 方法,其中 gMethods 参数为 JNINativeMethod 数组,每一项依次为 函数名、函数签名(参数,返回值)和对应的 C++ 函数,如是便可以不用遵循上面的那一套函数名对应规则。

在 gradle 中进行配置

以上两步都算是代码,但是由于 android 是基于 gradle 构建的,所以还是要在 gradle 中进行配置,才能上 cmake 能够执行,以及可以给 cmake 传递一些编译参数等,

android {
	defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags ""
                arguments "-DANDROID_STL=c++_shared"
                abiFilters 'armeabi-v7a','arm64-v8a'
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

首先,在 android 模块中通过加入 defaultConfig.externalNativeBuild.cmake 指定了 cmake 编译的参数,其对应的类是 CoreExternalNativeCmakeOptions,它有四个参数:arguments 用于指定编译的一些参数,如arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang"等,这里指定了 ANDROID_STL 为 c++_shared,用作支持 c++ 语法(可能是);cFlags 用于指定 C 语言编译的信息,如cFlags "-D__STDC_FORMAT_MACROS";cppFlags 用于指定 c++ 编译参数,如 cppFlags "-fexceptions", "-frtti";abiFilters 可以用于指定编译结果需要支持的平台,如上面abiFilters 'armeabi-v7a','arm64-v8a'表示只需要支持 armeabi-v7a 和 arm64-v8a 两个平台即可,然后就可以看到在 build/intermediates/cmake/debug/obj 文件夹中就只有指定的这两个输出文件夹;target 用于指定编译的目标文件,也就是在 CMakeList.txt 文件中指定的 library native-lib,默认会编译所有的 target,如果这里有指定就只会编译指定的那些。

然后还要在 android.externalNativeBuild 中指定 cmake 文件的路径,或者还有版本等等,这里的 cmake 与上面的不同,它对应的类是 CmakeOptions,它有三个参数:path 用于指定 cmake 文件的路径,也就是 CmakeList.txt 文件的路径(支持相对路径);buildStagingDirectory 用于指定 cmake 编译产物的路径;version 指定 cmake 的版本(不是必须的)。

以上配置指定完了之后,重新 build 项目,就可以看到编译产物。

jni 中引用三方库

如果 jni 中还需要引用第三方库,就需要再做额外的处理,主要分为两步,

  1. 在 cmake 中导入库
  2. 在 gradle 中引入库

cmake 引用库

引用库也分为两部分,首先声明一个库,执行它的一些属性,然后将其与目标库链接到一起。默认的 cmake 文件也是有导入一个库的,不过那是内置的库,不需要指定库的路径等属性。

add_library(
        effect
        SHARED
        IMPORTED
)

set_target_properties(
        effect
        PROPERTIES
        IMPORTED_LOCATION /Users/wuruoye/Downloads/jni_test/effct_sdk/libs/${ANDROID_ABI}/libeffect.so
)

target_link_libraries( # Specifies the target library.
                       native-lib effect

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

与声明 native-lib 库类似,首先使用 add_library 添加一个库,指定其名称,类型(引入的动态库),然后再使用 set_target_properites 设置 effect 的属性,这里设置的是 IMPORTED_LOCATION,即 effect 库的路径,注意,这里不能使用相对路径。声明完了之后,还是在 target_link_libraries 中将 effect 库添加到 native-lib 库的链接中进行编译即可。

gradle 引用库

之后在项目的 gradle 文件中还要引用 effect 库,因为在 cmake 中导入的 effect 库实际上只是作为动态链接的编译过程中使用,从指定的 effect 库路径也可以看出,这里指定的是项目本地的文件路径,这个路径显然也不是给 apk 文件使用的,而如果要在 apk 中使用这个库,还是需要用 android 的方法,将库加载到 apk 文件中,也就是通过 gradle 文件配置。

android {
    sourceSets {
        main.jniLibs.srcDirs = ['libs']
    }
}

添加一项 android.sourceSets.main.jniLibs.srcDirs 指定需要导入的库文件的路径(支持相对路径),同时还要注意,库文件并不是直接放在 libs 文件夹下,而是还需要使用平台名包裹,实际上它的目录结构应该如下图,

image-20200405161206124

gradle 会给对应的平台的 apk 装载不同的库,不过默认情况下 gradle 是会把所有平台打包在一起编译的,分平台编译还需要再进行配置。

最终,编出来的 apk 结构就是这样的:

image-20200405161517114

如果想要将不同平台的 so 分别打包成不同的 apk,在 gradle 中进行如下配置:

android {
    splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a', 'arm64-v8a'
            universalApk true
        }
    }
}

通过 splits.abi 指定需要进行分平台打包,enable 表示 splits 是否可用,reset 对需要打包的平台进行清空,然后 include 指定需要打包的平台,最后的 universalApk 表示是否需要打包一个完整的 apk。配置了以上信息之后,打出来的包就是这样的:

image-20200405163104092

点进去之后也确实可以看到 apk 中只含有了一个平台的库:

image-20200405163151403

以上就是使用 cmake 进行 ndk 开发的基本知识。