## 音視頻學習 (四) 交叉編譯動態庫、靜態庫的入門學習
## 前言
該篇文章主要介紹 Android 端利用 NDK 工具庫來對 C/C++ 進行交叉編譯,并通過 makefile 和 cmake 來構建 Android 項目。
## 編譯器
了解 c/c++ 編譯器的基本使用,能夠在后續移植第三方框架進行交叉編譯時,清楚的了解應該傳遞什么參數。
**1\. clang**
clang 是一個`C、C++、Object-C`的輕量級編譯器。基于`LLVM`( LLVM是以C++編寫而成的構架編譯器的框架系統,可以說是一個用于開發編譯器相關的庫),對比 gcc,它具有編譯速度更快、編譯產出更小等優點,但是某些軟件在使用 clang 編譯時候因為源碼中內容的問題會出現錯誤。
**2\. gcc**
GNU C 編譯器。原本只能處理 C 語言,但是它很快擴展,變得可處理 C++。( GNU目標是創建一套完全自由的操作系統)。
**3\. g++**
GNU c++ 編譯器,后綴為 .c 的源文件,gcc 把它當作是 C 程序,而 g++ 當作是 C++ 程序;后綴為 .cpp 的,兩者都會認為是 c++ 程序,g++ 會自動鏈接 c++ 標準庫 stl ,gcc 不會,gcc 不會定義 \_\_cplusplus 宏,而 g++ 會。
## 編譯原理
一個 C/C++ 文件要經過預處理(preprocessing)、編譯(compilation)、匯編(assembly)、和鏈接(linking)才能變成可執行文件。
我們先在 linux 系統上創建一個 test.c 文件,編寫一個最簡單的 c 程序,代碼如下:
~~~
#include<stdio.h>
int main(){
printf(" 執行成功 ! \n");
return 19921001;
}
復制代碼
~~~
1. 預處理階段
預處理階段主要處理 include 和 define 等。它把 #include 包含進來的 .h 文件插入到 #include 所在的位置,把源程序中使用到的用 #define 定義的宏用實際的字符串代替。
我們可以通過以下命令來對 c/c++ 文件預處理,命令如下:
~~~
gcc -E test.c -o test.i //-E 的作用是讓 gcc 在預處理結束后停止編譯
復制代碼
~~~

可以看到輸入該命令之后就會生成一個 test.i 文件。
2. 編譯階段
在這個階段中,gcc 首先要檢查代碼的規范性、是否有語法錯誤等,以確定代碼的實際要做的工作。
我們可以通過如下命令來處理 test.i 文件,編譯成匯編文件,命令如下:
~~~
gcc -S test.i -o test.s//-S 的作用是編譯結束生成匯編文件。
復制代碼
~~~

3. 匯編階段
匯編階段把 .S 文件翻譯成二進制機器指令文件 .o ,這個階段接收.c ,.i ,.s 的文件都沒有問題。
下面我們通過以下命令生成二進制機器指令文件 .o 文件:
~~~
gcc -c test.s -o test.o
復制代碼
~~~

4. 鏈接階段
鏈接階段,鏈接的是函數庫。可以通過以下命令實現:
~~~
gcc -C test.o -o test
./test
復制代碼
~~~

最后我們通過實際操作,對編譯有了一定的了解,當然你也可以直接通過如下命令一步到位:
~~~
gcc test.c -o test
復制代碼
~~~
到這里我們成功的在 linux 平臺生成了可執行文件,試想一下我們可以將這個可執行文件拷貝到安卓手機上執行嗎?我們也不猜想了,實際測試下就行,我們把 test 可執行文件 push 到手機 /data/local/tmp 里面, 如下所示:

