?
歡迎關注公眾號,第一時間獲取最新文章:
?
本篇目錄
一、前言
本篇主要以window開發環境為背景介紹一下NDK開發中需要掌握的交叉編譯等基礎知識,選window系統主要是照顧大多數讀者,mac ,linux操作系統基本是同樣適用的。
Java -jar,交叉編譯就是在A平臺編譯出可以在B平臺執行的文件,對于我們安卓開發者來說交叉編譯就是在window或者mac或者linux系統上編譯出可在安卓系統上運行的可執行文件,什么時候需要用到交叉編譯呢?音視頻開發基本都會用到ffmpeg,opengl es等三方庫,這時我們就需要在window或者mac或者linux系統上編譯出可在安卓系統執行的文件,這里可編譯出靜態庫或者動態庫使用,這時候就會用到交叉編譯。
本篇雖然是一些基礎的知識或者操作,但是對于后續三方庫的編譯移植,CMake的配置是很重要的,否則后續遇到沒用過的三方庫你會感覺無從下手編譯,很多CMake的配置也只是會配置而不懂具體什么含義。
進行本篇學習請先自己配置好MinGW(C/C++編譯器)編譯環境并配置到系統環境變量中,這些都是基礎的操作,自己查詢一下配置好就可以了,此外還需要自己下載好安卓平臺提供的交叉編譯工具鏈,下載地址:安卓平臺交叉編譯工具,我下載的是17c版本的。
Android NDK。好了,進入本文的學習
下文相關代碼均來自:相關演示代碼
二、常用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++文件中用到了三方的函數庫,在連接階段就需要連接三方函數庫,如果連接不到就會報錯 |
android不再支持ndk?比如在命令行中我們執行如下命令:
1 gcc -o d:\main C:\Users\wanglei55\Desktop\main.c
將C:\Users\wanglei55\Desktop\main.c文件編譯為可執行文件,輸出到d盤名稱為main,整個編譯過程就包括預處理、編譯、匯編、和連接過程。
以上主要介紹了常用C/C++編譯器的區別以及C/C++文件的編譯過程,大體了解一下即可。
三、交叉編譯
android studio jdk版本。接下來我們具體看一下交叉編譯的流程,我們先來看一下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 JNICALL2 Java_com_wanglei55_ndk_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {34 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")67 # 相當于用-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}56 extern "C"7 JNIEXPORT jstring JNICALL8 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)89 #生成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等三方開源庫又是十分重要的基礎知識,好了,本篇到此為止。