本篇文章旨在简介 Android 中
NDK
是什么以及重点解说最新 Android Studio 编译工具CMake
的应用
1 NDK 简介
在介绍 NDK
之前还是首推 Android 官网 NDK
文档。
官网文档别离从以下几个方面介绍了 NDK
NDK
的根底概念- 如何编译
NDK
我的项目 ABI
是什么以及不同 CPU 指令集反对哪些ABI
- 如何应用您本人及其他预建的库
本节将会对文档进行总结和补充。所以倡议先浏览一遍文档,或者看完本篇文章再回头看一遍文档。
1.1 NDK 根底概念
首先先用简略的话别离解释下 JNI
、NDK
,以及别离和 Android 开发、c/c++ 开发的配合。在解释过程中会对 Android.mk
、Application.mk
、ndk-build
、CMake
、CMakeList
这些常见名词进行扫盲。
JNI(Java Native Interface):Java 本地接口。是为了不便 Java 调用 c、c++ 等本地代码所封装的一层接口(也是一个规范)。大家都晓得,Java 的长处是跨平台,然而作为长处的同时,其在本地交互的时候就编程了毛病。Java 的跨平台个性导致其本地交互的能力不够弱小,一些和操作系统相干的个性 Java 无奈实现,于是 Java 提供了 jni 专门用于和本地代码交互,这样就加强了 Java 语言的本地交互能力。
NDK(Native Development Kit): 原生开发工具包,即帮忙开发原生代码的一系列工具,包含但不限于编译工具、一些公共库、开发 IDE 等。
NDK
工具包中提供了残缺的一套将 c/c++ 代码编译成动态 / 动静库的工具,而 Android.mk
和 Application.mk
你能够认为是形容编译参数和一些配置的文件。比方指定应用 c ++11 还是 c ++14 编译,会援用哪些共享库,并形容关系等,还会指定编译的 abi
。只有有了这些 NDK
中的编译工具能力精确的编译 c/c++ 代码。
ndk-build
文件是 Android NDK r4 中引入的一个 shell 脚本。其用处是调用正确的 NDK
构建脚本。其实最终还是会去调用 NDK
本人的编译工具。
那 CMake
又是什么呢。脱离 Android 开发来看,c/c++ 的编译文件在不同平台是不一样的。Unix 下会应用 makefile
文件编译,Windows 下会应用 project
文件编译。而 CMake
则是一个跨平台的编译工具,它并不会间接编译出对象,而是依据自定义的语言规定(CMakeLists.txt
)生成 对应 makefile
或 project
文件,而后再调用底层的编译。
在 Android Studio 2.2 之后,工具中减少了 CMake
的反对,你能够这么认为,在 Android Studio 2.2 之后你有 2 种抉择来编译你写的 c/c++ 代码。一个是 ndk-build
+ Android.mk
+ Application.mk
组合,另一个是 CMake
+ CMakeLists.txt
组合。这 2 个组合与 Android 代码和 c /c++ 代码无关,只是不同的构建脚本和构建命令。本篇文章次要会形容后者的组合。(也是 Android 当初主推的)
1.2 ABI 是什么
ABI
(Application binary interface)应用程序二进制接口。不同的 CPU 与指令集的每种组合都有定义的 ABI
(应用程序二进制接口),一段程序只有遵循这个接口标准能力在该 CPU 上运行,所以同样的程序代码为了兼容多个不同的 CPU,须要为不同的 ABI
构建不同的库文件。当然对于 CPU 来说,不同的架构并不意味着肯定互不兼容。
- armeabi 设施只兼容 armeabi;
- armeabi-v7a 设施兼容 armeabi-v7a、armeabi;
- arm64-v8a 设施兼容 arm64-v8a、armeabi-v7a、armeabi;
- X86 设施兼容 X86、armeabi;
- X86\_64 设施兼容 X86\_64、X86、armeabi;
- mips64 设施兼容 mips64、mips;
- mips 只兼容 mips;
具体的兼容问题能够参见这篇文章。Android SO 文件的兼容和适配
当咱们开发 Android 利用的时候,因为 Java 代码运行在虚拟机上,所以咱们素来没有关怀过这方面的问题。然而当咱们开发或者应用原生代码时就须要理解不同 ABI
以及为本人的程序抉择接入不同 ABI
的库。(库越多,包越大,所以要有抉择)
上面咱们来看下一共有哪些 ABI
以及对应的指令集
ABI
2 CMake 的应用
这一节将重点介绍 CMake
的规定和应用,以及如何应用 CMake
编译本人及其他预建的库。
2.1 Hello world
咱们通过一个 Hello World 我的项目来了解 CMake
首先创立一个新的蕴含原生代码的我的项目。在 New Project 时,勾选 Include C++ support
我的项目创立好当前咱们能够看到和一般 Android 我的项目有以下 4 个不同。
main
上面减少了cpp
目录,即搁置 c/c++ 代码的中央- module-level 的
build.gradle
有批改 - 减少了
CMakeLists.txt
文件 - 多了一个
.externalNativeBuild
目录
build.gradle
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"
arguments "-DANDROID_ARM_NEON=TRUE"
}
}
}
buildTypes {...}
externalNativeBuild {
cmake {path "CMakeLists.txt"}
}
}
...
因为 CMake
的命令集成在了 gradle
– externalNativeBuild
中,所以在 gradle
中有 2 个中央配置 CMake
。
defaultConfig
里面的 externalNativeBuild - cmake
,指明了 CMakeList.txt
的门路;defaultConfig
外面的 externalNativeBuild - cmake
,次要填写 CMake
的命令参数。即由 arguments
中的参数最初转化成一个可执行的 CMake
的命令,能够在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_command.txt
中查到。如下
更多的能够填写的命令参数和含意能够参见 Android NDK-CMake 文档
CMakeLists.txt
CMakeLists.txt
中次要定义了哪些文件须要编译,以及和其余库的关系等。
看下新我的项目中的 CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 编译出一个动静库 native-lib,源文件只有 src/main/cpp/native-lib.cpp
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).
src/main/cpp/native-lib.cpp )
# 找到预编译库 log_lib 并 link 到咱们的动静库 native-lib 中
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 )
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
这其实是一个最根本的 CMakeLists.txt
,其实 CMakeLists.txt
外面能够十分弱小,比方自定义命令、查找文件、头文件蕴含、设置变量等等。倡议联合 CMake
的官网文档应用。同时在这举荐一个中文翻译的繁难的 CMake 手册
2.2 CMake 应用本人及其他预建的库
当你须要引入已有的动态库 / 动静库(FFMpeg)或者本人编译外围局部并提供进来时就须要思考如何在 CMake
中应用本人及其他预建的库。
Android NDK 官网的应用现有库的文档中还是应用 ndk-build
+ Android.mk
+ Application.mk
组合的阐明文档。(其实官网文档中大部分都是的,并没有应用 CMake
)
侥幸的是,Github 上的官网示例 外面有个我的项目 hello-libs 实现了如何创立出动态库 / 动静库,并援用它。当初咱们把代码拉下来看下具体是如何实现的。
咱们先看下 Github 上的 README 介绍:
- app – 从
$project/distribution/
中应用一个动态库和一个动静库 - gen-libs – 生成一个动静库和一个动态库并复制到
$project/distribution/
目录,你不须要再编译这个库,二进制文件曾经保留在了我的项目中。当然,如果有须要你也能够编译本人的源码,只须要去掉setting.gradle
和app/build.gradle
中的正文,而后执行一次,接着正文回去,避免在 build 的过程中不受影响。
咱们采纳自底向上的形式剖析模块,先看下 gen-libs
模块。
gen-libs/build.gradle
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
arguments '-DANDROID_PLATFORM=android-9',
'-DANDROID_TOOLCHAIN=clang'
// explicitly build libs
targets 'gmath', 'gperf'
}
}
}
...
}
...
查问文档能够晓得 arguments
中 -DANDROID_PLATFORM
代表编译的 android 平台,文档倡议间接设置 minSdkVersion
就行了,所以这个参数可疏忽。另一个参数 -DANDROID_TOOLCHAIN=clang
,CMake
一共有 2 种编译工具链 – clang
和 gcc
,gcc
曾经废除,clang
是默认的。
targets 'gmath', 'gperf'
代表编译哪些项目。(不填就是都编译)
cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
set(lib_src_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(lib_build_DIR $ENV{HOME}/tmp)
file(MAKE_DIRECTORY ${lib_build_DIR})
add_subdirectory(${lib_src_DIR}/gmath ${lib_build_DIR}/gmath)
add_subdirectory(${lib_src_DIR}/gperf ${lib_build_DIR}/gperf)
外层的 CMakeLists
外面外围就是 add_subdirectory
,查问 CMake 官网文档 能够晓得这条命令的作用是为构建增加一个子门路。子门路中的 CMakeLists.txt
也会被执行。即会去别离执行 gmath
和 gperf
中的 CMakeLists.txt
cpp/gmath/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
add_library(gmath STATIC src/gmath.c)
# copy out the lib binary... need to leave the static lib around to pass gradle check
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../distribution)
set_target_properties(gmath
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY
"${distribution_DIR}/gmath/lib/${ANDROID_ABI}")
# copy out lib header file...
add_custom_command(TARGET gmath POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E
copy "${CMAKE_CURRENT_SOURCE_DIR}/src/gmath.h"
"${distribution_DIR}/gmath/include/gmath.h"
# **** the following 2 lines are for potential future debug purpose ****
# COMMAND "${CMAKE_COMMAND}" -E
# remove_directory "${CMAKE_CURRENT_BINARY_DIR}"
COMMENT "Copying gmath to output directory")
这个是其中一个动态库的 CMakeLists.txt
,另一个跟他很像。只是把 STATIC
改成了 SHARED
(动静库)。
add_library(gmath STATIC src/gmath.c)
之前用到过,编译出一个动态库,源文件是 src/gmath.c
set_target_properties
命令的意思是设置指标的一些属性来扭转它们构建的形式。这个命令中设置了 gmath
的 ARCHIVE_OUTPUT_DIRECTORY
属性。也就是扭转了输入门路。
add_custom_command
命令是自定义命令。命令中把头文件也复制到了 distribution_DIR
中。
以上就是一个动态库 / 动静库的编译过程。总结以下 3 点
- 编译动态库 / 动静库
- 批改输入门路
- 复制裸露的头文件
接着,咱们看下 app
模块是如何应用预建好的动态库 / 动静库的。
app/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# configure import libs
set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../distribution)
# 创立一个动态库 lib_gmath 间接援用 libgmath.a
add_library(lib_gmath STATIC IMPORTED)
set_target_properties(lib_gmath PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/gmath/lib/${ANDROID_ABI}/libgmath.a)
# 创立一个动静库 lib_gperf 间接援用 libgperf.so
add_library(lib_gperf SHARED IMPORTED)
set_target_properties(lib_gperf PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/gperf/lib/${ANDROID_ABI}/libgperf.so)
# build application's shared lib
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
# 创立库 hello-libs
add_library(hello-libs SHARED
hello-libs.cpp)
# 退出头文件
target_include_directories(hello-libs PRIVATE
${distribution_DIR}/gmath/include
${distribution_DIR}/gperf/include)
# hello-libs 库链接上 lib_gmath 和 lib_gperf
target_link_libraries(hello-libs
android
lib_gmath
lib_gperf
log)
我将解释放在了正文中。能够看下基本上分成了 4 个步骤引入:
- 别离创立动态库 / 动静库,间接援用曾经有的 .a 文件 或者 .so 文件
- 创立本人利用的库
hello-libs
- 退出之前裸露头文件
- 链接上动态库 / 动静库
还是很好了解的。编辑好并 Sync
后,你就能够发现 hello-libs
中的 c /c++ 代码能够援用裸露的头文件调用外部办法了。
3 材料文献
首推 Android NDK 官网文档,尽管很多都不残缺,然而相对是必须看一遍的货色。
当初次接触 NDK
开发又感觉新建的 Hello World 我的项目过于简略时。倡议把 googlesamples – android-ndk 我的项目拉下来。外面有多个实例参考,比官网文档残缺很多。
当你发现示例里的一些 NDK 配置满足不了你的需要后,你就须要到 CMake 官网文档 去查问残缺的反对的函数,同时这里也提供一个中文翻译的繁难的 CMake 手册。
以上文档资料仅为了解决 NDK 开发过程中编译配置问题,具体 c/c++ 的逻辑编写、jni 等不在此领域。
彩蛋
文末献上一组彩蛋,将 CMake
或者 NDK
开发过程中遇到的坑和小技巧以 Q&A 的形式列出。继续更新
Q1:怎么指定 C++ 规范?
A:在 build_gradle
中,配置 cppFlags -std
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions -std=c++14"
arguments '-DANDROID_STL=c++_shared'
}
}
Q2:add\_library 如何编译一个目录中所有源文件?
A:应用 aux_source_directory
办法将门路列表全副放到一个变量中。
# 查找所有源码 并拼接到门路列表
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/api SRC_LIST)
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/core CORE_SRC_LIST)
list(APPEND SRC_LIST ${CORE_SRC_LIST})
add_library(native-lib SHARED ${SRC_LIST})
Q3:怎么调试 CMakeLists.txt 中的代码?
A:应用 message
办法
cmake_minimum_required(VERSION 3.4.1)
message(STATUS "execute CMakeLists")
...
而后运行后在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt
中查看 log。
Q4:什么时候 CMakeLists.txt 外面会执行?
A:测试了下,如同在 sync 的时候会执行。执行一次后会生成 makefile
的文件缓存之类的货色放在 externalNativeBuild
中。所以如果 CMakeLists.txt
中没有批改的话再次同步如同是不会从新执行的。(或者删除 .externalNativeBuild
目录)
真正编译的时候如同只是读取 .externalNativeBuild
目录中曾经解析好的 makefile
去编译。不会再去执行 CMakeLists.txt
本文转自 https://juejin.cn/post/6844903486455300104,如有侵权,请分割删除。
相干视频举荐:
【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili
Android 音视频学习——穿插编译与 CameraX 预览_哔哩哔哩_bilibili