Android:JNI與NDK(二)交叉編譯與動態庫,靜態庫

WangLei_ClearHeart發表於2019-07-31

 

歡迎關注公眾號,第一時間獲取最新文章:

 

本篇目錄

一、前言

本篇主要以window開發環境為背景介紹一下NDK開發中需要掌握的交叉編譯等基礎知識,選window系統主要是照顧大多數讀者,mac ,linux作業系統基本是同樣適用的。

交叉編譯就是在A平臺編譯出可以在B平臺執行的檔案,對於我們安卓開發者來說交叉編譯就是在window或者mac或者linux系統上編譯出可在安卓系統上執行的可執行檔案,什麼時候需要用到交叉編譯呢?音視訊開發基本都會用到ffmpeg,opengl es等三方庫,這時我們就需要在window或者mac或者linux系統上編譯出可在安卓系統執行的檔案,這裡可編譯出靜態庫或者動態庫使用,這時候就會用到交叉編譯。

本篇雖然是一些基礎的知識或者操作,但是對於後續三方庫的編譯移植,CMake的配置是很重要的,否則後續遇到沒用過的三方庫你會感覺無從下手編譯,很多CMake的配置也只是會配置而不懂具體什麼含義。

進行本篇學習請先自己配置好MinGW(C/C++編譯器)編譯環境並配置到系統環境變數中,這些都是基礎的操作,自己查詢一下配置好就可以了,此外還需要自己下載好安卓平臺提供的交叉編譯工具鏈,下載地址:安卓平臺交叉編譯工具,我下載的是17c版本的。

好了,進入本文的學習

下文相關程式碼均來自:相關演示程式碼

二、常用C/C++編譯器瞭解以及C/C++檔案編譯過程

常用C/C++編譯器
編譯器名稱描述
clang clang 是一個C、C++、Object-C的輕量級編譯器。基於LLVM(LLVM是以C++編寫而成的構架編譯器的框架系統,可以說是一個用於開發編譯器相關的庫),對比gcc,它具有編譯速度更快、編譯產出更小等優點,但是某些軟體在使用clang編譯時候因為原始碼中內容的問題會出現錯誤
gcc GNU C編譯器。原本只能處理C語言,很快擴充套件,變得可處理C++。(GNU計劃,又稱革奴計劃。目標是建立一套完全自由的作業系統)
g++ GNU c++編譯器,字尾為.c的原始檔,gcc把它當作是C程式,而g++當作是C++程式;字尾為.cpp的,兩者都會認為是c++程式,g++會自動連結c++標準庫stl,gcc不會,gcc不會定義__cplusplus巨集,而g++會
C/C++檔案編譯過程

C/C++檔案要經過預處理、編譯、彙編、和連線才能變成可執行檔案。

過程名稱主要作用
預處理 預處理階段主要處理include和define等。它把#include包含進來的.h 檔案插入到#include所在的位置,把源程式中使用到的用#define定義的巨集用實際的字串代替
編譯 編譯階段,編譯器檢查程式碼的規範性、語法錯誤等,檢查無誤後,編譯器把程式碼翻譯成組合語言。
彙編 彙編階段把 .s檔案翻譯成二進位制機器指令檔案.o,這個階段接收.c, .i, .s的檔案都沒有問題
連線 連結階段,連結的是其餘的函式庫,比如我們自己編寫的c/c++檔案中用到了三方的函式庫,在連線階段就需要連線三方函式庫,如果連線不到就會報錯

比如在命令列中我們執行如下命令:

1 gcc -o d:\main C:\Users\wanglei55\Desktop\main.c

將C:\Users\wanglei55\Desktop\main.c檔案編譯為可執行檔案,輸出到d盤名稱為main,整個編譯過程就包括預處理、編譯、彙編、和連線過程。

以上主要介紹了常用C/C++編譯器的區別以及C/C++檔案的編譯過程,大體瞭解一下即可。

三、交叉編譯

接下來我們具體看一下交叉編譯的流程,我們先來看一下window平臺怎麼編譯出可執行檔案。

我們編寫如下C檔案:
main.c

1 #include <stdio.h>
2 int main()
3 {
4    int nn = 55;
5    printf("nn = %d\n", nn);
6    return 0;
7 }

很簡單,就是輸出一些資訊,接下來我們將main.c用gcc編譯器編譯為可執行檔案,執行如下命令:

1 gcc -o d:\main C:\Users\wanglei55\Desktop\main.c

這樣就會在d盤根目錄生成mian.exe檔案(window平臺下會加入副檔名.exe,mac/linux平臺下則不會)。

接下來我們就可以在命令列執行這個可執行檔案了:


