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 中还需要引用第三方库,就需要再做额外的处理,主要分为两步,
- 在 cmake 中导入库
- 在 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 文件夹下,而是还需要使用平台名包裹,实际上它的目录结构应该如下图,

gradle 会给对应的平台的 apk 装载不同的库,不过默认情况下 gradle 是会把所有平台打包在一起编译的,分平台编译还需要再进行配置。
最终,编出来的 apk 结构就是这样的:
如果想要将不同平台的 so 分别打包成不同的 apk,在 gradle 中进行如下配置:
android {
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
}
}
通过 splits.abi 指定需要进行分平台打包,enable 表示 splits 是否可用,reset 对需要打包的平台进行清空,然后 include 指定需要打包的平台,最后的 universalApk 表示是否需要打包一个完整的 apk。配置了以上信息之后,打出来的包就是这样的:

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

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