原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码。
使用 NDK 将 C 和 C++ 代码编译到原生库中,然后使用 Android Studio 的集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过Java原生接口(JNI)调用原生库中的函数。
NDK的作用:
- 进一步提升设备性能,以降低延迟,或运行计算密集型应用,如游戏或物理模拟。
- 重复使用您自己或其他开发者的 C 或 C++ 库。
Android Studio 编译原生库的默认构建工具是CMake。
主要组件:
- 原生共享库:NDK 从 C/C++ 源代码编译这些库或
.so
文件; - 原生静态库:NDK 也可编译静态库或
.a
文件,可将静态库关联到其他库; - Java 原生接口 (JNI):JNI 是 Java 和 C++ 组件用以互相通信的接口;
- 应用二进制接口 (ABI):ABI 可以非常精确地定义应用的机器代码在运行时应该如何与系统交互。NDK 根据这些定义编译
.so
文件。不同的 ABI 对应不同的架构:NDK 为 32 位 ARM、AArch64、x86 及 x86-64 提供 ABI 支持;
开发流程:
- 设计应用,确定要以 Java 实现的部分,以及要以原生代码形式实现的部分;
- 像创建任何其他 Android 项目一样创建一个 Android 应用项目;
- 在“JNI”目录中创建一个描述原生库的
Android.mk
文件,包括名称、标记、关联库和要编译的源文件; - 或者,也可以创建一个配置目标 ABI、工具链、发布/调试模式和 STL 的
Application.mk
文件。对于其中任何您未指明的项,将分别使用以下默认值:所有非弃用的ABI;工具链Clang;发布;系统; - 将原生源代码放在项目的
jni
目录下; - 使用 ndk-build 编译原生(
.so
、.a
)库; - 编译 Java 组件,生成可执行
.dex
文件; - 将所有内容封装到一个 APK 文件中,包括
.so
、.dex
以及应用运行所需的其他文件。
1.下载NDK和工具:
应用编译和调试原生代码,您需要以下组件:
- Android 原生开发套件 (NDK):这套工具使您能在 Android 应用中使用 C 和 C++ 代码。
- CMake:一款外部编译工具,可与 Gradle 搭配使用来编译原生库。如果您只计划使用 ndk-build,则不需要此组件。
- LLDB:Android Studio 用于调试原生代码的调试程序。
2.创建CMake构建脚本:
CMake 构建脚本是一个纯文本文件,必须将其命名为 CMakeLists.txt
,并在其中包含 CMake 构建您的 C/C++ 库时需要使用的命令。
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add_library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
add_library( # Specifies 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 )
# Specifies a path to native header files.
include_directories(src/main/cpp/include/)
find_library( # Defines the name of the path variable that stores the
# location of the NDK library.
log-lib
# Specifies the name of the NDK library that
# CMake needs to locate.
log )
# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the log library to the target library.
${log-lib} )
CMake命令解释:
- cmake_minimum_required():设置CMake所需要的最小版本;
- add_library():设置共享库的名字,设置共享类型,添加C/C++原生代码;名字规范为liblibrary-name.so,library-name为库名字,如libnative-lib.so共享库为native-lib,在Android中加载如下:
static {
System.loadLibrary("native-lib");
}
共享类型有:STATIC/SHARED/MUDULE,STATIC:是对象文件的存档,用于链接其他目标;SHARED:静态加载和运行时加载;MUDULE:是没有链接到其他目标的插件,但是可以在运行时使用dlopen-link功能动态加载;
添加C/C++原生代码:添加C/C++的相对路径。
- find_library():以找到 NDK 库并将其路径存储为一个变量。可以使用此变量在构建脚本的其他部分引用 NDK 库;
- target_link_libraries():为了让原生库能够调用如
log
库中的函数;
关联到原生库:
- NDK 还以源代码的形式包含一些库,您将需要构建这些代码并将其关联到您的原生库。您可以使用 CMake 构建脚本中的
add_library()
命令将源代码编译到原生库中。要提供本地 NDK 库的路径,您可以使用 Android Studio 自动为您定义的ANDROID_NDK
路径变量,以下命令告诉 CMake 要编译android_native_app_glue.c
(负责管理 NativeActivity生命周期事件和触摸输入),并将其链接到静态库native-lib
中:
add_library( app-glue
STATIC
${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )
# You need to link static libraries against your shared native library.
target_link_libraries( native-lib app-glue ${log-lib} )
添加其他预构建库:
include_directories( imported-lib/include/ )
add_library( imported-lib
SHARED
IMPORTED )
set_target_properties( # Specifies the target library.
imported-lib
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
imported-lib/src/${ANDROID_ABI}/libimported-lib.so )
- set_target_properties():命令指定库的路径,${ANDROID_ABI}添加库的多个 ABI 版本的相对路径。
- IMPORT:标记CMake将预构建库导入项目中;
- include_directories():定位构建库的头文件;
包含其他CMake项目:
# Sets lib_src_DIR to the path of the target CMake project.
set( lib_src_DIR ../gmath )
# Sets lib_build_DIR to the path of the desired output directory.
set( lib_build_DIR ../gmath/outputs )
file(MAKE_DIRECTORY ${lib_build_DIR})
# Adds the CMakeLists.txt file located in the specified directory
# as a build dependency.
add_subdirectory( # Specifies the directory of the CMakeLists.txt file.
${lib_src_DIR}
# Specifies the directory for the build outputs.
${lib_build_DIR} )
# Adds the output of the additional CMake build as a prebuilt static
# library and names it lib_gmath.
add_library( lib_gmath STATIC IMPORTED )
set_target_properties( lib_gmath PROPERTIES IMPORTED_LOCATION
${lib_build_DIR}/${ANDROID_ABI}/lib_gmath.a )
include_directories( ${lib_src_DIR}/include )
# Links the top-level CMake build output against lib_gmath.
target_link_libraries( native-lib ... lib_gmath )
要构建多个 CMake 项目并在 Android 项目中包含它们的输出,可以使用一个CMakeLists.txt
个作为顶级CMake构建脚本,加其他 CMake 项目作为此构建脚本的依赖项;
- add_subdirectory():将另一个
CMakeLists.txt
文件指定为构建依赖项,然后关联其输出。
3.Java原生接口(JNI):
JNI 是指 Java 原生接口。它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)互动的方式。JNI 不依赖于供应商,支持从动态共享库加载代码,虽然较为繁琐,但有时相当有效。
尽量减少 JNI 层的占用空间,遵循原则:
- 尽可能减少跨 JNI 层编组资源的次数;
- 尽可能避免在使用受管理编程语言编写的代码与使用 C++ 编写的代码之间进行异步通信;
- 尽可能减少需要接触 JNI 或被 JNI 接触的线程数;
- 将接口代码保存在少量易于识别的 C++ 和 Java 源位置,以便将来进行重构。
JavaVM和JNIEnv:
JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”。两者本质上都是指向函数表的二级指针。(在 C++ 版本中,它们是一些类,这些类具有指向函数表的指针,并具有每个通过该函数表间接调用的 JNI 函数的成员函数。)
- JavaVM:提供“调用接口”函数,您可以利用此类来函数创建和销毁 JavaVM。理论上,每个进程可以有多个 JavaVM,但 Android 只允许有一个;
- JNIEnv:提供了大部分 JNI 函数。原生函数都会收到 JNIEnv 作为第一个参数;
线程:
所有线程都是 Linux 线程,由内核调度。线程通常从受管理代码启动(使用 Thread.start
),但也可以在其他位置创建,然后附加到 JavaVM。Android 不会挂起执行原生代码的线程。如果正在进行垃圾回收,或者调试程序已发出挂起请求,则在线程下次调用 JNI 时,Android 会将其挂起。通过 JNI 附加的线程在退出之前必须调用 DetachCurrentThread
。
jclass/jmethodID/jfieldID:
- jclass:使用FindClass获取类的对象引用;
- jmethodID:使用GetFieldID获取字段的ID;
- jfieldID:使用适当内容获取字段的内容,如GetIntField.
要调用方法,首先要获取类对象引用,然后获取方法 ID。方法 ID 通常只是指向内部运行时数据结构的指针。查找方法 ID 可能需要进行多次字符串比较,但一旦获取此类 ID,便可以非常快速地进行实际调用以获取字段或调用方法。
局部引用和全局引用:
传递给原生方法的每个参数,以及 JNI 函数返回的几乎每个对象都属于“局部引用”。这意味着,局部引用在当前线程中的当前原生方法运行期间有效。 在原生方法返回后,即使对象本身继续存在,该引用也无效;
获取非局部引用的唯一方法是通过 NewGlobalRef
和 NewWeakGlobalRef
函数;