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 中:

public native String stringFromJNI();

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

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

./DemoProject/app/src/main/java

javah org.example.demo.MainActivity

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

/* 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目录树如下所示:

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

#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

# 指定 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

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、加载类时就加载动态库即可

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的一些开发技巧

打印日志

// 定义输出的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中配置一下:

......
android {
    ....
    ndkVersion '20.0.5594570'
    sourceSets {
        main {
            // 配置一下库的位置
            jniLibs.srcDirs = ['src/main/cpp/lib']
        }
    }
}

dependencies {
    .....
}

定义支持的平台

......
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的静态库到自定义的动态库中:

├─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

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

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:

#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