首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。
码字不易,转载请注明出处!
教程代码:【Github传送门】 |
---|
本文将介绍如何将上一篇文章编译出来的
FFmpeg so
库,引入到Android
工程中,并验证so
是否可以正常使用。
在过去,通常使用 makefile
的方式在项目中引入 C/C++
代码支持,随着 Android Studio
的普及,makefile
的方式已经基本被 CMake
替代。
有了 Android
官方的支持,NDK
层代码的开发变得更加容易。以前一谈到 Android NDK
,许多人就会大惊失色,感觉是深不可测的东西,一方面是 makefile
的编写很难,一方面是 C/C++
相比 Java
来说,比较晦涩。
但是不必担心,一是有了 CMake
,二是对于 C/C++
的基本使用其实和 Java
差不多,本系列涉及到的,也都是对 C/C++
的基础使用,毕竟,高级的我也不会不是吗?哈哈哈~~
首先,需要下载 CMake
相关工具,在 Android Studio
中依次点击 Tools->SDK Manager->SDK Tools
,然后勾选
CMake
: CMake 构建工具
LLDB
: C/C++ 代码调试工具
NDK
: NDK 环境
最后依次点击 OK->OK->Finish
,开始下载(文件比较大,可能会比较慢,请耐心等待)。
有两种方式:
一是,新建一个新的工程,并勾选
C/C++
支持选项,系统将自动创建一个支持C/C++
编码的工程。
二是,在已有的项目上,手动添加所有的添加项来支持
C/C++
编码,其实就是自己手动添加「第一种方式」
中Android Studio
为我们自动创建的那些东西。
首先,通过新建一个新工程的方式,看看 IDE
为我们生成了那些东西。
依次点击 File -> New -> New Project
,进入新建工程页面,拉到最后,选择 Native C++
然后按照默认配置,一路 Next -> Next -> Finish
即可。
生成的工程目录如下:
重点关注上图标注的3个地方:
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = stringFromJNI()
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
external fun stringFromJNI(): String
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
很简单,使用过 so
库的应该都看得懂,这里简单说一下。
代码的最下面,companion object
在 Kotlin
中表示静态代码块,类似 Java
中的 static { }
,其中的代码有且只会被执行一次。
接着在 init{}
方法中,加载了 C/C++
代码编译成的 so
库: native-lib
。
往上一句代码,用 external
声明了一个外部引用的方法 stringFromJNI()
,这个方法和 C/C++
层的代码是对应的。
最终在最上面的 onCreate
中,将从 C/C++
层返回的 String
显示出来。
cpp
文件包其中有两个文件非常重要,分别是 native-lib.cpp
、 CMakeLists.txt
。
i. native-lib.cpp
:是一个 C++ 接口文件,在 MainActivity
中声明的外部方法将在这里得到实现。
自动生成 native-lib.cpp
的内容如下:
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_com_chenlittleping_mynativeapp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
可以看到,这个 cpp 文件中的方法命名非常的长,不过其实非常简单。
首先是头部固定写法 extern "C" JNIEXPORT jstring JNICALL
:
extern "C"
表示以 C语言
的方式来编译;
jstring
表示该方法返回类型是 Java
层的 String
类型,类似的还是有: void
jint
等;
然后是 Java 层对应方法的映射,即整个方法命名其实是 Java
层对应方法的绝对路径。
其中,最前面的 Java_
是固定写法;
com_chenlittleping_mynativeapp_MainActivity_
: 对应的是 com.chenlittleping.mynativeapp.MainActivity.
,其实就是 .
换为 _
;
stringFromJNI
和 Java 层的方法一致。
最后是两个参数, JNIEnv *env
和 jobject
,分别代表 JNI
的上下文环境和调用这个接口的 Java
的类的实例。
调用这个方法,将会在 C++
层创建一个字符串,并以 Java#String
的类型返回。
ii. CMakeLists.txt
: 也就是构建脚本。内容如下:
# cmake 最低版本
cmake_minimum_required(VERSION 3.4.1)
# 配置so库编译信息
add_library(
# 输出so库的名称
native-lib
# 设置生成库的方式,默认为SHARE动态库
SHARED
# 列出参与编译的所有源文件
native-lib.cpp)
# 查找代码中使用到的系统库
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)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 列出所有需要链接的库
${log-lib})
这是最简单的编译配置,具体见上面的注释。
CMakeLists.txt
的目的就是配置可以编译出 native-lib
so 库的构建信息。
说白了,就是告诉编译器:
- 编译的目标是谁
- 依赖的源文件在哪里找
- 依赖的 `系统或第三方` 的 `动态或静态` 库在哪里找。
在 第二步
中,已经把构建 so
库的信息配置好了,接下来要把这些信息注册到 Gradle
中,编译器才会去编译它。
app 的 build.gradle
内容如下:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
buildToolsVersion "29.0.1"
defaultConfig {
applicationId "com.chenlittleping.mynativeapp"
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 1) CMake 编译配置
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// 2) 配置 CMakeLists 路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
dependencies {
// 省略无关代码
//......
}
最主要的两个地方是两个 externalNativeBuild
。
第 1 个 externalNativeBuild
中,可以做一些优化配置,比如只打包包含 armeabi
架构的 so
:
externalNativeBuild {
cmake {
cppFlags ""
}
ndk {
abiFilters "armeabi" //, "armeabi-v7a"
}
}
第 2 个 externalNativeBuild
,主要是配置 CMakeLists.txt
的路径和版本。
Android Studio
为我们生成的关于C/C++
支持的主要就是以上三个地方,有了以上配置,就可以在MainActivity
页面中正常的显示出Hello from C++
。
C/C++
支持前面就说过,在已有项目上添加 C/C++
支持,就是由我们自己手动添加整个配置。那么根据签名介绍的三个步骤,依葫芦画瓢,就可以添加了。
这里刚好就用添加 FFMpeg so
到本系列文章现有 Demo 工程中来演示一遍。
首先,在 app/src/main/
目录下,新建文件夹,并命名为 cpp
。
接着,在 cpp
目录下,右键 New -> C/C++ Source File
,新建 native-lib.cpp
文件。
接着,在 cpp
目录下,右键 New -> File
,新建 CMakeLists.txt
,先将上面 IDE
生成的那份代码粘贴进来, FFmpeg的配置在后面详细讲解。
# CMakeLists.txt
# cmake 最低版本
cmake_minimum_required(VERSION 3.4.1)
# 配置so库编译信息
add_library(
# 输出so库的名称
native-lib
# 设置生成库的方式,默认为SHARE动态库
SHARED
# 列出参与编译的所有源文件
native-lib.cpp)
# 查找代码中使用到的系统库
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)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 列出所有需要链接的库
${log-lib})
android {
// ...
defaultConfig {
// ...
// 1) CMake 编译配置
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
// ...
// 2) 配置 CMakeLists 路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
// ...
如果只是简单的编写 C/C++
代码,以上基础配置就可以了。
接着来看看本文的重点,如何使用 CMakeLists.txt
引入 FFmpeg
的动态库。
在 上一篇文章中,我们编译的 FFmpeg so
库的 CPU
架构为 armv7-a
,所以,我们需要把所有的 so
库放置到 armeabi-v7a
目录下。
首先,在 app/src/main/
目录下,新建文件夹,并命名为 jniLibs
。
app/src/main/jniLibs
是 Android Studio 默认的放置 so 动态库的目录。
接着,在 jniLibs
目录下,新建 armeabi-v7a
目录。
最后把 FFmpeg
编译得到的所有 so
库粘贴到 armeabi-v7a
目录。如下:
在编译 FFmpeg
的时候,除了生成 so
外,还会生成对应的 .h
头文件,也就是 FFmpeg
对外暴露的所有接口。
在 cpp
目录下,新建 ffmpeg
目录,然后把编译时生成的 include
文件粘贴进来。
上面已经把 so
和 头文件
放置到对应的目录中了,但是编译器是不会把它们编译、链接、并打包到 Apk
中的,我们还需要在 CMakeLists.txt
中显性的把相关的 so
添加和链接起来。完整的 CMakeLists.txt
如下:
cmake_minimum_required(VERSION 3.4.1)
# 支持gnu++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
# 1. 定义so库和头文件所在目录,方面后面使用
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
# 2. 添加头文件目录
include_directories(${ffmpeg_head_dir}/include)
# 3. 添加ffmpeg相关的so库
add_library( avutil
SHARED
IMPORTED )
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavutil.so )
add_library( swresample
SHARED
IMPORTED )
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswresample.so )
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
add_library( avfilter
SHARED
IMPORTED)
set_target_properties( avfilter
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavfilter.so )
add_library( swscale
SHARED
IMPORTED)
set_target_properties( swscale
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswscale.so )
add_library( avformat
SHARED
IMPORTED)
set_target_properties( avformat
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavformat.so )
add_library( avdevice
SHARED
IMPORTED)
set_target_properties( avdevice
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavdevice.so )
# 查找代码中使用到的系统库
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 )
# 配置目标so库编译信息
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp
)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 4. 连接 FFmpeg 相关的库
avutil
swresample
avcodec
avfilter
swscale
avformat
avdevice
# Links the target library to the log library
# included in the NDK.
${log-lib} )
主要看看注释中新加入的 1~4
点。
1)通过 set
方法定义了 so
和 头文件
所在目录,方便后面使用。
其中
CMAKE_SOURCE_DIR
为系统变量,指向CMakeLists.txt
所在目录。ANDROID_ABI
也是系统变量,指向 so 对应的CPU
框架目录:armeabi、armeabi-v7a、x86 …
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
2)通过 include_directories
设置头文件查找目录
include_directories(${ffmpeg_head_dir}/include)
3)通过 add_library
添加 FFmpeg 相关的 so
库,以及 set_target_properties
设置 so
对应的目录。
其中,add_library 第一个参数为 so 名字,
SHARED
表示引入方式为动态库引入。
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
4)最后,通过 target_link_libraries
把前面添加进来的 FFMpeg so
库都链接到目标库 native-lib
上。
这样,我们就将 FFMpeg
相关的 so
库都引入到当前工程中了。下面就要来测试一下,是否可以正常调用到 FFmpeg
相关的方法了。
要检查 FFmpeg
是否可以使用,可以通过获取 FFmpeg
基础信息来验证。
把获取到的 FFmpeg
信息显示出来。
class FFmpegActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ffmpeg_info)
tv.text = ffmpegInfo()
}
private external fun ffmpegInfo(): String
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
#include
#include
#include
extern "C" {
#include
#include
#include
#include
JNIEXPORT jstring JNICALL
Java_com_cxp_learningvideo_FFmpegActivity_ffmpegInfo(JNIEnv *env, jobject /* this */) {
char info[40000] = {0};
AVCodec *c_temp = av_codec_next(NULL);
while (c_temp != NULL) {
if (c_temp->decode != NULL) {
sprintf(info, "%sdecode:", info);
switch (c_temp->type) {
case AVMEDIA_TYPE_VIDEO:
sprintf(info, "%s(video):", info);
break;
case AVMEDIA_TYPE_AUDIO:
sprintf(info, "%s(audio):", info);
break;
default:
sprintf(info, "%s(other):", info);
break;
}
sprintf(info, "%s[%10s]\n", info, c_temp->name);
} else {
sprintf(info, "%sencode:", info);
}
c_temp = c_temp->next;
}
return env->NewStringUTF(info);
}
}
首先,我们看到代码被包裹在 extern "C" { }
当中,和前面的系统创建的稍微有些不同,通过这个大括号包裹,我们就不需要每个方法都添加单独的 extern "C"
开头了。
另外,由于 FFmpeg
是使用 C
语言编写的,所在 C++
文件中引用 #include
的时候,也需要包裹在 extern "C" { }
,才能正确的编译。
方法的新建就不用说了,和前面介绍的命名方法一致。
在方法中,使用 FFmpeg
提供的方法 av_codec_next
,获取到 FFmpeg 的编解码器,然后通过循环遍历,将所有的音视频编解码器信息拼接起来,最后返回给 Java
层。
至此,FFmpeg
加入到工程中,并被调用。
如果一切正常,App运行后,就会显示出 FFmpeg
音视频编解码器的信息。
如果由提示
so
或者头文件
找不到,需要检查CMakeLists.txt
中设置的so
和头文件
的路径是否正确。