到這裡我們成功的在window平臺生成了可執行檔案,試想一下我們可以將這個可執行檔案拷貝到安卓手機上執行嗎?估計很多同學及時沒試過也會覺得不會執行,但是為什麼呢?最簡單的說法就是安卓平臺不認識.exe結尾的可執行檔案,那如果我是在linux平臺編譯出來的呢?不就沒有.exe了嗎?及時在linux平臺編譯出來的拷貝到安卓平臺同樣是不能執行的,主要原因是兩個平臺的CPU指令集不一樣,根本就無法識別指令

那我們怎麼將main.c檔案編譯為可以在安卓平臺執行的檔案呢?這樣就用到交叉編譯了,這裡就是在window平臺編譯出可在安卓平臺執行的檔案,既然要編譯出在安卓平臺執行的檔案就需要用到目標平臺提供的編譯工具了,安卓提供的編譯工具上面已經給出了下載連結,我下載的是17c版本的:


下載對應平臺的zip包即可。

解壓後(我解壓到桌面上了)目錄下toolchains目錄就有對應平臺的編譯工具,安卓手機目前大部分cpu都是arm架構的了,我們以arm平臺為例:


對應目錄下就為我們提供了相應的gcc編譯器。

 

接下來我們就用安卓平臺提供的gcc編譯器來編譯main.c檔案,這裡要多說一下接下來的過程我會講的細一些,因為這裡很重要,很重要,很重要,我工作中接觸很多同事不明白編譯器的引數傳入方式有問題只能百度,即使問題解決了也不明白咋回事,其實很簡單,下面過程會講解到,好了,我們具體看一下吧編譯安卓平臺可執行檔案的過程吧:

首先cd到arm-linux-androideabi-gcc.exe所在目錄,執行如下命令:

1 arm-linux-androideabi-gcc.exe -o d:\main C:\Users\wanglei55\Desktop\main.c

執行命令會報如下錯誤:


這種錯誤是說在我們編譯的時候編譯器找不到我們引入的stdio.h標頭檔案,那怎麼告訴編譯器stdio.h標頭檔案在哪呢?

給編譯器指定標頭檔案的查詢目錄

我們可以通過如下方式給編譯器指定標頭檔案的查詢目錄:

指定格式說明
--sysroot=XX 使用xx作為這一次編譯的標頭檔案與庫檔案的查詢目錄,查詢下面的 usr/include usr/lib目錄,--sysroot即可指定標頭檔案又可指定庫檔案
-isysroot XX 指定標頭檔案查詢目錄,覆蓋--sysroot ,查詢 XX/usr/include目錄下標頭檔案
-isystem XX 指定標頭檔案查詢路徑(直接查詢根目錄)
-IXX 標頭檔案查詢目錄,I是大寫的

指定方式有多種,選取其中一種即可。

既然知道了標頭檔案的指定方式,那我們得知道stdio.h的標頭檔案目錄,stdio.h標頭檔案位於如下目錄:android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include


既然也知道標頭檔案的目錄了,我們就可以指定了,這裡通過-isystem方式指定:

1 arm-linux-androideabi-gcc.exe -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include -o d:\main C:\Users\wanglei55\Desktop\main.c

執行上面命令,又會報如下錯誤:


又提示 asm/types.h標頭檔案找不到,我們也沒用這個標頭檔案啊?這裡實在stdio.h檔案中引入的:


所以,我們還需要指定上面的標頭檔案目錄,標頭檔案所在目錄為:android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include\arm-linux-androideabi

修改命令如下,增加額外查詢命令:

1 arm-linux-androideabi-gcc.exe -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include\arm-linux-androideabi -o d:\main C:\Users\wanglei55\Desktop\main.c

執行,還是會報錯:


這裡我就直接說了,上面我們都是指定標頭檔案的查詢路徑,但是執行程式需要具體的實現來完成作用,比如在main.c中並沒有定義”printf”的函式實現,且在預編譯中包含進的”stdio.h”中也只有該函式的宣告。系統把這些函式實現都被做到名為libc.so的動態庫。那怎麼指定查詢具體實現庫的目錄呢?同樣編譯的時候可以指定庫檔案的查詢目錄:

指定方式說明
--sysroot=XX 上面已經說過--sysroot=XX即可指定標頭檔案又可指定庫檔案的查詢目錄
-LXX 指定庫檔案查詢目錄
-lxx 指定需要連結的庫名,如果庫名為libc.so,指定庫名可簡寫:-lc ,lib和.so可去掉

printf這種常用的函式都在libc.so動態庫中實現,那libc.so在哪個目錄下呢?如下:


接下來我們需要在編譯的時候指定相關庫的查詢路徑以及庫名,修改命令如下:

1 arm-linux-androideabi-gcc.exe --sysroot=C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\platforms\android-22\arch-arm -lc -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include\arm-linux-androideabi -o d:\main C:\Users\wanglei55\Desktop\main.c

