前言
梳理一下在 Android 项目中包含 C/C++ 代码时,如何灵活的生成 so 文件。以及如何使用第三方提供 so 实现 C/C++ 代码的调用。
CMakeLists.txt 配置
为了方便,以下著名的 bspatch 库为例,进行说明,bsdiff 和 bspatch 的使用细节不再赘述。
在 cpp_native.cpp 中定义 JNI 方法,通过调用 bspatch.h 中定义的 main
函数实现最终的功能。
c++
#include <jni.h>
#include <string>
#include "androidlog.h"
#include "bspatch.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_example_cpp_1native_internal_PatchUtil_patchAPK(JNIEnv *env, jclass clazz,
jstring old_apk_file,
jstring new_apk_file, jstring patch_file) {
int argc = 4;
char *argv[argc];
argv[0] = (char *) ("bspatch");
argv[1] = (char *) (env->GetStringUTFChars(old_apk_file, nullptr));
argv[2] = (char *) (env->GetStringUTFChars(new_apk_file, nullptr));
argv[3] = (char *) (env->GetStringUTFChars(patch_file, nullptr));
LOGD("argv[1] = %s", argv[1]);
LOGD("argv[2] = %s", argv[2]);
LOGD("argv[3] = %s", argv[3]);
main(argc, argv);
env->ReleaseStringUTFChars(old_apk_file, argv[1]);
env->ReleaseStringUTFChars(new_apk_file, argv[2]);
env->ReleaseStringUTFChars(patch_file, argv[3]);
}
下面看一下如何在 CMakeLists.txt 中配置这些依赖项。
当项目中包含 C/C++ 内容时,可以通过配置 CMakeLists.txt 文件实现打包生成 so 的功能。涉及的配置主要是在 add_library 和 target_link_libraries 这两个函数中。
add_library
在 add_library 方法中配置即可。
- so 文件名称
- 最终会生成 build/intermediates/merged_native_libs/release/out/lib/${abi}/lib-xxx.so 。基于配置的 abi 会有对应类型的 so 生成。
- SHARED 共享库,这个一般是固定的
- 当前 so 所依赖的文件
cmake
add_library( # Sets the name of the library.
cpp_native
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
bspatch.c
cpp_native.cpp)
这里依赖的文件包括以下几种情况
cpp_native.cpp 已经涵盖了所有需要依赖的内容
这里又会细分为两种情况
-
一种是整体功能比较简单,cpp_native.cpp 单个文件已经包含了所有的功能,需要这样一个 cpp 文件就可以生成最终的编译结果了。
-
一种是 cpp_native.cpp 通过 #include 的方式已近导入了所有需要依赖的其他 c/cpp 文件。由于在 c/c++ 语言中,#include 的实现效果相当于是把原文件和当前文件合并了,这样 cpp_native.cpp 变相的包含了所有需要的功能代码。
我们这里就属于第二种情况,bspatch.c 导入了所有的头文件和 .c 文件,而 cpp_native.cpp 又导入了 bspatch.h 头文件,这样 bspatch.c
和 cpp_native.cpp
就包含了所有依赖的内容,add_library 中配置这两项就可以了。
cpp_native.cpp 只涵盖了所有需要依赖的部分内容
对于上面的第二种场景,一般情况下,通过 #include 导入的应该只是 .h 头文件,其中包含了一些函数、变量或常量的声明以及宏定义等。这个时候,cpp_native.cpp 其实并没有包含编译时所有需要依赖的文件,因此需要在 add_library 的第三个部分列出所有依赖的文件。这里列出依赖时可以基于当前 CMakeLists.txt 文件所处的路劲配置相应的文件。
也可以提前定义变量,配置一些依赖项,举个栗子
cmake
file(GLOB BZIP bzip/*.c)
#导入头文件
include_directories(bzip)
include_directories(logcat)
add_library( # Sets the name of the library.
cpp_native
# Sets the library as a shared library.
SHARED
#将bzip下的.c文件添加到library
${BZIP}
bspatch.c
# Provides a relative path to your source file(s).
cpp_native.cpp)
CMAKE_CURRENT_SOURCE_DIR 是当前 CMakeLists.txt 文件所处的目录。
这里通过 file 函数,导入了 bzip 目录下的所有文件,并赋值给 BZIP。在 add_library 函数中,可以直接使用 BZIP 这个变量引入 bzip 目录下的所有文件。总的来说,add_library 这里的实现是比较灵活的。 这样,在编译时就会生成名为 libcpp_native.so 的文件,同时会根据当前模块配置的 abiFilters 参数生成符合多个 CPU 架构的文件。
include_directories
这里再说一下 include_directories
,通过这个配置导入之后,在实际使用时,就可以直接 include 文件名,而不用按照相对路径的名称导入文件了。
生成多个 so
可以通过调用多个 add_library 方法生成多个 so,在 add_library 中定义相应的名称和需要的依赖文件即可。比如对于上面的内容,还可以做如下定义
cmake
# 将 bspatch 的源码编译为共享库
add_library(
bspatch_tool
SHARED
bspatch.c)
这样就可以将代码中 bspatch.c 的具体实现编译为 libbspatch_tool.so 的库 ,对于这部分内容的使用可以更加灵活。
可以看到已经成功生成了 libbspatch_tool.so 文件。这样 bspatch.c 的具体实现就打包到这个 so 文件里了。
target_link_libraries
target_link_libraries 这个函数其实就是说当前要生成的 so 还依赖了哪些其他的原生库。这个原生库可以是 Android 系统自带的,也就是 NDK 直接提供的,还可以是自行编译的其他三方库。
cmake
target_link_libraries( # Specifies the target library.
cpp_native
# Links the target library to the log library
# included in the NDK.
${log-lib})
比如这里就是依赖了 Android 系统 log-lib 库。这里需要明确的是,如果 cpp_native 这个库所需要的 cpp 中如果并没有使用到 log-lib 这个原生库的话,这个 target_link_libraries 其实是可以省略的,也就是说这里按需添加即可。结合上面生成多个 so 的实现,也可以对于不同的 library 通过调用 target_link_libraries 添加必须的依赖。 这里有点类似于 Android 中的依赖,就是要编译 apk 时会依赖一些三方库,需要显示的声明。这里也是同样的道理,编译 so 库时也需要显示声明需要 link 的其他库。
调用三方的 so
这里提到的调用三方提供的 so 是指在 C/C++ 代码中依赖这些已近封装好底层库,而不是在 Java 中调用。 一些通用功能可以通过编译为 so 和 头文件(.h文件)的形式对外提供。做到了隐藏细节,同时也省去了编译配置相关的问题。比如上面编译好的 libbspatch_tools.so, 对外只需要提供 bspatch.h 和 so 文件即可,不需要再提供源码了,使用方也不必和无需知道具体的实现细节。
对于这类预先编译好的 so 库,可以按如下步骤在 CMakeLists 中进行配置。我们以上面生成的 libbspatch_tool.so 为例。
cmake
## 定义 distributionDir 变量,声明所依赖的 so 库存放的位置,这个不是必须的,只是为了灵活方便
set(distributionDir ${CMAKE_CURRENT_SOURCE_DIR}/third-lib)
## 所依赖的三方库定义名称,STATIC IMPORTED 表示是导入的
add_library(lib_bspatch STATIC IMPORTED)
## 定义导入的 so 库的路径
set_target_properties(lib_bspatch PROPERTIES IMPORTED_LOCATION ${distributionDir}/${ANDROID_ABI}/libbspatch_tool.so)
最后在 target_link_libraries 中添加这个配置,表示最终生成的库需要依赖这一项,从构建流程来说是需要 link 这个动态库。
cmake
target_link_libraries( # Specifies the target library.
cpp_native
lib_bspatch
# Links the target library to the log library
# included in the NDK.
${log-lib})
这里即表示当前 cpp_native 这个库编译时需要链接到 lib_bspatch 这个库。 添加这个配置之后,就可以在实际代码中结合 libbspatch_tool.so 对应的 头文件进行代码编写,调用相应的函数。因此,就可以移除对于 bspatch.c 源码的依赖了。
cmake
add_library( # Sets the name of the library.
cpp_native
# Sets the library as a shared library.
SHARED
#bspatch.c
# Provides a relative path to your source file(s).
cpp_native.cpp)
最终编译阶段,除了 cpp_native.so 之外,还会包含这个 libbspatch_tool.so 的内容。我们可以比较一下这两种实现方式的差异。
差异 | 依赖c/cpp 实现 | 依赖 so |
---|---|---|
apk | ||
优点 | 依赖源码,易排查问题,对实现进行修改 | 封装具体实现,使用时配置简单 |
缺点 | 源码较多,依赖和编译关系复杂时,配置复杂,接入成本高 | 排查问题较难 |
通过 apk 内的 so 可以看到,使用源码编译时,只有 cpp_native.so,而直接依赖 so 时,cpp_native.so 减小的体积恰好就是多出来的 libbspatch_tool.so 的体积。 当然两种方式各有优劣,需要基于实际情况做出合适的选择。
CMakeLists.txt 相关内容
CMakeLists.txt 文件本质上是定义构建脚本,在其内部就是调用一些函数做一些事情。因此,也可以调用相应的方法输出一些日志信息方便我们在遇到问题时可以有更多的信息,去解决问题和排查问题。
- CMAKE_VERBOSE_MAKEFILE
cmake
// 打开 make 时的信息输出
set(CMAKE_VERBOSE_MAKEFILE on)
- message 方法
cmake
file(GLOB BZIP bzip/*.c)
message("----------start------")
message("${BZIP}")
message("${CMAKE_CURRENT_SOURCE_DIR}")
message("----------end------")
可以使用 messge 方法打印一些变量,包括自定义的变量,系统自带的变量。