可以看到 test 在手機 /data/local/tmp 的路徑下是有可讀可寫可執行的權限,但是最后執行不成功,這是為什么呢? 其實**主要原因是兩個平臺的 CPU 指令集不一樣,根本就無法識別指令**。那么怎么解決這個問題呢? 下面就要用到今天一個比較重要的知識點了,**利用 Android NDK 工具包來對 C/C++ 代碼進行交叉編譯**。
## 交叉編譯
簡單地來說,交叉編譯就是程序的編譯環境和實際運行環境不一致,即在一個平臺上生成另一個平臺上的可執行代碼。
在音視頻開發中了解交叉編譯是很有必要的,因為無論在哪一種移動平臺下開發,第三方庫都是需要進行交叉編譯的。下面我們就以之前的例子來講解如何在 linux 環境下交叉編譯出移動平臺上的可執行代碼。
### 了解 NDK
Android 原生開發包 (NDK) 可用于 Android 平臺上的 C++ 開發,NDK 不僅僅是一個單一功能的工具,還是一個包含了 API 、交叉編譯器、調試器、構建工具等得綜合工具集。
下面大致列舉了一下經常會用到的組件。
* ARM 交叉編譯器
* 構建工具
* Java 原生接口頭文件
* C 庫
* Math 庫
* 最小的 C++ 庫
* ZLib 壓縮庫
* POSIX 線程
* Android 日志庫
* Android 原生應用 API
* OpenGL ES 庫
* OpenSL ES 庫
下面來看一下 Android 所提供的 NDK 跟目錄下的結構。
* ndk-build: 該 Shell 腳本是 Android NDK 構建系統的起始點,一般在項目中僅僅執行這一個命令就可以編譯出對應的動態鏈接庫了。
* ndk-gdb: 該 Shell 腳本允許用 GUN 調試器調試 Native 代碼,并且可以配置到 AS 中,可以做到像調試 Java 代碼一樣調試 Native 代碼。
* ndk-stack: 該 Shell 腳本可以幫組分析 Native 代碼崩潰時的堆棧信息。
* build: 該目錄包含 NDK 構建系統的所有模塊。
* platforms: 該目錄包含支持不同 Android 目標版本的頭文件和庫文件, NDK 構建系統會根據具體的配置來引用指定平臺下的頭文件和庫文件。
* toolchains: 該目錄包含目前 NDK 所支持的不同平臺下的交叉編譯器 - ARM 、X86、MIPS ,目前比較常用的是 ARM 。構建系統會根據具體的配置選擇不同的交叉編譯器。
下面我們就來為交叉編譯的環境變量配置
### 環境變量配置
* ndk 在 Linux 上的環境變量配置:
~~~
//1. vim /etc/profile
#NDK環境變量
export NDK_HOME=/root/android/ndk/android-ndk-r17c
export PATH=$PATH:$NDK_HOME
//2. 保存
source /etc/profile
//3. 測試
ndk-build -v
復制代碼
~~~
如果出現如下字樣,就證明配置成功了。

* 交叉編譯在 Linux 上的環境變量配置(做一個參考,采坑之后的環境配置):
~~~
export NDK_GCC_x86="/root/android/ndk/android-ndk-r17c/toolchains/x86-4.9/prebuilt/linux-x86_64/bin/i686-linux-android-gcc"
export NDK_GCC_x64="/root/android/ndk/android-ndk-r17c/toolchains/x86_64-4.9/prebuilt/linux-x86_64/bin/x86_64-linux-android-gcc"
export NDK_GCC_arm="/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc"
export NDK_GCC_arm_64="/root/android/ndk/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-gcc"
export NDK_CFIG_x86="--sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-x86 -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/i686-linux-android"
export NDK_CFIG_x64="--sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-x86_64 -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/x86_64-linux-android"
export NDK_CFIG_arm="--sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/arm-linux-androideabi"
export NDK_CFIG_arm_64="--isysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm64 -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/aarch64-linux-android"
export NDK_AR_x86="/root/android/ndk/android-ndk-r17c/toolchains/x86-4.9/prebuilt/linux-x86_64/bin/i686-linux-android-ar"
export NDK_AR_x64="/root/android/ndk/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"
export NDK_AR_arm="/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-ar"
export NDK_AR_arm_64="/root/android/ndk/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-ar"
復制代碼
~~~
你可以根據自己的 ndk 路徑對應我的環境變量來進行配置。下面我們就用 ndk gcc 來對 test.c 進行交叉編譯,步驟如下:
1. 首先找到 /root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86\_64/bin/arm-linux-androideabi-gcc

執行如下命令:
~~~
/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc -o test test.c
復制代碼
~~~

