在 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
| #include <jni.h>
#ifndef _Included_org_example_demo_MainActivity #define _Included_org_example_demo_MainActivity #ifdef __cplusplus extern "C" { #endif
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>
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_minimum_required(VERSION 3.10.2)
project("native-lib")
add_library( native-lib SHARED native-lib.cpp)
target_include_directories( native-lib PRIVATE include/native-lib.h )
find_library(log-lib log )
target_link_libraries( native-lib ${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 { ..... externalNativeBuild { cmake { cppFlags "" } } 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 ()); }
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
| #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 { ...... externalNativeBuild { cmake { cppFlags '-std=c++11' } }
ndk { 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) set(RTMP ${CMAKE_SOURCE_DIR}/rtmp)
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 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) { std::string info = "FFmpeg version = "; info.append(av_version_info()); return env->NewStringUTF(info.c_str()); }
|
FFmpeg4.2.1 static lib download url