前言
本文基于自己所学到的NDk的知识和一些在网上查到的资料
因为学习NDK的时间不是很长,学到的内容难免有所错漏,希望有问题的可以明确的指出来,我会积极采纳
本文会竭尽可能的将我现在所学到的关于NDK的知识清晰的表达出来,本文主要以NDK的基本概念,NDK的组成部分,NDK的注意事项三个部分组成
实践部分因为本篇文章已经6000多字了,为了不让文章过长,将会在后面的博客中
一. NDK的基本概念
在学习NDK之前,我首先得知道什么是NDK,NDK可以干些什么,使用NDK的好处有哪些,知道了这些,我们才可以更好的学习NDK
1. 什么是NDK
NDK即Native Development Kit
,是Android中的一个开发工具包,为我们提供native开发的环境
NDK是我们实现Java与Native进行交互的一个手段
2. NDK可以干什么
可以快速开发C
、 C++
的动态库,并自动将so
和应用一起打包成 APK
即可通过 NDK
使 Java
与native代码(如 C、C++)交互
NDK在现在很多热门的技术中都有使用,如:Android 音视频开发,热更新,OpenCV,等等
3. 我们为什么要使用NDK
允许程序开发人员直接使用 C/C++ 源代码,极大的提高了 Android 应用程序开发的灵活性
跨平台应用移植、使用第三方库。如:许多第三方库只有 C/C++ 语言的版本,而 Android 应用程序需要使用现有的第三方库,如 FFmpeg、OpenCV等,则必须使用NDK
采用C++代码来处理性能要求高的操作,提高了Android APP的性能
安全性高
4. NDK与SDK的关系
在Android开发中,最常用的是SDK,那么SDK与NDK的关系是什么呢?
在SDK中,我们使用Java来进行开发,而在NDK中,我们使用C++来进行开发
SDK支持了Android开发中的大部分操作,如UI展示,用户与手机的交互等,主要是支持了Android APP开发的基础功能
NDK支持了一些复杂的,比较高级的操作,如音视频的解析,大量数据的运算,提高Android游戏的运行速度等,主要是Android APP的一些高级功能
所以NDK与SDK是并列关系,NDK是SDK的有效补充
二. NDK的组成部分
现在我们知道了NDK是什么,NDK的作业,优点了,那我们该开始正式学习NDK了,但是工欲善其事,必先利其器
,在使用NDK这个工具以前,我们必须先好好地了解NDK
所以本部分将分析NDK中的一些组成及他们的作用
本部分将讲述NDK中的JNI
,二进制文件(.so 和 .a), ABI ,本机编译工具,交叉编译工具等等
1. JNI
-
定义
JNI 即
Java Native Interface
,即Javanative接口JNI是一种编程框架,使得 Java 虚拟机中的 Java 程序可以调用本地应用 / 或库,也可以被其他程序调用。 本地程序一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序
上文中,NDK也是支持Java代码与Native代码的交互,那他们之间有什么区别呢?
实际上,JNI是一个编程框架,是一个抽象的东西,NDK是一个工具包,是一个
所以:NDK是实现JNI的一个手段
作用 支持Java代码与native代码进行交互,即Java代码调用native代码 或者 native代码调用Java代码 native代码主要指
C
和C++
-
使用JNI的原因
有些事情 Java 无法处理时,JNI 允许程序员用其他编程语言来解决,例如,Java 标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序,供 Java 程序调用
-
使用JNI的步骤
使用
Native
关键字定义Java
方法(即需要调用的native方法)使用
javac
编译上述Java
源文件 (即.java文件
)最终得到.class
文件通过
javah
命令编译.class
文件,最终导出JNI
的头文件(.h
文件)使用
Java
需要交互的native代码,以及实现在Java
中声明的Native
方法编译
.so
库文件通过
Java
命令执行Java
程序,最终实现Java
调用native
代码编译生成的.so文件
实际上2345步骤的目的就是生成.so 文件
所以上面的步骤可以归纳为三步:
声明Native方法
实现Native方法,经过一系列操作,最终生成.so 文件
-
加载.so文件,调用Native方法
这里只简单介绍JNI的步骤,具体实现的例子在后面的博客中
2 .so 和 .a 文件
上面我们已经说到,JNI 支持了Java
代码和native
代码的互相调用
但是JNI是直接调用Java代码和native代码吗?
实际上,JNI是调用
java
代码和native
代码编译后的.so
和.a
文件来实现了Java代码和native代码的交互
那么.so
和.a
文件是什么呢?下面我列出了.so和.a文件的一些定义
-
动态链接库 (.so 后缀):
运行时才动态加载这个库;
动态链接库,也叫共享库,因为在 NDK 中用
shared
来表示是动态库现在热门的插件化,热更新以及缩小APK大小等技术都使用了
.so
文件 -
静态链接库 (.a 后缀)
在编译的时候, 就把静态库打包进 APK 中
缺点 : 使用静态库编译, 编译的时间比较长,同时也使得APK比较大
优点 : 只导出一个库, 可以隐藏自己调用的库;
.so
和.a
本质上都是二进制文件,下文我将用二进制文件统称这两个文件每个CPU系统只能使用相对应的二进制文件,即他们不像
jar
包一样,所有的CPU系统都可以使用一个jar包,.so 和 .a 每个系统必须使用自己的,不能使用别的,如armeabi
的.so
文件,不能被应用到x86
中
3. CPU架构
Android 平台,其支持的设备型号繁多,单单就设备的核心 CPU 而言,都有三大类:ARM、x86 和 MIPS
**ARM主要应用于手机中,x86主要应用于PC中
Android中使用x86
主要是因为:因为PC是x86架构,所以PC上的手机模拟器需要x86的二进制文件
而在NDK r17中,有了大的变化:
在NDK r17 以后,NDK 不在支持32位和64位 MIPS
和ARM v5(armeabi)
所以现在NDK只支持ARM
和x86
,而ARM
和x86
又各自分为两种:
简单的来说:ARM 和 x86 各分为 32位和64位两种,所以现在NDK一共支持4种CPU架构
即:ARM 32位 ,ARM 64位 , x86 32位 ,x86 64位
4. ABI
ABI : Application Binary Interface
我们上面说了,每个系统只能使用相对应的二进制文件,
不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)
简而言之:而ABI定义了二进制文件是怎么运行在对应的CPU中的
上文中我们大致了解了Android中常用的CPU架构,而且我们知道,ABI 定义了二进制文件时怎么在CPU中运行的,那么我们可以知道,每一个CPU架构必定有一个相对应的ABI
上面我们已经知道了有四种,那么ABI也有四种,他们分别是:armeabi-v7a,armeabi-v8a,x86,x86_64
ABI | 对应的CPU架构 | 应用 |
---|---|---|
armeabi-v7a | ARM 32位 | 手机 |
armeabi-v8a | ARM 64位 | 手机 |
X86 | X86 32位 | PC |
X86_64 | X86 64位 | PC |
CPU架构中64位的CPU架构兼容32位的ABI和64位的ABI,32位的CPU架构只支持32位的ABI
armeabi-v7a设备只兼容armeabi-v7a
armeabi-v8a设备兼容armeabi-v7a,armeabi-v7a
X86设备只兼容X86_64
X86_64兼容X86,X86_64
NDK编译实际上默认编译出所有系统的文件
但是有时我们只需要使用指定的系统,我们就可以指定编译什么系统,减少二进制文件,避免我们不会使用到的二进制文件被打包到apk中,如我们在PC上使用模拟器来执行APP时,我们需要x86,但是我们APP最终是要在手机上运行的,这样我们只需要ARM的就行了,我们就可以在最终打包APK时,去掉x86的
我们可以使用下面的代码来指定我们要编译什么CPU架构的二进制文件
//在project的build.gradle中
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
5. 编译工具
5.1 本机编译工具
我们已经知道,每个系统只能使用自己系统的二进制文件,本机编译工具正是编译出本机系统可以使用的二进制文件
在Android中可以使用的本机编译工具有两种:ndk-build
和 cmake
-
cmake
我们已经知道,每个系统只能使用自己的二进制文件
那么我们在开发中,就需要编译出对应的二进制文件,而且如果我们的软件想要跨平台,那么我们就得保证在每一个平台上都可以编译
即:如果我们的软件想在Mac OS 和 Windows上运行,那么我们的软件在Mac OS上运行时,要生成可以在Mac OS上运行的二进制文件,在Windows上运行,要生成Windows上可以运行的二进制文件
这样的话,那我们得为每一个平台都要编写一次
MakeFile
文件,这将是一个很无趣,很令人抓狂的操作
而CMake就是针对上述问题的一个工具
它允许开发者编写一种与平台无关的CMakeList.txt文件来定制整个编译流程,然后再根据目标用户的平台逐步生成所需的本地化Makefile和工程文件,如Unix的Makefile或Windows的Visual Studio工程
从而达到“只写一次,到处运行”
即:我们只要写一个 cmakeLists.txt
文件,那么就可以在所有平台中编译出对应的二进制文件
这样的话,当运行在Windows上时,自动生成Windows的,在Mac OS行运行,自动生成MacOS的,但是,我们只需要写一个cmakeList.txt
文件即可!!!
使用 CMake 生成 Makefile 并编译的流程如下:
- 编写 CMake 配置文件 CMakeLists.txt 。
- 执行命令
cmake PATH
或者ccmake PATH
生成 Makefile(ccmake
和cmake
的区别在于前者提供了一个交互式的界面)。其中,PATH
是 CMakeLists.txt 所在的目录。 - 使用
make
命令进行编译。
当然,在Android开发过程中,我们不需要执行上面的流程,因为在 Android studio 当中已经为我们集成了,我们只需要编写cmakeLists.txt
文件,剩下的就交给 Android studio 就行了
在Android开发过程中,使用cmake只需要两个文件,xxx.cpp
和CMakeLists.txt
- cmakeLists.txt 就如上面所述,定义整个的编译流程
- xxx.cpp 则是要被编译的 C++ 文件
具体的实现方式我会在下面列出
- ndk-build
ndk-build是一个和cmake功能差不多的工具,他们都减少了我们定制编译流程的操作
但是ndk-build是以前常使用的工具,我们现在常用cmake,ndk-build的操作要比cmake复杂一些
ndk-build
本质上是一个脚本,它的位置就在NDK目录的最上层,即在< NDK >/ndk-build
路径下
因为是一个脚本,所以下面的命令等同于ndk-build
# $GNUMAKE 指 GNU Make 3.81 或更高版本
# <ndk> 则指向 NDK 安装目录
$GNUMAKE -f <ndk>/build/core/build-local.mk <parameters>
#等同于
ndk-build
使用ndk-build我们需要两个文件: Android.mk 和 Application.mk
-
Android.mk
Google的官方文档对Android.mk的定义如下
Android.mk
文件位于项目jni/
目录的子目录中,用于向构建系统描述源文件和共享库。它实际上是构建系统解析一次或多次的微小 GNU makefile 片段。Android.mk
文件用于定义Application.mk
、构建系统和环境变量所未定义的项目级设置。它还可替换特定模块的项目级设置。Android.mk
的语法支持将源文件分组为“模块”。模块是静态库、共享库或独立的可执行文件。您可在每个Android.mk
文件中定义一个或多个模块,也可在多个模块中使用同一个源文件。构建系统只将共享库放入您的应用软件包。此外,静态库可生成共享库。除了封装库之外,构建系统还可为您处理各种其他事项。例如,您无需在
Android.mk
文件中列出头文件或生成的文件之间的显式依赖关系。NDK 构建系统会自动计算这些关系。因此,您应该能够享受到未来 NDK 版本中支持的新工具链/平台功能带来的益处,而无需处理Android.mk
文件。此文件的语法与随整个 Android 开源项目分发的
Android.mk
文件中使用的语法非常接近。虽然使用这些语法的构建系统实现并不相同,但通过有意将语法设计得相似,可使应用开发者更轻松地将源代码重复用于外部库。
这个的定义很长,简单的来说,Android.mk的作用如下:
- 描述了源文件的位置和名字
- 描述了生成什么文件,如共享库,静态库等
- 自动处理构建中的一些事项,如文件之间的依赖关系
- 语法设置得与Android开源项目相似,使得我们可以轻易的将源代码重复使用
Android.mk的一些常用的变量如下:
# 这个是源文件的路径,call my-dir表示了返回当前Android.mk所在的目录
LOCAL_PATH := $(call my-dir)
# 清除许多 LOCAL_XXX 变量
# 注意:不会清除 LOCAL_PATH
include $(CLEAR_VARS)
# LOCAL_MODULE 变量存储要构建的模块的名称
# 这里最终会生成叫 libhello-jni.so 的文件
LOCAL_MODULE := hello-jni
# 源文件名字
# 可以有多个源文件,使用空格隔开
LOCAL_SRC_FILES := hello-jni.c
# 指定编译出什么二进制文件
# 这里编译出共享库,即:.so文件
# 编译出静态库可以使用: BUILD_STATIC_LIBRARY
include $(BUILD_SHARED_LIBRARY)
-
Application.mk
Application.mk
指定 ndk-build 的项目范围设置。默认情况下,它位于应用项目目录中的jni/Application.mk
下简单的来说,Application.mk的功能主要是:主要是描述了Android Native开发需要的模组(module)
一些常用的变量如下:
# 定义生成的二进制文件要生成的CPU架构
# 这里指定生成 arm64-v8a 可以用的二进制文件
APP_ABI := arm64-v8a
# 定义可以使用该二进制文件的Android版本
APP_PLATFORM := android-21
# 默认情况下,ndk-build 假定 Android.mk 文件位于项目根目录的相对路径 jni/Android.mk 中。
# 要从其他位置加载 Android.mk 文件,将 APP_BUILD_SCRIPT 设置为 Android.mk 文件的绝对路径。
APP_BUILD_SCRIPT
-
cmake 和 ndk-build 都可以在Android开发中使用,但是现在默认使用的是cmake,以前常用的是ndk-build
ndk-build
的实现过程比cmake
复杂的多,所以现在推荐使用cmake但是因为以前的项目常用
ndk-build
,如果我们要参与以前 NDK项目 的开发,那么ndk-build
也是需要了解的所以,如果要新建一个项目,那么推荐使用
cmake
,要参与以前老项目的开发,ndk-build
也不可落下
5.2交叉编译工具
与本机编译对应的,有时我们需要编译出其他系统的二进制文件,如我们在PC上写Android文件,那么我们PC中就需要编译出Android中可以运行的二进制文件
交叉编译工具,又叫交叉编译链( toolchain
)
交叉编译链(编译器、链接器等)来生成可以在其他系统中运行的二进制文件
在NDK中,交叉编译工具主要有两种:clang
和gcc
三. NDK中一些值得注意的事情
1. NDK版本变化的问题
1.1 NDK中编译工具的变化
cmake 和 ndk-build 都可以在Android开发中使用,但是现在默认使用的是cmake,以前常用的是ndk-build
ndk-build
的实现过程比cmake
复杂的多,所以现在推荐使用cmake
但是因为以前的项目常用 ndk-build
,如果我们要参与以前 NDK项目 的开发,那么 ndk-build
也是需要了解的
所以,如果要新建一个项目,那么推荐使用cmake
,要参与以前老项目的开发,ndk-build
也不可落下
1.2 NDK中交叉编译的工具的变化
在ndk r17c 以后默认使用的变成了clang,而不是gcc
库文件和头文件的变化
在r17c以前,头文件,库文件以及gcc的路径如下:
# 库文件路径 android-ndk-r17c/platforms/android-21/arch-arm/usr/lib # 头文件路径 android-ndk-r17c/sysroot/usr/include #gcc的路径 android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
而在r17以后,如 r20 中,头文件和库文件统一放到了sysroot
中:
#头文件
toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include
#库文件
toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib
我们可以发现 r20 把头文件和库文件弄到了一起
但是,r20 中也没有把 r17 中的库文件删除,即我们也可以在下面的路径中找到头文件
android-ndk-r17c/platforms/android-21/arch-arm/usr/lib
交叉编译工具位置的变化
在 NDK r19以前的 ndk
内置了一个可以自动生成交叉编译工具(toolchain)
的.py
文件,放在
ndk路径下面的build/tool/make_standalone_toolchain.py
要生成toolchain,使用下面的命令
$NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm --api 21 --install-dir /Users/fczhao/Desktop
后面的几个都是必要的
--arch 指定了生成的toolchain要在哪个CPU框架中使用
--api 指定了生成的toolchain要在哪个Android API 中使用
--install-dir 生成的toolchain的路径
如果使用的是NDK r19以前的
,可以参考下面的这个文章
https://developer.android.com/ndk/guides/standalone_toolchain?hl=zh-cn
但是NDK r19
以后的NDK已经内置了这些文件,如果运行上面的命令,会出现这样的日志
WARNING:__main__:make_standalone_toolchain.py is no longer necessary. The
#$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin 这个路径已经有了我们要生成的文件
$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin directory contains target-specific scripts that perform
the same task. For example, instead of:
$ python $NDK/build/tools/make_standalone_toolchain.py \
--arch arm --api 21 --install-dir toolchain
$ toolchain/bin/clang++ src.cpp
Instead use:
$ $NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi21-clang++ src.cpp
Installation directory already exists. Use --force.
然后让我们去上面输出的路径中看看,就可以看到NDK的确已经提供了很多的clang,这里我只截取了一部分
1.3 NDK支持的CPU架构的变化
上文中我们在ABI已经提到了CPU架构相关的东西,但是为了增加印象,这里稍微重新提一下
Android 平台核心 CPU有三大类:ARM、x86 和 MIPS
而在NDK r17中,有了大的变化:
在NDK r17 以后,NDK 不在支持32位和64位 MIPS
和ARM v5(armeabi)
所以现在NDK中只支持armeabi-v7a,armeabi-v8a,x86,x86_64四类
2. NDK 的系统是否适用的问题
值得注意的是:
在下载NDK时,不像我们下载JDK一样
当下载同一版本的JDK时,所有的电脑(不论是macOS,Windows还是Linus)下载的都是一样
而当我们下载相同版本的NDK时,会发现Google提供了适合于不同系统的NDK,如下图
之所以提供针对每个系统的NDK的原因是是
因为Java良好的跨平台性,所以所有的系统都可以使用同一个JDK
而NDK调用的是C++代码,C++没有这么好的跨平台性,从而我们必须为每一个平台配置适合于它的NDK
所以在我们看别人的NDK博客时,一定要注意是否与自己使用的NDK是一个系统的,一个版本的,否则一定会出现问题
3. NDK 中Java与native代码的交互
Java 和 c/c++ 有两种交互方式:
Java调用 native 代码
native 代码调用 Java 代码
也就是说,交互是Java与C++之间的互相调用
但是在NDK中他们实际上的顺序是:
Java调用C++代码,然后C++调用Java代码返回C++执行后的数据,如图所示
从图中可以看到,我们使用NDK的目的实际上是:
Java调用C++,让C++处理一些复杂的操作,然后把处理后的数据返回到Java中
而我们为什么不直接使用Java来进行处理,而是绕了一圈,通过 Java -> C++ -> Java
的方式来实现,其中的原因是:
C++速度比Java快
举个简单的例子,在ACM或者LeetCode中,C++代码运行的超时时间是 1s,而Java的是 2s
所以对于一些计算量很大的操作,如音视频的渲染等我们采用C++可以有效的减少计算时间和内存占用,减少手机发热和耗电量等
现在我们已经大致了解了一些NDK的东西了,后面的博客我将分别分析cmake,ndk-build,clang,gcc的具体实现过程