NDK 编译系列文章共三篇,目录如下:
NDK 编译(一)—— Linux 知识汇总
NDK 编译(二)—— NDK 编译与集成 FFmpeg
NDK 编译(三)—— CMake 原生构建工具
1、什么是 CMake
CMake 简介:
- 在 Android Studio 2.2 及以上,构建原生库的默认工具是 CMake。
- CMake 是一个跨平台的构建工具,可以用简单的语句来描述所有平台的安装(编译过程)。能够输出各种各样的 makefile 或者 project 文件。CMake 并不直接构建出最终的软件,而是产生其他工具的脚本(如 makefile),然后再依据这个工具的构建方式使用。
- CMake 是一个比 make 更高级的编译配置工具,它可以根据不同的平台、不同的编译器,生成相应的 makefile 或 vcproj 项目,从而达到跨平台的目的。
- Android Studio 利用 CMake 生成的是 ninja。ninja 是一个小型的关注速度的构建系统。我们不需要关心 ninja 的脚本,知道怎么配置 CMake 就可以了。
- CMake 其实是一个跨平台的支持产出各种不同的构建脚本的一个工具。
2、makefile 导入静态库与动态库
由于 CMake 实际上是对 makefile 的封装,并且在 CMake 之前是使用 makefile 进行编译的,因此我们来简单看下 makefile。
首先修改 build.gradle 屏蔽掉 CMake 的编译,而是使用指定的 Android.mk 文件编译:
android {
externalNativeBuild {
/*cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}*/
ndkBuild {
path 'src/main/ndkDir/Android.mk'
}
}
}
然后编辑这个 Android.mk:
# 1、通过宏函数 my-dir 获取 Android.mk 文件所在的路径
LOCAL_PATH := $(call my-dir)
$(info "LOCAL_PATH:${LOCAL_PATH}")
# 2、清理
include $(CLEAR_VARS)
# 引入预编译库,模块名为库文件名称去掉 lib 前缀
# 我们举得是动态库编译的例子,如果想编译静态库,将后续
# 静态库相关的注释打开即可
LOCAL_MODULE := test
# 指定动态(静态)库文件
LOCAL_SRC_FILES := libtest.so
#LOCAL_SRC_FILES := libtest.a
# 编译动态(静态)库
include $(PREBUILT_SHARED_LIBRARY)
#include $(PREBUILT_STATIC_LIBRARY)
# CLEAR_VARS 变量指向特殊 GNU makefile,可清除许多 LOCAL_XXX 变
# 量(不会清理 LOCAL_PATH 变量)
include $(CLEAR_VARS)
# 3、指定要构建的库的名字,唯一且不能有空格
# 如果模块名称的开头已是 lib,构建系统不会附加额外的前缀 lib,
# 而是按原样采用模块名称,并添加 .so 扩展名
LOCAL_MODULE := target
# 要编译的源文件,用空格或换行符 \ 分隔
LOCAL_SRC_FILES := Test.c \
Login.c
# 将需要的动态库(静态库)链接到目标的 target 库中
#LOCAL_SHARED_LIBRARIES := test
LOCAL_STATIC_LIBRARIES := test
# 导入 log 库
LOCAL_LDLIBS := -lm -llog
# 4、构建动态(静态)库,编译生成 libtarget.so(或 libtarget.a)
include $(BUILD_SHARED_LIBRARY)
#include $(BUILD_STATIC_LIBRARY)
通过以上代码可以看出,makefile 被弃用的原因:代码不人性化、规则太多。
3、CMakeLists 详解
CMake 是对 makefile 的封装,它可以生成 makefile,并且比 makefile 好用得多。
以下是最基础的 CMakeLists 文件:
# 最低支持的版本。注意不代表最终的版本,最终版本在 build.gradle 中设置
cmake_minimum_required(VERSION 3.22.1)
# 当前工程名,以前的旧版本,是没有设置的,这个可以设置,也可以不设置
project("tool")
# 批量导入源文件
# file 可以定义一个变量 SOURCE, GLOB(使用GLOB从源树中收集源文件列表,就可以开心的 *.cpp *.c *.h)
# https://www.imooc.com/wenda/detail/576408
file(GLOB SOURCE *.cpp *.c)
# 添加一个库(动态库 SHARED,静态库 STATIC)
# 将 SOURCE 表示的源文件进行编译,生成的库
# 的名字是 libtool.so
add_library(
tool
SHARED
${SOURCE})
# 查找 NDK 工具中的动态库 liblog.so 并赋值给变量 log-lib
# 如何知道哪些库可以写呢?通过查看 ndk 中的 system_libs.cmake 文件
find_library(
log-lib
log)
# tool 是我们编译生成的库 libtool.so,会被打包进 APK 的 apk/lib/ 目录下,
# 这里将 log-lib 变量所表示的 log 库链接到 libtool 中,也可以直接写 log
target_link_libraries(
tool
${log-lib})
需要详解的是 find_library 如何确定哪些库可以被寻找呢,这些库文件又在哪些目录下呢?可以分为如下几步:
-
先确定 Android Studio 使用的 NDK 版本,有三种方式可以查看:
-
查看模块的 build.gradle:
android { ndkVersion '26.1.10909125' }
-
查看 local.propetries 文件:
ndk.dir=C\:\\Users\\UserName\\Android\\Sdk\\ndk\\26.1.10909125
-
File -> Project Structure -> SDK Location 查看 Android NDK Location 选项内的路径。这个路径,如果没有通过前两种方式指定 NDK 的话,会显示为空,只有在指定之后才能显示对应的路径
-
-
根据所使用的 NDK 版本所在的目录,找到 system_libs.cmake 文件,完全路径是
C:\...\Sdk\ndk\26.1.10909125\build\cmake\system_libs.cmake
:set(NDK_SYSTEM_LIBS "libEGL.so;libGLESv1_CM.so;libGLESv2.so;libGLESv3.so;libOpenMAXAL.so;libOpenSLES.so;libaaudio.so;libamidi.so;libandroid.so;libbinder_ndk.so;libc.so;libcamera2ndk.so;libdl.so;libicu.so;libjnigraphics.so;liblog.so;libm.so;libmediandk.so;libnativehelper.so;libnativewindow.so;libneuralnetworks.so;libstdc++.so;libsync.so;libvulkan.so;libz.so")
这里定义了哪些库文件可以通过 find_library 查找
-
确定代码是运行在哪种 CPU 架构上的,比如真机一般是 arm-v7a 或 arm64,而模拟器是 x86 或 x86_64。这一点决定了使用的库在 NDK 的哪个目录下
-
在 build.gradle 中确定最小支持的版本:
android { defaultConfig { minSdk 24 } }
-
还是在 NDK 目录下,查找具体的库文件在 toolchains 目录下,一直向下找到
Android\Sdk\ndk\26.1.10909125\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\lib
目录,该目录下有 5 个目录:aarch64-linux-android arm-linux-androideabi i686-linux-android riscv64-linux-android x86_64-linux-android
具体选择哪一个目录需要根据 CPU 架构选择,比如 arm-v7a 就选择 arm-linux-androideabi,进去之后发现又有很多数字文件夹,从 21 ~ 34,这是对应的最小 API 版本,在上一点中我们查到 minSdk 为 24,所以这次就应该进入 24 这个文件夹。进去后会发现里面全是 system_libs.cmake 中定义的库文件,这些文件也就是 find_library 可以查找的文件
4、CMakeLists 使用进阶
在 CMakeLists 内可以定义变量、函数,进行流程控制:
# 1.设置一个变量,CMake 的变量都是字符串类型
set(var 666)
# 变量通过 ${} 引用,message 打印字符串内容
message("var = ${var}")
# 移除变量,后续不能再使用
unset(var)
# 2.声明列表变量,有两种方式
set(list1 1 2 3)
set(list2 "1;2;3;4;5")
message("list1 = ${list1}")
message("list2 = ${list2}")
# 3.条件命令,用来进行流程控制
# 表示 true 的值:1、ON、YES、TRUE、Y、非零值
# 表示 false 的值:0、OFF、NO、FALSE、N、IGNORE、NOTFOUND
# endif() 内可以不加参数,下例会输出 else
set(if_tap OFF)
set(elseif_tap ON)
if (${if_tap})
message("if")
elseif (${elseif_tap})
message("elseif")
else (${if_tap})
message("else")
endif (${${if_tap}})
# 4、while 循环,break() 与 continue() 行为与 Java 相同
set(a "")
while (NOT a STREQUAL xxx)
set(a ${a}x)
message(a = ${a})
endwhile ()
# 5、for 循环
foreach (item 1 2 3)
message(item = ${item})
endforeach (item)
# RANGE 默认从 0 开始,所以会打印 0、1、2
foreach (item RANGE 2)
message(item = ${item})
endforeach (item)
# 从 1~6 步长为 2,输出 1、3、5
foreach (item RANGE 1 6 2)
message(item = ${item})
endforeach (item)
# 遍历列表
set(list 1 2 3)
foreach (item IN LISTS list)
message(item = ${item})
endforeach (item)
# 5、函数
function(test_method n1 n2 n3)
message("call num_method method")
# n1 = 1
message("n1 = ${n1}")
# n2 = 2
message("n2 = ${n2}")
# n3 = 3
message("n3 = ${n3}")
# ARGC = 3
message("ARGC = ${ARGC}")
# arg1 = 1 arg2 = 2 arg3 = 3
message("arg1 = ${ARGV0} arg2 = ${ARGV1} arg3 = ${ARGV2}")
# all args = 1;2;3
message("all args = ${ARGV}")
endfunction(test_method)
# 调用函数
test_method(1 2 3)
输出可以在 AS 的 Build 信息栏或者 build/intermediates/cxx/Debug/y4b2j2i3/logs/x86/configure_stderr.txt 文件中查看。
5、动态库与静态库分析
target_link_libraries 在编译期会将需要用到的动态库或静态库与我们最终编译出来的目标库进行链接,但是两种库在链接时的表现有所不同:
- 静态库:在编译期将静态库的代码拷贝到目标库中,最终打包进 APK 的只有目标库文件,没有静态库文件,但是在运行期间,静态库代码可以完全独立运行
- 动态库:在编译期将动态库内函数的地址拷贝到目标库中,在运行期,通过该地址去寻找动态库内的函数(地址回填),这要求打包进 APK 的除了目标库文件之外,动态库文件也必须被打包,否则就无法通过地址找到动态库内对应的函数
示意图如下:
6、CMake 预编译库与依赖源码方式
有两种方式:
-
推荐的方式:
include_directories(inc) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}") target_link_libraries( tool log fmod fmodL)
-
以前常用的方式,可读性强:
include_directories(inc) add_library(fmod SHARED IMPORTED) set_target_properties(fmod PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libfmod.so) add_library(fmodL SHARED IMPORTED) set_target_properties(fmodL PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libfmodL.so) target_link_libraries( tool log fmod fmodL)
此外如果一个库既可能编译成静态库,又可能编译成动态库的情况下,可以定义变量通过 if 控制如何编译:
set(isSTATIC ON)
if (${isSTATIC})
# 导入静态库
add_library(getndk STATIC IMPORTED)
set_target_properties(getndk PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/cpp/libgetndk.a)
else (${isSTATIC})
# 导入动态库
add_library(getndk SHARED IMPORTED)
# 开始真正导入 动态库 System.loadLibrary("getndk");
set_target_properties(getndk PROPERTIES
IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/jniLibsaaa/${CMAKE_ANDROID_ARCH_ABI}/libgetndk.so)
endif (${isSTATIC})
target_link_libraries( # native-lib是我们的总库
native-lib # 被链接的总库
getndk # 链接到 libnative-lib.so 里面去【这个库,有可能是静态库,有可能是动态库】
)