乐趣区

关于android:Android-NDK开发扫盲及最新CMake的编译使用

本篇文章旨在简介 Android 中 NDK 是什么以及重点解说最新 Android Studio 编译工具 CMake 的应用

1 NDK 简介

在介绍 NDK 之前还是首推 Android 官网 NDK 文档。

官网文档别离从以下几个方面介绍了 NDK

  1. NDK 的根底概念
  2. 如何编译 NDK 我的项目
  3. ABI 是什么以及不同 CPU 指令集反对哪些 ABI
  4. 如何应用您本人及其他预建的库

本节将会对文档进行总结和补充。所以倡议先浏览一遍文档,或者看完本篇文章再回头看一遍文档。

1.1 NDK 根底概念

首先先用简略的话别离解释下 JNINDK,以及别离和 Android 开发、c/c++ 开发的配合。在解释过程中会对 Android.mkApplication.mkndk-buildCMakeCMakeList 这些常见名词进行扫盲。

JNI(Java Native Interface):Java 本地接口。是为了不便 Java 调用 c、c++ 等本地代码所封装的一层接口(也是一个规范)。大家都晓得,Java 的长处是跨平台,然而作为长处的同时,其在本地交互的时候就编程了毛病。Java 的跨平台个性导致其本地交互的能力不够弱小,一些和操作系统相干的个性 Java 无奈实现,于是 Java 提供了 jni 专门用于和本地代码交互,这样就加强了 Java 语言的本地交互能力。

NDK(Native Development Kit): 原生开发工具包,即帮忙开发原生代码的一系列工具,包含但不限于编译工具、一些公共库、开发 IDE 等。

NDK 工具包中提供了残缺的一套将 c/c++ 代码编译成动态 / 动静库的工具,而 Android.mkApplication.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)生成 对应 makefileproject 文件,而后再调用底层的编译。

在 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 个不同。

  1. main 上面减少了 cpp 目录,即搁置 c/c++ 代码的中央
  2. module-level 的 build.gradle 有批改
  3. 减少了 CMakeLists.txt 文件
  4. 多了一个 .externalNativeBuild 目录

build.gradle

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                arguments "-DANDROID_ARM_NEON=TRUE"
            }
        }
    }
    buildTypes {...}
    externalNativeBuild {
        cmake {path "CMakeLists.txt"}
    }
}
...

因为 CMake 的命令集成在了 gradleexternalNativeBuild 中,所以在 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.gradleapp/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=clangCMake 一共有 2 种编译工具链 – clanggccgcc 曾经废除,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 也会被执行。即会去别离执行 gmathgperf 中的 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 命令的意思是设置指标的一些属性来扭转它们构建的形式。这个命令中设置了 gmathARCHIVE_OUTPUT_DIRECTORY 属性。也就是扭转了输入门路。

add_custom_command 命令是自定义命令。命令中把头文件也复制到了 distribution_DIR 中。

以上就是一个动态库 / 动静库的编译过程。总结以下 3 点

  1. 编译动态库 / 动静库
  2. 批改输入门路
  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 个步骤引入:

  1. 别离创立动态库 / 动静库,间接援用曾经有的 .a 文件 或者 .so 文件
  2. 创立本人利用的库 hello-libs
  3. 退出之前裸露头文件
  4. 链接上动态库 / 动静库

还是很好了解的。编辑好并 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

退出移动版