CMake 进行 NDK 开发

在 Android 平台上开发音视频相关的项目,必定会涉及到 NDK (Native Develop Kit) 开发,Android 提供了 ndk-bundle 一系列 NDK 工具集。在早期 Android 的 NDK 开发会涉及到编写 Android.mk 文件,也就是 Android 专用的 MakeFile,现在已经不推荐这种方式了,所以还是拥抱 CMake 吧 (虽然 CMake 目前也不是最好的,但是相比 Android.md 确实已经方便了很多,另外 CMake 也可以用来构建其他的 C/C++ 项目)。所以在此记录一下如何使用 CMake 快速搭建一个 Android NDK 项目。

开发中常用的库

NDK 通常会引入一些库,下面大致列举了一下经常会用到的组件。C 库、Math 库、最小的 C++ 库、ZLib 压缩库、POSIX 线程、Android 日志库、Android 原生应用 API、OpenGL ES (包括 EGL) 库、OpenSL ES 库等。

原始的 NDK 开发方式

要完成一个 Java 层调用 Native 代码的项目,大致步骤如下。

1、编写一个 Java 类,并且在某个方法签名的修饰符中加上 native 修饰符。

2、使用 javac 命令编译第 1 步中的 Java 类,使之成为一个 class 文件。

3、使用 javah 命令将第 2 步的输出作为输入,生成 JNI 的头文件。

4、将 JNI 头文件复制到项目下的 jni 目录,并且建立一个 cpp 的实现文件实现该 JNI 头文件中的函数。

5、编写 Android.mk 文件,加入第 4 步的本地代码,利用 ndk-build 生成动态链接库。

6、在 Java 类中加载第 5 步生成的动态链接库。

7、在 Java 类中调用该 Native 方法。

Android.mk 就是 Android 构建原生库时用的 MakeFile,由于谷歌已经不推荐使用 Android.mk,而且 Android.mk 不再 Android6.0 以上的预编译动态库,所以干脆直接弃用。

通过 CMake 进行 NDK 开发

通过 CMake 的方式进行 NDK 开发,其实 Gradle 已经对 CMake 提供了比较完整的支持,大致步骤如下:

0、给项目添加 NDK 支持

File -> Project Structure -> Modules -> NDK Version -> 选择 NDK 版本

1、编写一个 Java 类,并且在某个方法签名的修饰符中加上 native 修饰符。

比如在 MainActivity 中:

1
public native String stringFromJNI();

2、使用 javah 命令生成头文件

项目根目录 /app/src/main/java/ 文件夹下执行 javah 命令生成头文件:

1
2
3
./DemoProject/app/src/main/java

javah org.example.demo.MainActivity

然后就会生成 org_example_demo_MainActivity.h 的头文件,为了方便现在直接改为 native-lib.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_example_demo_MainActivity */

#ifndef _Included_org_example_demo_MainActivity
#define _Included_org_example_demo_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_example_demo_MainActivity
* Method: stringFromJNI
* Signature: () Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_example_demo_MainActivity_stringFromJNI
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

3、在 app/src/main 新建 cpp 文件夹,cpp 下新建 include 文件夹,将 native-lib.h 放在 include 文件夹下,然后在 cpp 文件夹下新建 native-lib.c/.cpp,新建 CMakeLists.txt。此时 src 目录树如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src
├── androidTest
├── main
│   ├── AndroidManifest.xml
│   ├── cpp
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   │   └── native-lib.h
│   │   └── native-lib.cpp
│   ├── java
│   │   ├── org
│   │      └── example
│   │      └── demo
│   │      └── MainActivity.java
│   └── res
└── test

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "include/native-lib.h"
#include <android/log.h>

// 定义输出的 TAG
const char* LOG_TAG = "NDK-DEMO";

extern "C" JNIEXPORT jstring JNICALL
Java_org_example_demo_MainActivity_stringFromJNI(
JNIEnv *env, jobject thiz){
std::string hello = "Hello from C++, Tim";

// 打印日志
__android_log_print (ANDROID_LOG_ERROR, LOG_TAG, "num = % d", 200);

return env->NewStringUTF(hello.c_str());
}

CMakeList.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 指定 cmake 最小版本 
cmake_minimum_required(VERSION 3.10.2)

# 项目名称
project("native-lib")

# 指定编译产出为动态库
add_library( # 动态库名称
native-lib
# 指定为动态库,静态库则为 STATIC
SHARED
# 提供源文件路径
native-lib.cpp)

# 指定此动态库包含的头文件
target_include_directories(
# 库名称
native-lib
# 作用域
PRIVATE
# 头文件路径
include/native-lib.h
)

# 搜索预编译的库,这里添加了日志库
find_library(log-lib
log )
# 链接日志库
target_link_libraries(
# 指定目标库
native-lib
# 将目标库链接到 NDK 的日志库
${log-lib}
)

4、配置 app 的 build.gradle,然后同步 Sync Now

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
.....
defaultConfig {
.....
// 设置 cmake 编译脚本
externalNativeBuild {
cmake {
cppFlags ""
}
}

// 如果是在模拟器上运行还需要配置 x64 平台
ndk {
abiFilters 'armeabi-v7a'
}

}

.....

externalNativeBuild {
cmake {
path file ('src/main/cpp/CMakeLists.txt')
version '3.10.2'
}
}
ndkVersion '22.1.7171670'
}

dependencies {
.....
}