到這裡我們就可以正常編譯了,但是要編譯出安卓平臺可執行檔案,編譯時還需要加入 -pie ,完整命令如下:

1 arm-linux-androideabi-gcc.exe --sysroot=C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\platforms\android-22\arch-arm -lc -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include\arm-linux-androideabi -pie -o d:\main C:\Users\wanglei55\Desktop\main.c

到此我們就可以編譯出在安卓平臺的可執行檔案了:


整個過程是不是感覺很繁瑣,其實最核心的就是編譯過程中標頭檔案和庫檔案目錄的指定方式,讓編譯器可以找到對應檔案,否則編譯的時候就會報各種錯誤,如果你有ndk相關開發經驗,應該會理解我們在cmake或者mk中的配置很多也是這種配置,就是為了讓編譯器編譯的時候能查詢到對應標頭檔案或者庫檔案

四、動態庫與靜態庫的編譯與使用

在安卓平臺上我們用的最多的是動態庫與靜態庫,我們先來看看怎麼編譯出動態庫與靜態庫並在安卓平臺使用。

原始檔為:
test.c

1 #include <stdio.h>
2 int test(){
3    return 999;
4 }

就是定義了一個test方法,返回int值999,我們將這個原始檔在電腦上先編譯為動態庫,然後在安卓平臺使用。

編譯使用動態庫

在編譯動態庫的時候我們需要指定 -fPIC -shared額外引數給編譯器,完整命令如下:

1 arm-linux-androideabi-gcc.exe --sysroot=C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\platforms\android-22\arch-arm -lc -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include\arm-linux-androideabi -fPIC -shared C:\Users\wanglei55\Desktop\test.c -o d:\libTest.so

這樣就將桌面上的test.c原始檔(test.c我放在了桌面)在d盤生成了libTest.so動態庫,接下來我們在安卓工程中使用libTest.so動態庫中的test()方法。

在工程中新疆如下目錄,並將libTest.so拷貝進去:


如不特殊指定,使用三方的動態so庫,目錄名稱必須為jniLibs。

接下來我們在native-lib.cpp檔案中呼叫libTest.so庫中的test()方法,由於是在c++檔案中呼叫c檔案編譯為動態庫中的test()方法,所以需要加上如下宣告:

1 //C++中使用C程式碼需要這樣宣告,防止C++編譯器將C中方法名編譯後認不出了
2 extern "C"{
3    extern int test();
4 }

呼叫test()方法如下:

 1 JNIEXPORT jstring JNICALL
 2 Java_com_wanglei55_ndk_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {
 3
 4    LOGE("libTest.so動態庫中test()方法返回值為:%d", test());
 5    int i = test();
 6    std::string s1 = std::to_string(i);
 7    std::string s2 = "Hello from C++";
 8    std::string s = s1 + s2;
 9    return env->NewStringUTF(s.c_str());
10 }

接下來我們還要在CMakelist.txt檔案中配置一下讓編譯器編譯的時候能夠找到libTest.so庫檔案:

 1 # CMAKE_CXX_FLAGS 會傳給c++編譯器
 2 # CMAKE_C_FLAGS 會傳給c編譯器
 3 # CMAKE_SOURCE_DIR 的值是當前CMakelist.txt所在目錄
 4 #相當於-L給編譯器傳查詢庫檔案的目錄
 5 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a")
 6
 7 # 相當於用-l給編譯器傳庫名字引數
 8 target_link_libraries( # Specifies the target library.
 9                       native-lib
10                       # libTest.so 可以去掉lib與.so
11                       Test
12                       # Links the target library to the log library
13                       # included in the NDK.
14                       ${log-lib} )

上面已經給出了相應註釋不在多餘解釋,到此就可以執行工程了,控制檯輸入對應資訊:


到此我們就自己編譯了一個so動態庫並在安卓平臺使用了動態庫中的方法。

編譯使用靜態庫

接下來我們新建staticTest.c檔案:

1 #include <stdio.h>
2 int staticTest(){
3    return 666;
4 }

我們將staticTest.c編譯為靜態庫,編譯靜態庫需要分兩步:
第一步:先將原始檔使用gcc編譯為.o檔案,命令如下:

1 arm-linux-androideabi-gcc.exe --sysroot=C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\platforms\android-22\arch-arm -lc -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include -isystem C:\Users\wanglei55\Desktop\android-ndk-r17c-windows-x86_64\android-ndk-r17c\sysroot\usr\include\arm-linux-androideabi -fPIC -c C:\Users\wanglei55\Desktop\staticTest.c -o d:\staticTest.o 

