首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。android
碼字不易,轉載請註明出處!git
教程代碼:【Github傳送門】 |
---|
本文將介紹如何將上一篇文章編譯出來的
FFmpeg so
庫,引入到Android
工程中,並驗證so
是否能夠正常使用。github
在過去,一般使用 makefile
的方式在項目中引入 C/C++
代碼支持,隨着 Android Studio
的普及,makefile
的方式已經基本被 CMake
替代。web
有了 Android
官方的支持,NDK
層代碼的開發變得更加容易。之前一談到 Android NDK
,許多人就會大驚失色,感受是深不可測的東西,一方面是 makefile
的編寫很難,一方面是 C/C++
相比 Java
來講,比較晦澀。bash
可是沒必要擔憂,一是有了 CMake
,二是對於 C/C++
的基本使用其實和 Java
差很少,本系列涉及到的,也都是對 C/C++
的基礎使用,畢竟,高級的我也不會不是嗎?哈哈哈~~架構
首先,須要下載 CMake
相關工具,在 Android Studio
中依次點擊 Tools->SDK Manager->SDK Tools
,而後勾選app
CMake
: CMake 構建工具框架
LLDB
: C/C++ 代碼調試工具ide
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 <jni.h>
#include <string>
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 <jni.h>
#include <string>
#include <unistd.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavcodec/jni.h>
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);
} else {
sprintf(info, "%sencode:", 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[%s]\n", info, c_temp->name);
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
和頭文件
的路徑是否正確。