Android NDK——必知必会之Android Studio使用CMake构建NDK项目的背后的故事

Posted CrazyMo_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android NDK——必知必会之Android Studio使用CMake构建NDK项目的背后的故事相关的知识,希望对你有一定的参考价值。

文章大纲

引言

随着技术的进步,使用android Studio进行NDK 项目开发也变得愈发简单和容易,以前Android NDK——实战演练之配置NDK及使用Android studio开发Hello JNI并简单打包so库(二)这一文章中开发JNI的步骤也有所不同,我这篇文章就进行一些更新和一些全新知识的分享。

一、Android Studio 的NDK编译工具

Android Studio 编译NDK的工具有两种: CMake(默认,强烈推荐使用)和 ndk-build 。 无论是使用CMake还是ndk-build进行编译,NDK都会将 C 和 C++ 代码编译到原生库(.so或者.a文件中)中,最终由使用 IDE 的集成编译系统 Gradle 将原生库打包到 APK 中

  • ndk-build——NDK使用的ndk-build 脚本(Android.mk 和 Application.mk )是基于 Make 的编译系统构建项目。

  • CMake——NDK 通过工具链文件支持 CMake,工具链文件(位于 NDK 中的 < NDK >/build/cmake/android.toolchain.cmake)是用于自定义交叉编译工具链行为的 CMake 文件。

二、CMake

CMake是一种一个比make更高级的跨平台编译工具,可以用简单的语句来描述所有平台的安装(编译过程),并且能够输出各种各样的makefile或者project文件。但CMake 并不直接建构出最终的软件,而是产生其他工具的脚本(如Makefile)。它可以根据不同平台、不同的编译器,生成相应的Makefile或者vcproj项目,从而达到跨平台的目的。Android Studio利用CMake生成的是ninja脚本(ninja是一个小型的高速的构建系统)。主要是通过编写CMakeLists.txt文件,然后用cmake命令将CMakeLists.txt文件转化为make所需要的makefile文件,最后用make命令编译源码生成可执行程序或原生库(当然在Android Studio中我们不需关注具体步骤)。总结起来CMake的编译基本就两个步骤——cmakemake,如下图命令行所示:

cd build 
cmake .. 
make 

其中cmake … 在build里生成Makefile,make根据生成makefile文件,编译程序,make应当在有Makefile的目录下,根据Makefile生成可执行文件。

1、CMake的基本语法

1.1、变量定义和引用

CMake中变量的值要么是String,要么是List(多个String由分号“;”连接形成List),CMake没有赋值操作符的操作,只能通过set、option来定义变量,其中option只能定义OFF,ON的变量。

set(<variable_name> <value>... [PARENT_SCOPE])

option

option(<option_variable> "turn or on status" [initial value])

可以使用$variable_name来引用变量,如果变量没有定义返回空,更多详情参考CMake本身定义的系统变量

1.2、command

CMake代码由一系列command的调用组成,小到一个条件语句的判断都是command(command名大小写不敏感),通用格式:

commandIdentifier(以空格隔开的参数表)

command接收的参数有四种形式:

  • 引号"“形式——即放在”“内部的参数,”“会被当成一个参数传进函数,”"内部的变量引用或转义会
    被解析,可以用\\表示字符串还没有结束。
  • 无引号形式——CMake支持参数不带任何引号,因为所有值都会转换成String,所有的参数会被封装成List,所以参数列表内如果一个字符串用;分割则分号;两边会被当成两个参数。
  • 方括号形式。

1.3、add_library 和add_executable

add_library是通过指定的源文件创建原生库( Add a library to the project using the specified source files),可以创建四种类型的原生库:

  • Normal Libraries
add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            source1 [source2 ...])
  • Imported Libraries
add_library(<name> <SHARED|STATIC|MODULE|OBJECT|UNKNOWN> IMPORTED
            [GLOBAL])
  • Object Libraries
add_library(<name> OBJECT <src>...)
  • Alias Libraries
add_library(<name> ALIAS <target>)
  • Interface Libraries
add_library(<name> INTERFACE [IMPORTED [GLOBAL]])

add_executable而通过指定的源文件创建可执行文件,语法和add_library类似。

1.4、find_library

1.5、target_link_libraries

1.6、include_directories