這種錯誤是說在我們編得時候編譯器找不到我們引入的 stdio.h 頭文件,那怎么告訴編譯器 stdio.h 頭文件在哪里呢? 下面知識點說明怎么指定這些報錯的頭文件
2. 指定頭文件代碼
~~~
/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -pie -o test test.c
復制代碼
~~~
上面出現了幾個命令符號,不了解了可以看一下如下解釋:
**\--sysroot=?:**使用 ?作為這一次編譯的頭文件與庫文件的查找目錄,查找下面的 usr/include 目錄。
**\-isystem ?(主要中間有一個英文空格)**: 使用頭文件查找目錄,覆蓋 --sysroot, 查找 ?/usr/include 目錄下面的頭文件。
**\-isystem ?**(主要中間有一個英文空格): \*\* 指定頭文件的查找路徑。
**\-I?:**頭文件的查找目錄,I 是大寫。
這樣編譯之后還是會報一個 asm/types.h 文件找不到,我們還要繼續修改一下路徑,如下
~~~
/root/android/ndk/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=/root/android/ndk/android-ndk-r17c/platforms/android-21/arch-arm -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include -isystem /root/android/ndk/android-ndk-r17c/sysroot/usr/include/arm-linux-androideabi -pie -o test test.c
復制代碼
~~~
這樣就能編譯成一個 Android 平臺可執行的文件了,這樣看起來路徑太多不易閱讀,大家可以參考我提供的全局變量配置來進行設置,最后一行命令解決,如下:
~~~
$NDK_GCC_arm $NDK_CFIG_arm -pie -o test test.c
復制代碼
~~~

可以看到,我們使用 Android NDK 編譯出來的可執行文件已經在 Linux 平臺下不可執行了。下面我們將 test 文件導入到 手機 /data/local/tmp 目錄。
3. 將 NDK 交叉編譯出來的 test 可執行文件,導入 Android 手機中并執行 test 文件。

根據上面的錄屏,我們知道已經成功的在 Android 設備下執行了 NDK 交叉編譯后的 test 文件了。
下面我們利用 NDK 工具交叉編譯 test.c 輸出靜態動態庫。
## 動態庫 & 靜態庫
### 編譯靜態庫
1. 將 test.c 使用 NDK GCC 編譯為 .o 文件 ,命令如下:
~~~
$NDK_GCC_arm $NDK_CFIG_arm -fpic -c test.c -o test.o
復制代碼
~~~
如果出現如下文件,證明已經成功了。

2. 使用 NDK arm-linux-androideabi-ar 工具將 test.o 文件生成 test.a 靜態庫,命令如下:
~~~
$NDK_AR_arm r test.a test.o
復制代碼
~~~

之后我們把 test.a 文件導入到 AS 中,來對 .a 的使用。
### 編譯動態庫
在編譯動態庫的時候我們需要指定 -fPIC -shared 額外參數給編譯器,完整命令如下:
~~~
$NDK_GCC_arm $NDK_CFIG_arm -fpic -shared test.c -o libTest.so
復制代碼
~~~

