Android NDK JNI 开发整理

Android JNI 开发整理

前言

这两天收到一个需求,使用ffmpeg 将 pcm 8K 采样率 改成 16K 采样率,才发现自己对JNI 其实不是很熟悉,这里也算是个查漏补缺

基础概念

NDK是什么

官方描述:Android NDK 是一个工具集,可让您使用 C 和 C++ 等语言以原生代码实现应用的各个部分。对于特定类型的应用,这可以帮助您重复使用以这些语言编写的代码库。

简单来说,他就是一个工具包

下载NDK

官网下载

最好放在 Android Studio SDK的目录下

JNI 是什么

全称 Java Native Interface,jni 能够帮助我们 java 调用 C/C++ 的代码

Hello World 尝鲜

配置环境

Android Studio 已经很好的帮助我们去开发NDK了

安装 cmake 和 ndk 的工具包

新建C/C++ 的工程

好了 hello world 就完成了 ,nice, 这里主要就看一下 系统给你生成哪些东西,

实际项目 ffmpeg实战

在实际项目中 我们会将其他平台的代码编译然后通过JNI 去调用 下面我会有个简单的示例。编译ffmpeg 然后使用jni 调用 ffmpeg 的方法

FFmpeg 是个很强大的库,这边使用以及编译和精简好的so

新建一个model

为了只有打包成aar 所以我们新建一个model

目录结构如下

其中 libavutil.solibswresample.so 这个是已经编译好的 FFmpeg 动态库so

cpp/include/libavutilcpp/include/libswresample 这两个文件下方的是FFmpeg 的头文件

CmakeLists.txt 使我们需要编写的 makefile 文件

nativt-lib.cpp: 这个是我们具体的逻辑代码实现的地方

JniHelper:我们编写 java/kotlin navtive

上面介绍了主要的一些文件,下面说一下我们的目标,在Android 上能够 使用 java/kotlin 从 ffmpeg 中获取 ffmpeg 的版本,

编写 CmakeLists.txt

下面的注释写的很清楚了,主要的作用就是讲,ffmpeg 的 动态库能够供我们使用,

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.


#========================================================
# 定义变量 distribution_DIR
#========================================================
set(distribution_DIR ../../../../libs)


#========================================================
#① 设置创建本地库所需CMake最小版本
#========================================================
cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

#========================================================
# ② 添加库 自己编写的库
# 库名称:native-lib
# 库类型:SHARED,表示动态库,后缀为.so(如果是STATIC,则表示静态库,后缀为.a)
# 库源码文件:src/main/cpp/native-lib.cpp
#========================================================
add_library( # Sets the name of the library.
# 设置生成so 的名字 eg: libnative-lib.so
native-lib

# Sets the library as a shared library.
# 设置库的类型
# SHARED 动态库 .so 结尾 eg: libnative-lib.so
# STATIC 静态库 .a 结尾 eg: libnative-lib.a
SHARED

# Provides a relative path to your source file(s).
# 要编译的C/C++文件
# 也可以多个文件 例如
# src/main/cpp/codec/hello.c
# src/main/cpp/codec/world.c
native-lib.cpp)

#========================================================
# ③ 添加库 外部引入的库
# 库名称:avutil 不需要 lib 前缀
# 库类型:SHARED,表示动态库,后缀为.so(如果是STATIC,则表示静态库,后缀为.a)
# IMPORTED: 表明是外部引入的库
#========================================================
add_library(
# 外部引入的库 eg: libavutil.so 则需要导入 avutil
avutil
# 设置库的类型
SHARED
# 使用 IMPORTED 告知 cmake 需要将外部库导入
IMPORTED)

#========================================================
# ④ 设置目标属性
#========================================================
set_target_properties(
# 库名称
# Specifies the target library.
avutil
# Specifies the parameter you want to define.
# 定义库的位置
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
# 指定库的位置
${distribution_DIR}/${ANDROID_ABI}/libavutil.so)


add_library(swresample
SHARED
IMPORTED)
set_target_properties(swresample
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libswresample.so)

#========================================================
# ④ 引入头文件,为了让 CMake 能够在编译时定位您的头文件
# CMAKE_SOURCE_DIR 指的是当前的文件地址
#========================================================
include_directories(${CMAKE_SOURCE_DIR}/include)


#========================================================
# ⑤ 预构建库关联到您自己的原生库
# 简单来说就是讲导入的外部库给自己库引用
#========================================================
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
# 目标库
native-lib

# 依赖库
avutil
swresample

# Links the target library to the log library
# included in the NDK.
# 将日志库导入
${log-lib})


#========================================================
# ⑥ NDK 库并将其路径存储为一个变量
# 以下示例可以找到 Android 专有的日志支持库,并会将其路径存储在 log-lib 中
#========================================================
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
# 变量名称
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
# 库名称
log)

编写 JniHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package com.allens.lib_hello.jni

class JniHelper {

companion object {
//加载库
init {
System.loadLibrary("native-lib")
}
}

external fun helloWorld(): String

external fun getFFmpegVersion():String


}

编写 nativt-lib.cpp

在hello world 中 看到helloWorld的方法前面是不一样的,这里JNI 有一个自己的规则,

小技巧: 可以现在 JniHelper 声明方法,然后 AS 智能补全,自动生成

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

#include <jni.h>
#include <string>



// 因为 FFmpeg 是C 写的 所以用 extern "C" 包裹住
extern "C" {
#include <libavutil/avutil.h>
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_allens_lib_1hello_jni_JniHelper_helloWorld(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}


extern "C"
JNIEXPORT jstring JNICALL
Java_com_allens_lib_1hello_jni_JniHelper_getFFmpegVersion(JNIEnv *env, jobject thiz) {
//av_version_info 函数就是获取版本
return env->NewStringUTF(av_version_info());
}

配置 model的build.grale

这里实在 model 下面的 build.grale 而非 app 下的 build.grale

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"

//=====================================================================================
//配置so库的路径
//=====================================================================================
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}

//=====================================================================================
// 配置cmake编译选项和支持的CPU指令集
//=====================================================================================
externalNativeBuild {
cmake {
//设置c++编译器的可选标志
cppFlags "-frtti -fexceptions -Wno-deprecated-declarations"

//将可选参数传递给CMake。
// arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang"

//设置一个标志来为C编译器启用格式化宏常量。
// cFlags "-D__STDC_FORMAT_MACROS"
}
ndk {
// 设置支持的SO库架构
abiFilters "armeabi-v7a"
}
}
}

buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
//=====================================================================================
// 配置CMakeLists.txt 路径
//=====================================================================================
externalNativeBuild {
cmake {
// CMakeLists.txt 路径
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}

//=====================================================================================
// fix missing and no known rule to make it
//=====================================================================================
packagingOptions {
pickFirst 'lib/armeabi-v7a/libavutil.so'
pickFirst 'lib/armeabi-v7a/libswresample.so'
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

}

配置 app的build.grale

主要就是配置 ndk 的 架构

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
39
40
41
42
43
44
45
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
applicationId "com.allens.ndkdemo"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

//配置cmake编译选项和支持的CPU指令集
externalNativeBuild {
ndk{
abiFilters "armeabi-v7a"
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

implementation project(':lib_hello')
}

打包成 aar

需要注意的是 release 需要配置混淆

Demo

NDKDemo

参考

CMAKE手册