5、加载类时就加载动态库即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {
......
// 加载动态库
static {
System.loadLibrary ("native-lib");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
......
mTextView.setText (stringFromJNI ());
}

// 定义的 native 方法
public native String stringFromJNI();
}

直接生成 NDK 项目

其实新建项目时直接选择 Native C++ 即可完成上述配置。

Android NDK 目录结构

Android 所提供的 NDK 根目录下的结构:

ndk-build:该 Shell 脚本是 Android NDK 构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了。

ndk-gdb:该 Shell 脚本允许用 GUN 调试器调试 Native 代码,并且可以配置到 IDE 中,可以做到像调试 Java 代码一样调试 Native 的代码。

ndk-stack:该 Shell 脚本可以帮助分析 Native 代码崩溃时的堆栈信息。

build:该目录包含 NDK 构建系统的所有模块。

platforms:该目录包含支持不同 Android 目标版本的头文件和库文 件,NDK 构建系统会根据具体的配置来引用指定平台下的头文件和库文件。

toolchains:该目录包含目前 NDK 所支持的不同平台下的交叉编译器 ——ARM、x86、MIPS,其中比较常用的是 ARM 和 x86。构建系统会根据具体的配置选择不同的交叉编译器。

NDK 的一些开发技巧

打印日志

1
2
3
4
5
6
7
8
// 定义输出的 TAG
#define LOG_TAG "MP3Encoder"

#define LOGV (...) __android_log_print (ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD (...) __android_log_print (ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI (...) __android_log_print (ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW (...) __android_log_print (ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE (...) __android_log_print (ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

引入三方库

库放在 src /main/cpp /lib 下即可,在 Gradle 中配置一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
......
android {
....
ndkVersion '20.0.5594570'
sourceSets {
main {
// 配置一下库的位置
jniLibs.srcDirs = ['src/main/cpp/lib']
}
}
}

dependencies {
.....
}

定义支持的平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
......
android {
......
defaultConfig {
......
// 设置 cmake 编译脚本
externalNativeBuild {
cmake {
cppFlags '-std=c++11'
}
}

ndk {
// 设置支持的架构平台(x86 主要是给模拟器用的)
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
......
externalNativeBuild {
cmake {
path file ('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
}

dependencies {
......
}

Android 中引入 FFmpeg & RTMP

引入 FFmpeg & RTMP 的静态库到自定义的动态库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
├─app
│ ├─libs
│ └─src
│ ├─androidTest
│ ├─main
│ │ ├─cpp
│ │ │ ├─ffmpeg
│ │ │ │ ├─include
│ │ │ │ │ ├─libavcodec
│ │ │ │ │ ├─libavfilter
│ │ │ │ │ ├─libavformat
│ │ │ │ │ ├─libavutil
│ │ │ │ │ ├─libswresample
│ │ │ │ │ └─libswscale
│ │ │ │ └─libs
│ │ │ │ └─armeabi-v7a
│ │ │ │ └─libavcodec.a
│ │ │ │ └─libavfilter.a
│ │ │ │ └─libavformat.a
│ │ │ │ └─libavutil.a
│ │ │ │ └─libswresample.a
│ │ │ │ └─libswscale.a
│ │ │ ├─rtmp
│ │ │ │ └─libs
│ │ │ │ └─armeabi-v7a
│ │ │ │ └─librtmp.a
│ │ │ ├─CMakeLists.txt
│ │ │ └─native-lib.cpp
│ │ ├─java
│ │ └─res
│ └─test
│ └─java
└─gradle
└─wrapper

CMakeList.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
cmake_minimum_required(VERSION 3.10.2)

project("xxxx")

set(FFMPEG ${CMAKE_SOURCE_DIR}/ffmpeg) # ffmpeg path
set(RTMP ${CMAKE_SOURCE_DIR}/rtmp) # rtmp path

include_directories(${FFMPEG}/include)

# 批量导入源文件
file(GLOB SRC_FILES *.cpp)

link_directories(
${FFMPEG}/libs/${CMAKE_ANDROID_ARCH_ABI}
${RTMP}/libs/${CMAKE_ANDROID_ARCH_ABI}
)

add_library(
native-lib
SHARED
${SRC_FILES})

find_library(
log-lib
log)

target_link_libraries(
native-lib
${log-lib}
# 忽略顺序的方式,导入
-Wl,--start-group
avcodec avfilter avformat avutil swresample swscale
-Wl,--end-group
z # libz.so 库,是 FFmpeg 需要用 ndk 的 z 库,FFMpeg 需要额外支持 libz.so
rtmp
android
OpenSLES
)

app build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
plugins {
id 'com.android.application'
}

android {
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
}
}

ndk {
abiFilters 'armeabi-v7a'
}
}

externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}

dependencies {
......
}

注意点:

1、在 add_executable 之前就应该设置 link_directories

2、引入 FFMpeg 等库时,通过 extern C 的方式引入,否则默认是 C++ Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <jni.h>
#include <string>

extern "C"
{
#include "libavutil/avutil.h"
}

extern "C"
JNIEXPORT jstring JNICALL
Java_cn_tim_xxx_XXXPlayer_getFFmpegVersion(JNIEnv *env, jobject thiz) {
// TODO: implement getFFmpegVersion ()
std::string info = "FFmpeg version = ";
info.append(av_version_info());
return env->NewStringUTF(info.c_str());
}

FFmpeg4.2.1 static lib download url