### 動態庫與靜態庫的區別
在平時工作中我們經常把一些常用的函數或者功能封裝為一個個庫供給別人使用,java開發我們可以封裝為 ja r包提供給別人用,安卓平臺后來可以打包成 aar 包,同樣的,C/C++ 中我們封裝的功能或者函數可以通過靜態庫或者動態庫的方式提供給別人使用。
Linux 平臺靜態庫以 .a 結尾,而動態庫以 .so 結尾。
那靜態庫與動態庫有什么區別呢?
**1\. 靜態庫**
與靜態庫連接時,靜態庫中所有被使用的函數的機器碼在**編譯**的時候都被拷貝到最終的可執行文件中,并且會被添加到和它連接的每個程序中:
**優點**:運行起來會快一些,不用查找其余文件的函數庫了。
**缺點**:導致最終生成的可執行代碼量相對變多,運行時, 都會被加載到內存中. 又多消耗了內存空間。
**2\. 動態庫**
與動態庫連接的可執行文件只包含需要的函數的引用表,而不是所有的函數代碼,只有在程序**執行**時, 那些需要的函數代碼才被拷貝到內存中。
優點:生成可執行文件比較小, 節省磁盤空間,一份動態庫駐留在內存中被多個程序使用,也同時節約了內存。
缺點:由于運行時要去鏈接庫會花費一定的時間,執行速度相對會慢一些。
**靜態庫是時間換空間,動態庫是空間換時間,二者均有好壞。**
如果我們要修改函數庫,使用動態庫的程序只需要將動態庫重新編譯就可以了,而使用靜態庫的程序則需要將靜態庫重新編譯好后,將程序再重新編譯一遍。
## mk & cmake
上一小節我們通過 NDK 交叉編譯了 test.c 為動態靜態庫,那么該小節我們就基于 makefile 和 cmake 來構建一個 C/C++ 的 Android 程序, 并使用 test .a /libTest.so
### mk
Android.mk 是在 Android 平臺上構建一個 C 或者 C ++ 語言編寫的程序系統的 Makefile 文件,不同的是, Android 提供了一些列內置變量來提供更加方便的構建語法規則。Application.mk 文件實際上是對應用程序本身進行描述的文件,它描述了應用程序要針對哪些 CPU 架構打包動態 so 包、要構建的是 release 包還是 debug 包以及一些編譯和鏈接參數等。
#### 語法基礎
**1\. Android.mk**
* LOCAL\_PATH :=$(call my-dir)
返回當前文件在系統中路徑,Android.mk 文件開始時必須定義該變量。
* include $(CLEAR\_VARS), 表明清楚上一次構建過程的所有全局變量,因為在一個 Makefile 編譯腳本中,會使用大量的全局變量,使用這行腳本表明需要清除掉所有的全局變量。
* LOCAL\_SRC\_FILES, 要編譯的 C 或者 CPP 的文件,注意這里不需要列舉頭文件,構建系統會自動幫組開發者依賴這些文件。
* LOCAL\_LDLIBS:= -L$定編譯過程所依賴的提供的動態靜態庫,變量代表的是下面的目錄(SYSROOT)/usr/lib -Ilog -IOpenSLES -IGLESv2 -IEGL -Iz,定編譯過程所依賴的 NDK 提供的動態靜態庫, SYSROOT 變量代表的是 NDK\_ROOT 下面的目錄 $NDK 提供的動態與靜態庫,SYSROOT 變量代表的是 NDK\_ROOT 下面目錄 $NDK\_ROOT/platforms/android-21/arch-arm, 而在這個目錄的 usr/lib/ 目錄下有很多對應的 so 的動態庫以及 .a 的靜態庫。
* LOCAL\_CFLAGS , 編譯 C 或者 CPP 的編譯標志,在實際編譯的時候會發送給編譯器。比如常用的實例是加上 -DAUTO\_TEST , 然后在代碼中就可以利用條件判斷 #ifdef AUTO\_TEST 來做一些與自動化測試相關的事情。
* LOCAL\_LDFLAGS, 鏈接標志的可選列表,當對目標文件進行鏈接以生成輸出文件的時候,將這些標志帶給鏈接器。該指令與 LOCAL\_LDLIBS 有些類似,一般情況下,該選項會用于指定第三方編譯的靜態庫,LOCAL\_LDLIBS 經常用于指定系統的庫(比如 log、OpenGLES、EGL 等)。
* LOCAL\_MODULE, 該模塊的編譯的目標名,用于區分各個模塊,名字必須是唯一并不包含空格的,如果編譯目標是 so 庫,那么該 so 庫的名稱就是 lib 項目名 .so。
* include $(BUILD\_SHARED\_LIBRARY) ,其實類似的 include 還有很多,都是構建系統提供的內置變量,該變量的意義是構建動態庫,其他的內置變量還包括如下幾種。
* \---BUILD\_STATIC\_LIBRARY: 構建靜態庫
* \---PREBUILT\_STATIC\_LIBRARY: 對已有的靜態庫進行包裝,使其成為一個模塊。
* \---PREBUILT\_SHARED\_LIBRARY: 對已有的靜態庫進行包裝,使其成為一個模塊。
* \---BUILD\_EXECUTABLE: 構建可執行文件。
**2\. Application.mk**
* APP\_ABI := XXX ,這里的 XXX 是指不同平臺,可以選填的有 x86 、mips 、armeabi、armeabi-v7a、all 等,值得一提的是,若選擇 all 則會構建構建出所有平臺的 so ,如果不填寫該項,那么將默認構建為 armeabi 平臺下的庫。
* APP\_STL := gnustl\_static ,NDK 構建系統提供了由 Android 系統給出的最小 C++ 運行時庫 (、system/lib/libstdc++.so)的 C++ 頭文件。
* APP\_CPPFLAGS :=-std=gnu++11 -fexceptions, 指定編譯過程的 flag ,可以在該選項中開啟 exception rtti 等特性,但是為了效率考慮,最好關閉 rtti。
* NDK\_TOOLCHAIN\_VERSION = 4.8,指定交叉工具編譯鏈里面的版本號,這里指定使用 4.8。
* APP\_PLATFORM :=android-9,指定創建的動態庫的平臺
* APP\_OPTIM := release,該變量是可選的,用來定義 “release” 或者 “debug” ,"release" 模式是默認的,并且會生成高度優化的二進制代碼;“debug” 模式生成的是未優化的二進制代碼,但是可以檢測出很多的 BUG, 經常用于調試階段,也相當于在 ndk-build 指令后邊直接加上參數 NDK\_DEBUG=1。
#### 構建 C/C++ Android 項目
[項目源代碼](https://github.com/yangkun19921001/NDK_AV_SAMPLE/tree/master/mk_application)
**效果:**

Makefile 的方式我們只做一個了解,因為以后我們構建 C/C++ 的 Android 項目都是用 cmake 方式來構建,所以我們重點掌握 cmake 就行。
### cmake
之前做 NDK 開發或者老的項目都是基于 Android.mk、Application.mk 來構建項目的,但從 AS 2.2 之后便開始采用 CMake 的方式來構建 C/C++ 項目,采用 CMake 相比與之前的 Android.mk、Application.mk 方便簡單了許多。下面我們簡單的來介紹下 cmake 基礎語法吧。
#### 語法基礎
~~~
#1. 指定 cmake 的最小版本
cmake_minimum_required(VERSION 3.4.1)
#2. 設置項目名稱
project(demo)
#3. 設置編譯類型
add_executable(demo test.cpp) # 生成可執行文件
add_library(common STATIC test.cpp) # 生成靜態庫
add_library(common SHARED test.cpp) # 生成動態庫或共享庫
#4. 明確指定包含哪些源文件
add_library(demo test.cpp test1.cpp test2.cpp)
#5. 自定義搜索規則并加載文件
file(GLOB SRC_LIST "*.cpp" "protocol/*.cpp")
add_library(demo ${SRC_LIST}) //加載當前目錄下所有的 cpp 文件
## 或者
file(GLOB SRC_LIST "*.cpp")
file(GLOB SRC_PROTOCOL_LIST "protocol/*.cpp")
add_library(demo ${SRC_LIST} ${SRC_PROTOCOL_LIST})
## 或者
aux_source_directory(. SRC_LIST)//搜索當前目錄下的所有.cpp文件
aux_source_directory(protocol SRC_PROTOCOL_LIST)
add_library(demo ${SRC_LIST} ${SRC_PROTOCOL_LIST})
#6. 查找指定庫文件
find_library(
log-lib //為 log 定義一個變量名稱
log ) //ndk 下的 log 庫
#7. 設置包含的目錄
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
)
#8. 設置鏈接庫搜索目錄
link_directories(
${CMAKE_CURRENT_SOURCE_DIR}/libs
)
#9. 設置 target 需要鏈接的庫
target_link_libraries( # 目標庫
demo
# 目標庫需要鏈接的庫
# log-lib 是上面 find_library 指定的變量名
${log-lib} )
#10. 指定鏈接動態庫或者靜態庫
target_link_libraries(demo libtest.a) # 鏈接libtest.a
target_link_libraries(demo libtest.so) # 鏈接libtest.so
#11. 根據全路徑鏈接動態靜態庫
target_link_libraries(demo ${CMAKE_CURRENT_SOURCE_DIR}/libs/libtest.a)
target_link_libraries(demo ${CMAKE_CURRENT_SOURCE_DIR}/libs/libtest.so)
#12. 指定鏈接多個庫
target_link_libraries(demo
${CMAKE_CURRENT_SOURCE_DIR}/libs/libtest.a
test.a
boost_thread
pthread)
復制代碼
~~~
#### 常用變量
| 預定義變量 | 說明 |
| --- | --- |
| **PROJECT\_SOURCE\_DIR** | 工程的根目錄 |
| **PROJECT\_BINARY\_DIR** | 運行 cmake 命令的目錄,通常是 ${PROJECT\_SOURCE\_DIR}/build |
| **PROJECT\_NAME** | 返回通過 project 命令定義的項目名稱 |
| **CMAKE\_CURRENT\_SOURCE\_DIR** | 當前處理的 CMakeLists.txt 所在的路徑 |
| **CMAKE\_CURRENT\_BINARY\_DIR** | target 編譯目錄 |
| **CMAKE\_CURRENT\_LIST\_DIR** | CMakeLists.txt 的完整路徑 |
| **CMAKE\_CURRENT\_LIST\_LINE** | 當前所在的行 |
| **CMAKE\_MODULE\_PATH** | 定義自己的 cmake 模塊所在的路徑,SET(CMAKE\_MODULE\_PATH ${PROJECT\_SOURCE\_DIR}/cmake),然后可以用INCLUDE命令來調用自己的模塊 |
| **EXECUTABLE\_OUTPUT\_PATH** | 重新定義目標二進制可執行文件的存放位置 |
| **LIBRARY\_OUTPUT\_PATH** | 重新定義目標鏈接庫文件的存放位置 |
#### 構建 C/C++ Android 項目
1. 以靜態庫構建項目
* 定義 native 接口
~~~
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testCmake();
}
/**
* 測試 cmake 構建程序
*/
public native static void testCmake();
}
復制代碼
~~~
* 編寫 cpp
~~~
// extern int main(); 這樣寫有坑,因為 main 方法是屬于 c 的,而當前是 CPP
extern "C" { //必須這樣定義
int main();
}
extern "C" JNIEXPORT void JNICALL
Java_com_devyk_cmake_1application_MainActivity_testCmake(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
__android_log_print(ANDROID_LOG_DEBUG, "DevYK", "main--->:%d", main());
}
復制代碼
~~~
* 編寫 CmakeLists.txt 文件
~~~
cmake_minimum_required(VERSION 3.4.1)
# 打印日志
message("當前CMake的路徑是:${CMAKE_SOURCE_DIR}")
message("當前 CMAKE_ANDROID_ARCH_ABI 的路徑是:${CMAKE_ANDROID_ARCH_ABI}")
# 批量引入源文件
file(GLOB allCpp *.cpp)
# 加入cpp源文件
add_library(
native-lib
SHARED
# native-lib.cpp 替換 ${allCpp} 批量導入文件
${allCpp}
)
# 導入靜態庫
add_library(test_a STATIC IMPORTED)
# 開始真正的導入
set_target_properties(test_a PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libtest.a)
# 只能找系統的
find_library(
log-lib
log)
message("當前的log路徑在哪里啊 >>>>>>>>>>>>>>>>> ${log-lib}")
#開始鏈接指定的庫
target_link_libraries(
native-lib
${log-lib}
test_a
)
復制代碼
~~~
* app/build.gradle cmake 配置
~~~
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
// cppFlags "" // 默認包含四大平臺
abiFilters 'armeabi-v7a'//編譯armeabi-v7a平臺
}
}
ndk {
//過濾,只使用這個版本的庫,否則默認的可是4個平臺
abiFilters 'armeabi-v7a'
}
}
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt" //指定 CMakeLists 路徑
}
}
}
復制代碼
~~~
* 測試結果