相当于环境变量中增加路径到CPLUS_INCLUDE_PATH变量的作用。即为了确保 CMake 可以在编译时定位头文件,可以使用 #include < xx > 引入,否则需要使用 #include “path/xx”

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

例如:include_directories(src/main/cpp/include/)

比如下图在引入giflib时在源文件中找不到对应的头文件,即使已经把giflib下的所有源文件都假如到编译过程了,但是如果是以下图的结构的话直接使用 #include “gif_lib.h” 引入是会报错的,因为根据以下工程结构来说是需要指定对应的相对地址—— #include “giflib/gif_lib.h” 才可以找到(使用<> 引用的话相当于是去系统预置的环境变量中对应的路径去查找,而使用" "引用的话相当于是去当前文件的对应的路径下去查找,而使用include_directoried也就相当于是添加路径到头文件的环境变量)

总之include_directories指令就是让我们可以不通过相对路径也能引用到,在CMakeList里添加上 include_directories(src/main/giflib) 即可

1.6、message

message 是CMake 提供给用户的日志打印command

message([<mode>] "message to display" ...)

其中mode代表的是日志消息的类型:

  • (none) —— 重要消息
  • STATUS —— 附带消息
  • WARNING —— CMake警告,继续处理
  • AUTHOR_WARNING —— CMake警告(dev),继续处理
  • SEND_ERROR —— CMake错误,继续处理,但跳过生成
  • FATAL_ERROR —— CMake错误,停止处理和生成
  • DEPRECATION —— 如果分别启用了变量CMAKE_ERROR_DEPRECATED或- CMAKE_WARN_DEPRECATED,则CMake弃用错误或警告,否则无消息

不同的Gradle 版本显示的消息类型(有些版本直接忽略 (none) 和 STATUS类型的)和位置都有所区别,有的是显示在build 窗体下,有些是在cmake_server_log.txt和build_output.txt下,建议直接使用WARNING(会打印出 CMakeLists.txt 目录以及 行号,后面才是要输出的消息,形如这样子的日志CMake Warning at CMakeLists.txt:22 (message):xxxx)

# 1. 声明要求的cmake最低版本
cmake_minimum_required( VERSION 3.4.1)
​
# 2. 添加c++11标准支持
set( CMAKE_CXX_FLAGS "-std=c++11" )
​
# 3. 声明一个cmake工程
PROJECT(rpt_main)

​#打印相关消息消息
MESSAGE(STATUS "Project: SERVER") 
  
# 4. 头文件
include_directories(
$PROJECT_SOURCE_DIR/../include/mq 
$PROJECT_SOURCE_DIR/../include/incl 
)
​
# 5. 通过设定SRC变量,将源代码路径都给SRC,如果有多个,可以直接在后面继续添加
set(SRC 
$PROJECT_SOURCE_DIR/../include/incl/tfc_base_config_file.cpp 
$PROJECT_SOURCE_DIR/../include/mq/tfc_ipc_sv.cpp 
$PROJECT_SOURCE_DIR/local_util.cpp
$PROJECT_SOURCE_DIR/AgentMemRpt.cpp 
)
​
# 6. 创建共享库/静态库
​
# 设置路径(下面生成共享库的路径)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY $PROJECT_SOURCE_DIR/lib)
# 即生成的共享库在工程文件夹下的lib文件夹中
 
set(LIB_NAME rpt_main_lib)
# 创建共享库(把工程内的cpp文件都创建成共享库文件,方便通过头文件来调用)
# 这时候只需要cpp,不需要有主函数 
# $PROJECT_NAME是生成的库名 表示生成的共享库文件就叫做 lib工程名.so
# 也可以专门写cmakelists来编译一个没有主函数的程序来生成共享库,供其它程序使用
# SHARED为生成动态库,STATIC为生成静态库
add_library($LIB_NAME STATIC $SRC)
 
# 7. 链接库文件
# 把刚刚生成的$LIB_NAME库和所需的其它库链接起来
# 如果需要链接其他的动态库,-l后接去除lib前缀和.so后缀的名称,以链接
# libpthread.so 为例,-lpthread
target_link_libraries($LIB_NAME pthread dl)
   
# 8. 编译主函数,生成可执行文件
# 先设置路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $PROJECT_SOURCE_DIR/bin)
   
# 可执行文件生成
add_executable($PROJECT_NAME $SRC)
   
