前言
在日常App開發中,難免有些功能是需要藉助NDK來完成的,比如現在常見的音視訊處理等,今天就以ffmpeg入手,來學習下Android NDK開發的套路.
JNI和NDK
很多人並不清除JNI和NDK的概念,經常搞混這兩樣東西,先來看看它們各自的定義吧.
JNI
-
是什麼
JNI
是Java Native Interface
的縮寫,它提供了若干的API實現了Java和其他語言的通訊(主要是C&C++). -
設計目的
標準的java類庫不支援你的程式所需的特性。或者你已經有了一個用其他語言寫成的庫或程式,而你希望在java程式中使用它。或者是你需要一個高效能的庫來完成一些操作. 複製程式碼
-
使用步驟
- 編寫帶有native宣告的方法的java類
- 使用javac命令編譯所編寫的java類
- 然後使用javah + java類名生成副檔名為h的標頭檔案
- 使用C/C++實現本地方法
- 將C/C++編寫的檔案生成動態連線庫*(在Android中就是.so庫)
- java程式碼中呼叫native方法
NDK
NDK
全稱Native Development Kit
,是Android的一個開發工具包,與Java並沒有什麼關係.
NDK
的核心目的之一是讓您將 C 和 C++ 原始碼構建為可用於應用的共享庫。嗯,就是它提供了交叉編譯的功能.
CPU架構
我們都知道 CPU 是什麼,那 CPU 架構到底是什麼呢?迴歸到“架構”這個詞本身含義,CPU 架構就是 CPU 的框架結構、設計方案,處理器廠商以某種架構為基礎,生產自己的 CPU,就好比“總-分-總”是文章的一種架構,多篇文章可以都基於“總-分-總”架構。
常見的 CPU 架構有 x86、x86-64 以及 arm 等, x86-64 其實也是基於 x86 架構,只是在 x86 的基礎上做了一些擴充套件,以支援 64 位程式的應用,常見的 Intel 、AMD 處理器都是基於 x86 架構的。
而 x86 架構主打的是 pc 端,對於移動端,arm 架構處於霸主地位 ,由於其體積小、低功耗、低成本、高效能的優點,被廣泛應用在嵌入式系統中,目前大多數安卓、蘋果手機的 CPU 都基於 arm 架構,此處所說的 arm 架構指 arm 系列架構,其中包括 ARMv5 、ARMv7 等等。
最後再看 Android 端 , Android 系統目前支援 ARMv5、ARMv7、ARMv8、 x86 、x86_64、MIPS 以及 MIPS64 共七種 CPU 架構,也就是說除此之外其他 CPU 架構的硬體並不能執行 Android 系統。
交叉編譯
在某個平臺上,編譯該平臺的可執行程式,叫做本地編譯,比如在 Windows 平臺上編譯 Windows 自身的可執行程式;在 x86 平臺上,編譯 x86 平臺自身的可執行程式。
在某個平臺上,編譯另一種平臺的可執行程式,就是交叉編譯,比如在 x86 平臺上,編譯 arm 平臺的可執行程式,這也是 Android 端使用最多的交叉編譯型別。
在交叉編譯時,由於主機與目標的體系架構、環境不同,所以交叉編譯比本地編譯複雜很多,需要一些工具來解決主機與目標不同特性的問題,這些工具構成的工具集就叫做交叉編譯鏈。
既然交叉編譯比本地複雜很多,那為什麼不使用本地編譯,比如在 arm 平臺編譯 arm 平臺的可執行程式呢?這是因為目標平臺儲存空間和計算能力通常是有限的,而編譯過程需要較大的儲存空間和較快的計算能力,但目標平臺無法提供。
專案中使用NDK
這裡可以檢視一篇官方文件,中文,寫的很詳細:向您的專案新增C和C++ 程式碼,強烈建議認真閱讀下這部分文件
CMake
NDK的構建有兩種方式,一種是早期使用的ndk-build
,一種是在Android Studio2.2之後推薦使用的cmake
,我們今天只說推薦的cmake
這種方式.
CMakeLists.txt的寫法
-
add_library 使用指定的原始檔將庫新增到專案中
-
普通庫
// 新增普通庫的語法 add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [source1] [source2 ...]) // 建立ndk專案中預設生成的例子 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). src/main/cpp/native-lib.cpp ) 複製程式碼
name
屬性沒什麼好說的,注意全域性唯一就好.[STATIC | SHARED | MODULE]
的話是生成的庫的型別,STATIC
的話生成的是靜態庫,也就是.a
字尾的.我們一般用的都是SHARED
生成動態連結庫,也就是.so
字尾的. -
匯入庫
// 語法 add_library(<name> <SHARED|STATIC|MODULE|OBJECT|UNKNOWN> IMPORTED [GLOBAL]) // 匯入編譯好的ffmpeg樣例 add_library( ffmpeg SHARED IMPORTED ) // 設定需要匯入的ffmpeg位置 set_target_properties( ffmpeg PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libffmpeg.so ) 複製程式碼
這種方式可以把我們在外部編譯好的.so庫導進來
還有幾種我也沒用過了,可以參考官方文件看下add_library
-
-
include_directories 用來匯入相關標頭檔案
include_directories(src/main/cpp) 複製程式碼
-
find_library 用來引入NDK中提供的庫. Android NDK 原生 API
find_library( # 定義儲存NDK庫位置的路徑變數的名稱。 log-lib # 指定CMake要查詢的NDK庫的名稱。 log ) 複製程式碼
-
target_link_libraries 將匯入的庫和自己的原生庫關聯起來
target_link_libraries( # 指定目標庫。 native-lib # 將目標庫連結到NDK中包含的日誌庫。 ${log-lib} ) 複製程式碼
FFmpeg
FFmpeg
是一套可以用來記錄、處理數字音訊、視訊,並將其轉換為流的開源框架,採用LPL或GPL許可證,提供了錄製、轉換以及流化音視訊的完整解決方案。名稱中的mpeg來自視訊編碼標準mpeg
,而字首FF
是Fast Forward
的首字母縮寫.音視訊處理的開源庫,可以完成絕大多數音視訊相關的功能.很多知名軟體,開源庫都是基於它進行的二次開發,比如bilibi的ijkPlayer
.
編譯FFmpeg
FFmpeg
與大部分GNU軟體的編譯方式類似,都是通過configure
指令碼來實現編譯前的定製,這種方式允許使用者在編譯前對軟體進行裁剪,同時通過對最終執行到的系統及目標平臺的配置來決定對某些模組設定合適的配置.所以這裡是通過configure
的方式來生成Makefile
檔案,然後使用make
和make install
編譯和安裝.
-
配置環境
首先我們需要先準備相關的編譯環境,這裡推薦在
linux
下進行編譯,配置簡單問題少.當然Mac
也行,不推薦Windows
.- Linux環境(Ubuntu 16.04)
Windows
的話下載個VMware Workstation
,裝個ubuntu
還是方便的. - NDK環境 這裡使用的是ndk-r17,附上相關下載連結NDK 下載
- 下載FFmpeg原始碼 FFmpeg下載地址
- Linux環境(Ubuntu 16.04)
-
修改configure檔案
由於FFmpeg預設生成的庫檔案格式為libavcodec.so.xx.xx.x。其中的xx就是主副版本號,這種格式在Ubuntu下使用是沒有問題的,但是在Android下開發使用,並不把其作為有效的庫檔案。所以需要修改其他生成的檔名的格式。
通過修改configure檔案要實現,開啟configure,找到如下內容:
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)' 複製程式碼
修改為:
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"' SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)' SLIB_INSTALL_LINKS='$(SLIBNAME)' 複製程式碼
-
編寫指令碼檔案 在
FFmpeg
根目錄下建立build.sh
指令碼檔案,來更方便的配置configure
.如下:#!/bin/bash # 配置NDK路徑 NDK=/home/xinyang/develop/android-ndk-r17 # 指定了交叉編譯環境,使其在編譯過程中能夠引用到 NDK 提供的原生標頭和共享庫檔案 SYSROOT=$NDK/platforms/android-23/arch-arm/ TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64 # 宣告方法 function build_one { ./configure \ --prefix=$PREFIX \ # 設定輸出路徑 --enable-shared \ # 開啟動態庫輸出 --disable-static \ # 關閉靜態庫輸出 --disable-doc \ # 關閉不需要的功能 --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-avdevice \ --disable-symver \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ # 指定交叉編譯工具鏈 --target-os=linux \ # 目標系統 android基於linux 所以這裡指定為linux --arch=armeabi-v7a \ # 目標平臺架構 --enable-cross-compile \# 開啟交叉編譯 --sysroot=$SYSROOT \ # 交叉編譯環境 --extra-cflags="-Os -fpic $ADDI_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ $ADDITIONAL_CONFIGURE_FLAG make clean make make install } CPU=armeabi-v7a PREFIX=$(pwd)/android/$CPU ADDI_CFLAGS="-marm" build_one 複製程式碼
--cross-prefix
類似於萬用字元方式指定 bin 目錄下以 arm-linux-androideabi- 開頭的交叉編譯工具,假如不支援這種配置方式則需分別指定:
- CC:$TOOLCHAIN/bin/arm-linux-androideabi-gcc 編譯器,對C原始檔進行編譯處理,生成彙編檔案.
- CXX:$TOOLCHAIN/bin/arm-linux-androideabi-g++
- AR:$TOOLCHAIN/bin/arm-linux-androideabi-ar 打包器,用於庫操作,可以通過該工具從一個庫中刪除或者增加目的碼模組.
- LD:$TOOLCHAIN/bin/arm-linux-androideabi-ld 連結器,為前面生成的目的碼分配地址空間,將多個目標檔案連結成一個庫或是可執行檔案.
-
執行指令碼 cd 到ffmpeg目錄下
chmod 777 build.sh 複製程式碼
首先修改下指令碼檔案的可執行許可權
./build.sh 複製程式碼
然後執行指令碼,整個過程比較慢,耐心等待就好,整個過程大概需要5-10分鐘.編譯完成後就可以看到如下圖,其中
include
中是一些標頭檔案,lib
中就是生成的.so動態庫了
整合FFmpeg
到這裡就可以把生成的.so檔案整合到我們的專案中了,來看看步驟:
-
專案關聯NDK,按這裡的教程執行向您的專案新增C和C++ 程式碼;
-
拷貝生成的.so檔案到
libs
目錄下(或是jniLibs); -
拷貝生成的
include
資料夾到cpp
目錄; -
拷貝
ffmpeg\fftools
目錄下檔案到cpp目錄; -
編寫
native
方法package com.xinyang.ndkdemo; public class FFmpegCmd { static { System.loadLibrary("ffmpeg"); } public native static void handle(); } 複製程式碼
-
在
cpp
目錄下建立ffmpeg_cmd.c
檔案,實現native
方法,這裡可以採用javah
生成標頭檔案再實現的方式,也可以直接在java
類中使用快捷鍵提示,直接生成方法:#include <jni.h> #include <malloc.h> #include <string.h> #include <android/log.h> #include "ffmpeg/ffmpeg.h" JNIEXPORT void JNICALL Java_com_xinyang_ndkdemo_FFmpegCmd_handle (JNIEnv *env, jclass obj){ char info[40000] = {0}; av_register_all(); AVCodec *c_temp = av_codec_next(NULL); while(c_temp != NULL){ if(c_temp->decode!=NULL){ sprintf(info,"%s[Dec]",info); }else{ sprintf(info,"%s[Enc]",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); c_temp=c_temp->next; } __android_log_print(ANDROID_LOG_INFO,"myTag","info:\n%s",info); } 複製程式碼
這段程式用於輸出 FFmpeg 支援的編解碼資訊,通過 < android/log.h > 的 __android_log_print 方法可以直接將資訊輸出到 Android Studio 的 logcat 。
-
編輯
CMakeLists.txt
匯入相關.so檔案,使用add_library
匯入庫的方式把生成的.so檔案依次匯入,使用include_directories
匯入標頭檔案,最後再用target_link_libraries
把匯入的庫和生成的目標庫關聯起來,如下所示:# 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. 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. add_library( # Sets the name of the library. ffmpeg # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/ffmpeg_cmd.c src/main/cpp/ffmpeg/cmdutils.c src/main/cpp/ffmpeg/ffmpeg.c src/main/cpp/ffmpeg/ffmpeg_filter.c src/main/cpp/ffmpeg/ffmpeg_opt.c ) include_directories(src/main/cpp) include_directories(src/main/cpp/include) add_library( avutil-55 SHARED IMPORTED ) set_target_properties( avutil-55 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libavutil-55.so ) add_library( avcodec-57 SHARED IMPORTED ) set_target_properties( avcodec-57 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libavcodec-57.so ) add_library( avformat-57 SHARED IMPORTED ) set_target_properties( avformat-57 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libavformat-57.so ) add_library( avdevice-57 SHARED IMPORTED ) set_target_properties( avdevice-57 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libavdevice-57.so ) add_library( swresample-2 SHARED IMPORTED ) set_target_properties( swresample-2 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libswresample-2.so ) add_library( swscale-4 SHARED IMPORTED ) set_target_properties( swscale-4 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libswscale-4.so ) add_library( postproc-54 SHARED IMPORTED ) set_target_properties( postproc-54 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libpostproc-54.so ) add_library( avfilter-6 SHARED IMPORTED ) set_target_properties( avfilter-6 PROPERTIES IMPORTED_LOCATION ../../../../libs/armeabi-v7a/libavfilter-6.so ) # 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 ) # 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. ffmpeg avutil-55 avcodec-57 avformat-57 avdevice-57 swresample-2 swscale-4 postproc-54 avfilter-6 # Links the target library to the log library # included in the NDK. ${log-lib} ) 複製程式碼
-
試著呼叫
native
方法,在logcat
中檢視具體輸出資訊,如下:
總結
總的來說使用CMake方式還是比較簡單的,編寫CMakeLists.txt
檔案,在gradle中指定檔案位置就好.重點在於相關庫的交叉編譯及編寫呼叫相關api檔案的C檔案,這裡就需要一些C的基礎了.
參考