Android NDK开发扫盲及最新CMake的编译使用 - 简书

标签: | 发表时间:2019-06-27 11:23 | 作者:
出处:https://www.jianshu.com

本篇文章旨在简介 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语言的本地交互能力。 上述部分文字摘自任玉刚的 Java JNI 介绍

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

New Project

项目创建好以后我们可以看到和普通Android项目有以下4个不同。

  1. main下面增加了 cpp目录,即放置 c/c++ 代码的地方
  2. module-level 的 build.gradle有修改
  3. 增加了 CMakeLists.txt文件
  4. 多了一个 .externalNativeBuild目录
Difference

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中查到。如下

cmake command

更多的可以填写的命令参数和含义可以参见 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实现了如何创建出静态库/动态库,并引用它。现在我们把代码拉下来看下具体是如何实现的。

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项目拉下来。里面有多个实例参考,比官方文档完整很多。

Google Samples

当你发现示例里的一些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

相关 [android ndk 开发] 推荐:

Android NDK开发Crash错误定位

- - 极客521 | 极客521
在Android开发中,程序Crash分三种情况:未捕获的异常、ANR(Application Not Responding)和闪退(NDK引发错误). 其中 未捕获的异常根据logcat打印的堆栈信息很容易定位错误. ANR错误也好查,Android规定,应用与用户进行交互时,如果5秒内没有响应用户的操作,则会引发ANR错误,并弹出一个系统提示框,让用户选择继续等待或立即关闭程序.

android NDK的学习

- - CSDN博客推荐文章
NDK是基于系统原生的C/C++的开发,但是它不是一种主流,而是Android SDK开发的有益补充,因为NDK没有提供界面,也没有提供生命周期管理这一类环境. NDK是一系列的工具包,使用这些工具包能够让我们很方便的进行JNI的开发. Java native interface,JNI就是java和C/C++相互调用的接口.

Android 之 JNI 开发 详解 - NDK从入门到精通

- - CSDN博客推荐文章
-- 第一个JNI示例程序下载 :  GitHub - https://github.com/han1202012/NDKHelloworld.git . JNI概念 : Java本地接口, Java Native Interface, 它是一个 协议, 该协议用来沟通Java代码和外部的本地C/C++代码, 通过该协议 Java代码可以调用外部的本地代码, 外部的C/C++ 代码可以调用Java代码;.

Android NDK开发扫盲及最新CMake的编译使用 - 简书

- -
本篇文章旨在简介 Android 中. NDK是什么以及重点讲解最新 Android Studio 编译工具. NDK之前还是首推 Android 官方. 官方文档分别从以下几个方面介绍了. ABI是什么以及不同 CPU 指令集支持哪些. 如何使用您自己及其他预建的库. 所以建议先浏览一遍文档,或者看完本篇文章再回头看一遍文档.

基于 Android NDK 的学习之旅-----数据传输一(基本数据类型和数组传输)(附源码)

- UnderSn0w - 博客园-首页原创精华区
基于 Android NDK 的学习之旅-----数据传输(基本数据类型和数组传输).        之前的一些文章都有涉及到上层和中间层的数据传输,简单来说,也就是参数和返回值的使用. 因为中间层要做的最多的也就是数据传输与转换,下面来介绍下这方面的知识.        数据传输可分为 基本数据类型传输 和 引用数据类型的传输 , 因为数组传输也比较特别(其实数组也是引用类型),所以这里也专门分出来讲讲.

NDK开发第一课:环境配置与第一个JNI程序 - 阿飞的博客 - CSDN博客

- -
    JNI 是 Java Native Interface 的缩写,即 Java 的本地接口.     目的是使得 Java 与本地其他语言(如 C/C++)进行交互.     JNI 是属于 Java 的,与 Android 无直接关系.     NDK 是 Native Development Kit 的缩写,是 Android 的工具开发包.

Android开发Tips

- - CSDN博客推荐文章
欢迎Follow我的 GitHub, 关注我的 CSDN.. 介绍一些, 在Android开发中, 会经常使用的小知识点.. submodule与git可以保持实时同步. 导入, 路径多于一个, 前面不添加冒号(:).. 使用PackageManager.. // 检查App是否安装 private boolean appInstalledOrNot(String uri) {.

Android 开发者调查

- - 爱范儿 · Beats of Bits
Startup 是为 Android 开发者提供盈利模式的一个公司. Android 开发者只要在应用上推广 Startup 服务,并且为网站带去流量,就可以得到网站给予的补贴. 今年 3 月的时候,Startup 网站对 Android 开发者进行了一次调查. 现在,他们将调查的结果制成了信息图,并 发布在网站之上.

Android敏捷开发指南

- - 互联网的那点事
本文紧密结合移动开发方法与技术,围绕Android平台的开发探讨提供更高质量移动产品的解决方案. 作者中分析了移动开发中常见的问题,从两方面阐述了ThoughtWorks使用的测试开发方案和相应的架构方法与常用工具应用,并进一步阐述了为移动开发流程所提供的持续发布方案. 随着云计算、移动互联等一系列新技术概念的崛起,新一轮的IT经济正在不断扩大发展.

Android应用开发资源

- - InfoQ cn
Android应用设计和开发人员现在可以参考由Android用户体验(UX)团队官方发布的 Android设计指南. 该指南提供了开发者应该遵循的基本原则,并列出了很多细节指导,涉及 设备与显示、 主题、 触控交互、 度量与栅格、 排版、 色彩、 图标设计,以及如何 编写用户交互界面的提示语.