接下來使用ar工具將上一步生成的staticTest.o 檔案生成libStaticTest.a靜態庫,命令如下(第一步生成的staticTest.o檔案我自己又拷貝到桌面了):

1 arm-linux-androideabi-ar.exe r d:\libStaticTest.a C:\Users\wanglei55\Desktop\staticTest.o

ar與gcc位於同一目錄:


接下來我們就可以將靜態庫匯入安卓工程使用了,靜態庫不用非得放入jniLibs目錄,可以自己決定放入的目錄,我放入的目錄如下:


然後我們就可以使用其中的int staticTest()方法了:

 1 extern "C"{
 2    extern int test();
 3    extern int staticTest();//宣告靜態庫中的方法
 4}
 5
 6 extern "C"
 7 JNIEXPORT jstring JNICALL
 8 Java_com_wanglei55_ndk_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {
 9
10    LOGE("libTest.so動態庫中test()方法返回值為:%d", test());
11    LOGE("libStaticTest.a靜態庫中staticTest()方法返回值為:%d", staticTest());
12    int i = test();
13    int j = staticTest();
14    std::string s1 = std::to_string(i);
15    std::string s2 = std::to_string(j);
16    //std::string s2 = "Hello from C++";
17    std::string s = s1 +":::"+s2;
18    return env->NewStringUTF(s.c_str());
19 }

最後,通動態庫一樣,也需要配置匯入的靜態庫目錄為了讓編譯器編譯連結的時候能找到靜態庫,CMakeLists.txt中靜態庫匯入配置如下:

 1 。。。
 2 #引入靜態庫
 3 # IMPORTED: 表示靜態庫是以匯入的形式新增進來(預編譯靜態庫)
 4 add_library(StaticTest STATIC IMPORTED)
 5 。。。
 6 #設定靜態庫的匯入路徑
 7 set_target_properties(StaticTest PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/static/armeabi-v7a/libStaticTest.a)
 8
 9 #生成native-lib動態庫需要用到Test StaticTest log動態或者靜態庫
10 target_link_libraries( # Specifies the target library.
11                       native-lib
12                       # libTest.so 可以去掉lib與.so
13                       Test
14                       StaticTest
15                       # Links the target library to the log library
16                       # included in the NDK.
17                       log )

這樣我們就算將靜態庫引入工程並能正常呼叫其中方法了:


好了,到此我們經過上述操作將原始檔用命令列方式分別生成動態庫與靜態庫並匯入安卓工程正常使用了,這都是一些基礎方面的知識但是很重要,以後我們使用的三方庫很多都是下載原始碼,然後自己來生成靜態庫或者動態庫來使用,上面就是演示的這樣一個大題流程,那靜態庫與動態庫有什麼區別呢?接下來我們討論一下二者的區別。

五、動態庫與靜態庫的區別

在平時工作中我們經常把一些常用的函式或者功能封裝為一個個庫供給別人使用,java開發我們可以封裝為jar包提供給別人用,安卓平臺後來可以打包成aar包,同樣的,C/C++中我們封裝的功能或者函式可以通過靜態庫或者動態庫的方式提供給別人使用。

Linux平臺靜態庫以.a結尾,而動態庫以.so結尾。

那靜態庫與動態庫有什麼區別呢?

靜態庫

程式與靜態庫連線時,靜態庫中所有被使用的函式的機器碼在編譯的時候都被拷貝到最終的可執行檔案中,並且會被新增到和它連線的每個程式中:

優點:執行起來會快一些,不用查詢其餘檔案的函式庫了。

缺點:導致最終生成的可執行程式碼量相對變多,執行時, 都會被載入到記憶體中. 又多消耗了記憶體空間。

動態庫

與動態庫連線的可執行檔案只包含需要的函式的引用表,而不是所有的函式程式碼,只有在程式執行時, 那些需要的函式程式碼才被拷貝到記憶體中。

優點:生成可執行檔案比較小, 節省磁碟空間,一份動態庫駐留在記憶體中被多個程式使用,也同時節約了記憶體。

缺點:由於執行時要去連結庫會花費一定的時間,執行速度相對會慢一些。

靜態庫是時間換空間,動態庫是空間換時間,二者均有好壞。

如果我們要修改函式庫,使用動態庫的程式只需要將動態庫重新編譯就可以了,而使用靜態庫的程式則需要將靜態庫重新編譯好後,將程式再重新編譯一遍。

六、總結

本篇我們主要講解了交叉編譯,以及交叉編譯出可在安卓平臺執行的可執行檔案,動態庫,靜態庫,核心是理解整個流程,以及給編譯器傳遞標頭檔案,庫檔案的查詢路徑,本篇同樣是基礎知識部分,但是對於後續我們編譯ffmpeg等三方開源庫又是十分重要的基礎知識,好了,本篇到此為止。

相關文章