赞
踩
本文基于自己所学到的NDk的知识和一些在网上查到的资料
因为学习NDK的时间不是很长,学到的内容难免有所错漏,希望有问题的可以明确的指出来,我会积极采纳
本文会竭尽可能的将我现在所学到的关于NDK的知识清晰的表达出来,本文主要以NDK的基本概念,NDK的组成部分,NDK的注意事项三个部分组成
实践部分因为本篇文章已经6000多字了,为了不让文章过长,将会在后面的博客中
在学习NDK之前,我首先得知道什么是NDK,NDK可以干些什么,使用NDK的好处有哪些,知道了这些,我们才可以更好的学习NDK
NDK即Native Development Kit
,是Android中的一个开发工具包,为我们提供native开发的环境
NDK是我们实现Java与Native进行交互的一个手段
可以快速开发C
、 C++
的动态库,并自动将so
和应用一起打包成 APK
即可通过 NDK
使 Java
与native代码(如 C、C++)交互
NDK在现在很多热门的技术中都有使用,如:Android 音视频开发,热更新,OpenCV,等等
允许程序开发人员直接使用 C/C++ 源代码,极大的提高了 Android 应用程序开发的灵活性
跨平台应用移植、使用第三方库。如:许多第三方库只有 C/C++ 语言的版本,而 Android 应用程序需要使用现有的第三方库,如 FFmpeg、OpenCV等,则必须使用NDK
采用C++代码来处理性能要求高的操作,提高了Android APP的性能
安全性高
在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中的JNI
,二进制文件(.so 和 .a), ABI ,本机编译工具,交叉编译工具等等
定义
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的步骤,具体实现的例子在后面的博客中
上面我们已经说到,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
中
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位
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'
- }
- }
我们已经知道,每个系统只能使用自己系统的二进制文件,本机编译工具正是编译出本机系统可以使用的二进制文件
在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 PATH
或者 ccmake PATH
生成 Makefile(ccmake
和 cmake
的区别在于前者提供了一个交互式的界面)。其中, PATH
是 CMakeLists.txt 所在的目录。make
命令进行编译。当然,在Android开发过程中,我们不需要执行上面的流程,因为在 Android studio 当中已经为我们集成了,我们只需要编写cmakeLists.txt
文件,剩下的就交给 Android studio 就行了
在Android开发过程中,使用cmake只需要两个文件,xxx.cpp
和CMakeLists.txt
具体的实现方式我会在下面列出
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.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
也不可落下
与本机编译对应的,有时我们需要编译出其他系统的二进制文件,如我们在PC上写Android文件,那么我们PC中就需要编译出Android中可以运行的二进制文件
交叉编译工具,又叫交叉编译链( toolchain
)
交叉编译链(编译器、链接器等)来生成可以在其他系统中运行的二进制文件
在NDK中,交叉编译工具主要有两种:clang
和gcc
cmake 和 ndk-build 都可以在Android开发中使用,但是现在默认使用的是cmake,以前常用的是ndk-build
ndk-build
的实现过程比cmake
复杂的多,所以现在推荐使用cmake
但是因为以前的项目常用 ndk-build
,如果我们要参与以前 NDK项目 的开发,那么 ndk-build
也是需要了解的
所以,如果要新建一个项目,那么推荐使用cmake
,要参与以前老项目的开发,ndk-build
也不可落下
在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,这里我只截取了一部分
上文中我们在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四类
值得注意的是:
在下载NDK时,不像我们下载JDK一样
当下载同一版本的JDK时,所有的电脑(不论是macOS,Windows还是Linus)下载的都是一样
而当我们下载相同版本的NDK时,会发现Google提供了适合于不同系统的NDK,如下图
之所以提供针对每个系统的NDK的原因是
因为Java良好的跨平台性,所以所有的系统都可以使用同一个JDK
而NDK调用的是C++代码,C++没有这么好的跨平台性,从而我们必须为每一个平台配置适合于它的NDK
所以在我们看别人的NDK博客时,一定要注意是否与自己使用的NDK是一个系统的,一个版本的,否则一定会出现问题
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的具体实现过程
作者:TorMooa
链接:https://www.jianshu.com/p/fe54d70a260e
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。