# 这个可执行文件所需的库(一般就是刚刚生成的工程的库咯)
target_link_libraries($PROJECT_NAME pthread dl $LIB_NAME)

更多详请参阅cmake-command

2、CMake 工具链android.toolchain.cmake

所谓工具链本质也是一个cmake脚本,执行时候支持一些参数配置,交叉编译的时候主要是对以下十大参数进行配置。

  • ANDROID_TOOLCHAIN
  • ANDROID_ABI
  • ANDROID_PLATFORM
  • ANDROID_STL
  • ANDROID_PIE
  • ANDROID_CPP_FEATURES
  • ANDROID_ALLOW_UNDEFINED_SYMBOLS
  • ANDROID_ARM_MODE
  • ANDROID_ARM_NEON
  • ANDROID_DISABLE_NO_EXECUTE
  • ANDROID_DISABLE_RELRO
  • ANDROID_DISABLE_FORMAT_STRING_CHECKS
  • ANDROID_CCACHE

2.1、传递参数给CMake

开发中我们可以通过以下两种方式传递:

  • Android Studio中是Gradle 自动传递,所以必须通过android.defaultConfig.externalNativeBuild.cmake.arguments传递参数。

  • 通过命令行使用cmake进行编译,则将参数加上 -D前缀再传递给 CMake (如强制 armeabi-v7a 始终使用 Neon 支持,则传递 -DANDROID_ARM_NEON=TRUE)

2.2、CMake支持的参数

在Gradle 构建系统中都是通过android.defaultConfig.externalNativeBuild.cmake.arguments传递参数。

 android 
	 ...
	 defaultConfig 
	     minSdkVersion 21
	     externalNativeBuild 
	     	//传给CMake的参数
	         cmake 
	         	 arguments "ANDROID_ARM_MODE=ARM","ANDROID_LD=lld"	
	             cppFlags ""
	             //Gradle 的build task执行时(使用defaultConfig时)只会把abi架构为armeabi-v7a和x86的so打包进apk
	             abiFilters "armeabi-v7a","x86"
	             ...
	         
	     
	 
	 externalNativeBuild 
        cmake 
        //指定CMakeLists的路径和版本,build.gradle中默认使用3.6.0;若要使用其他版本可在build.gradle 文件中指定。
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        
    
 

2.2.1、ANDROID_ABI(必传)

ANDROID_ABI表示目标的CPU架构,Gradle已经自动提供了,不需要在build.gradle文件中配置,但是可以通过abiFilters 参数指定Gradle 只打包编译设置的ABI架构的原生库,若不配置则会把所有架构的原生库打包进APK,只支持以下的值:

  • armeabi-v7a
  • armeabi-v7a with NEON 与 -DANDROID_ABI=armeabi-v7a、 -DANDROID_ARM_NEON=ON 相同。
  • arm64-v8a
  • x86
  • x86_64

业界目前针对so导致apk包过大的问题,正在进行一项名为arm裁剪的工作,将来只会提供arm64-v8a架构的库,去兼容其他架构的,但是目前如果你需要编译不同ABI的版本,还是建议单独编译。

2.2.2、ANDROID_ARM_MODE

指定是否为 armeabi-v7a 生成 ARM 或 Thumb 指令,对其他 ABI 没有影响。

2.2.3、ANDROID_ARM_NEON

为 armeabi-v7a 启用或停用 NEON。对其他 ABI 没有影响。 API 23(minSdkVersion 或 ANDROID_PLATFORM)或更新版本默认为 true,以下则为 false。

2.2.4、ANDROID_LD

选择要使用的链接器,可通过此参数启用,只能传递两个值:lld(启动lld)和默认连接器。

2.2.5、ANDROID_NATIVE_API_LEVEL

ANDROID_PLATFORM的别名,用于指定应用或库所支持的最低 API 级别,自动对应于应用的 minSdkVersion,NDK 库无法在 API 级别低于编译代码所用的 ANDROID_PLATFORM 值的设备上运行。

2.2.6、ANDROID_STL

指定要为此应用使用的 STL(C++标准库的支持),默认将使用 c++_static,可以配置以下的值:

  • c++_shared—— libc++ 的共享库变体。
  • c++_static—— libc++ 的静态库变体。
  • 无—— 不支持 C++ 标准库。
  • system——系统 STL

在编译一些库时,很可能因为设置不对导致编译出现大问题