2. 以動態庫構建項目
* 代碼加載 so 庫到手機中
~~~
static {
System.loadLibrary("Test");
System.loadLibrary("native-lib");
}
復制代碼
~~~
* so 庫導入在 main/jniLibs 下

* CmakeLists.txt 配置
~~~
cmake_minimum_required(VERSION 3.4.1)
# 打印日志
message("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>>>")
message("當前CMake的路徑是:${CMAKE_SOURCE_DIR}")
message("當前 CMAKE_ANDROID_ARCH_ABI 的路徑是:${CMAKE_ANDROID_ARCH_ABI}")
# 批量引入源文件
file(GLOB allCpp *.cpp)
# 加入cpp源文件
add_library(
native-lib
SHARED
# native-lib.cpp
${allCpp}
)
# 導入靜態庫
#add_library(test_a STATIC IMPORTED)
# 開始真正的導入
#set_target_properties(test_a PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libtest.a)
# 導入動態庫
add_library(test_so SHARED IMPORTED)
# 早起的cmake ANDROID_ABI == 當前CPU平臺
set_target_properties(test_so PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libTest.so)
# 只能找系統的
find_library(
log-lib
log)
message("當前的log路徑在哪里啊 >>>>>>>>>>>>>>>>> ${log-lib}")
# CMAKE_SOURCE_DIR == D:\NDK\CoursewareCreate\ndk_12\project\ndk12_cmake\app\src\main\cpp\CMakeLists.txt
target_link_libraries(
native-lib
${log-lib}
test_so
)
復制代碼
~~~
* 測試

到這里,mk 和 cmake 入門基礎知識就講完了,想要全部掌握還需要自己多動手實踐一翻。
## 總結
該篇文章主要講解了如何利用 NDK 對 C 程序進行交叉編譯,以及交叉編譯后的動態靜態庫在 Android 項目中的使用,還有 makefile 和 cmake 在 Android 的使用,本篇文章也是比較基礎的,對于后續使用或者編譯 FFmpeg 打下基礎。
[文章中所有代碼已上傳 GitHub 倉庫](https://github.com/yangkun19921001/NDK_AV_SAMPLE)
- 前言
- JNI基礎知識
- C語言知識點總結
- ①基本語法
- ②數據類型
- 枚舉類型
- 自定義類型(類型定義)
- ③格式化輸入輸出
- printf函數
- scanf函數
- 編程規范
- ④變量和常量
- 局部變量和外部變量
- ⑤類型轉換
- ⑥運算符
- ⑦結構語句
- 1、分支結構(選擇語句)
- 2、循環結構
- 退出循環
- break語句
- continue語句
- goto語句
- ⑧函數
- 函數的定義和調用
- 參數
- 函數的返回值
- 遞歸函數
- 零起點學通C語言摘要
- 內部函數和外部函數
- 變量存儲類別
- ⑨數組
- 指針
- 結構體
- 聯合體(共用體)
- 預處理器
- 預處理器的工作原理
- 預處理指令
- 宏定義
- 簡單的宏
- 帶參數的宏
- 預定義宏
- 文件包含
- 條件編譯
- 內存中的數據
- C語言讀文件和寫文件
- JNI知識點總結
- 前情回顧
- JNI規范
- jni開發
- jni開發中常見的錯誤
- JNI實戰演練
- C++(CPP)在Android開發中的應用
- 掘金網友總結的音視頻開發知識
- 音視頻學習一、C 語言入門
- 1.程序結構
- 2. 基本語法
- 3. 數據類型
- 4. 變量
- 5. 常量
- 6. 存儲類型關鍵字
- 7. 運算符
- 8. 判斷
- 9. 循環
- 10. 函數
- 11. 作用域規則
- 12. 數組
- 13. 枚舉
- 14. 指針
- 15. 函數指針與回調函數
- 16. 字符串
- 17. 結構體
- 18. 共用體
- 19. typedef
- 20. 輸入 & 輸出
- 21.文件讀寫
- 22. 預處理器
- 23.頭文件
- 24. 強制類型轉換
- 25. 錯誤處理
- 26. 遞歸
- 27. 可變參數
- 28. 內存管理
- 29. 命令行參數
- 總結
- 音視頻學習二 、C++ 語言入門
- 1. 基本語法
- 2. C++ 關鍵字
- 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. 多線程
- 總結
- 音視頻學習 (三) JNI 從入門到掌握
- 音視頻學習 (四) 交叉編譯動態庫、靜態庫的入門學習
- 音視頻學習 (五) Shell 腳本入門
- 音視頻學習 (六) 一鍵編譯 32/64 位 FFmpeg 4.2.2
- 音視頻學習 (七) 掌握音頻基礎知識并使用 AudioTrack、OpenSL ES 渲染 PCM 數據
- 音視頻學習 (八) 掌握視頻基礎知識并使用 OpenGL ES 2.0 渲染 YUV 數據
- 音視頻學習 (九) 從 0 ~ 1 開發一款 Android 端播放器(支持多協議網絡拉流/本地文件)
- 音視頻學習 (十) 基于 Nginx 搭建(rtmp、http)直播服務器
- 音視頻學習 (十一) Android 端實現 rtmp 推流
- 音視頻學習 (十二) 基于 FFmpeg + OpenSLES 實現音頻萬能播放器
- 音視頻學習 (十三) Android 中通過 FFmpeg 命令對音視頻編輯處理(已開源)