3、CMake交叉编译过程

在我们执行Gradle的build task成功之后,Gradle 插件会在在< project-root >/< module-root >/.cxx/cmake目录下生成一系列的日志文件(包含message输出的信息),无论是学习原理还是调试 CMake ,这些文件都很有意义。

高版本会默认把构建流程的一些详细信息隐藏起来,此时你需要手动去执行build task,另外需要注意build 是支持增量构建的,如果build 第一次build之后, CMakeLists.txt 中的内容不被再次 build,CMake 是不会重新编译的,所以需要修改一下 CMakeLists.txt。

较旧版本的 Android Gradle 插件会将这些文件放入 .externalNativeBuild 目录而不是 .cxx 目录,本文的Gradle 插件是3.5.0。

  • CMakeSystem.cmake——

  • CMakeOutput.log——

  • build.ninja——

  • build_command.txt——Android Gradle 插件会将用于为每个 ABI 和编译类型对执行 CMake 编译的参数保存至 build_command.txt。

  • cmake_server_log.txt——

四、Android Studio 开发NDK项目

Android Studio开发NDK项目,主要有两大步骤:

  • 创建新的原生源代码文件,并将其添加到 Android Studio 项目的cpp源集下。

  • 配置 CMake 以将原生源代码构建入库,配置CMake 主要有两部分的工作:配置CmakeLists.txt和module根目录下的build.gradle脚本 。

  • 提供 CMake 或 ndk-build 脚本文件的路径以配置 Gradle。Gradle 使用构建脚本将源代码导入您的 Android Studio 项目并将原生库(SO 文件)打包到 APK 中。

1、从0开始创建支持C/C++的NDK项目

  • 在向导的 Choose your project 部分中,选择 Native C++ 项目类型。
  • 点击 Next。
  • 填写向导下一部分中的所有其他字段。
  • 点击 Next。
  • 在向导的 Customize C++ Support 部分中,您可以使用 C++ Standard 字段来自定义项目。使用下拉列表选择您想要使用哪种 C++ 标准化。选择 Toolchain Default 可使用默认的 CMake 设置。
  • 点击 Finish

创建成功之后,会发现在java的同级目录下多了一个cpp源集,在这个cpp目录下包含着所有的C/C++的源文件头文件、CMake 构建脚本(CmakeLists.txt)或者或 ndk-build 的构建脚本,配置build.gradle脚本之后,点击Run的时候Android Studio就会开始自动构建NDK项目:

  1. Gradle 调用CMake外部构建脚本 CMakeLists.txt
  2. CMake 按照构建脚本中的命令将 C++ 源代码文件集构建到共享的对象库中,并将其命名为 libxxxx.so,Gradle 随后会将后者打包到 APK 中(可以通过Analyze APK在lib/< ABI >/下的 APK 分析器窗口看到so)。
  3. 当执行 System.loadLibrary() 时自动加载原生库。

2、把已有的项目改造为支持C/C++的NDK项目

  • 首先在与/main目录上右键New -> Directory输入 cpp 作为目录名称,然后点击 OK,创建cpp源集。
  • 创建C/C++ 源代码文件
  • 编写CMakeLists.txt
  • 在build.gradle脚本中配置CMake,主要就是两个配置externalNativeBuild ,参见2.2。

五、NDK的调试

默认情况下是不支持NDK调试的,但我们只要做些简单配置即可实现支持。

1、打开JNI调试 openModuleSettings——>选中module——>Build Types——>Jni Debuggable为true——Apply

2、配置Android Native - Debugger run——>Edit configurations——>选中对应的module——>Debugger——>Debugger Type 选native——Apply

3、下载安装LLDB,Done。

LLDB是Android Studio 用于调试原生代码的调试程序。

以上是关于Android NDK——必知必会之Android Studio使用CMake构建NDK项目的背后的故事的主要内容,如果未能解决你的问题,请参考以下文章

Android NDK——必知必会之Android Studio使用CMake构建NDK项目的背后的故事

2021Android App开发工作必知必会之性能优化

2021Android App开发工作必知必会之性能优化

2021Android App开发工作必知必会之性能优化

Android项目实战 | 从零开始写app(14)实现图片发布模块 | 必知必会之调用系统相机拍照相册一一解决android7 打开相机闪退奔溃问题

前端必知必会之文件上传攻略