gcc/g++/clang,相當於javac:
了解c/c++編譯器的基本使用,能夠在后續移植第三方框架進行交叉編譯時,清楚的了解應該傳遞什么參數。
clang:
clang 是一個C、C++、Object-C的輕量級編譯器。基於LLVM (LLVM是以C++編寫而成的構架編譯器的框架系統,可以說是一個用於開發編譯器相關的庫)
gcc:
GNU C編譯器。原本只能處理C語言,很快擴展,變得可處理C++。(GNU計划,又稱革奴計划。目標是創建一套完全自由的操作系統)
g++:
GNU c++編譯器
gcc、g++、clang都是編譯器。
-
gcc和g++都能夠編譯c/c++,但是編譯時候行為不同。
這塊需要特別的注意,並非gcc是為c而生,而g++是為c++而生的。 -
clang也是一個編譯器,對比gcc,它具有編譯速度更快、編譯產出更小等優點,但是某些軟件在使用clang編譯時候因為源碼中內容的問題會出現錯誤。
-
clang++與clang就相當於gcc與g++。
對於gcc與g++:
-
后綴為
.c的源文件,gcc把它當作是C程序,而g++當作是C++程序;后綴為.cpp的,兩者都會認為是c++程序 -
g++會自動鏈接c++標准庫stl,gcc不會
-
gcc不會定義__cplusplus宏,而g++會
編譯器過程:
一個C/C++文件要經過預處理(preprocessing)、編譯(compilation)、匯編(assembly)、和連接(linking)才能變成可執行文件。
- 預處理
gcc -E main.c -o main.i
-E的作用是讓gcc在預處理結束后停止編譯。預處理階段主要處理include和define等。它把#include包含進來的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定義的宏用實際的字符串代替。
- 編譯階段
gcc -S main.i -o main.s
-S的作用是編譯后結束,編譯生成了匯編文件。 在這個階段中,gcc首先要檢查代碼的規范性、是否有語法錯誤等,以確定代碼的實際要做的工作,在檢查無誤后,gcc把代碼翻譯成匯編語言。
- 匯編階段
gcc -c main.s -o main.o
匯編階段把 .s文件翻譯成二進制機器指令文件.o,這個階段接收.c, .i, .s的文件都沒有問題。
- 連接階段
gcc -o main main.s
鏈接階段,鏈接的是函數庫。在main.c中並沒有定義”printf”的函數實現,且在預編譯中包含進的”stdio.h”中也只有該函數的聲明。系統把這些函數實現都被做到名為
libc.so的動態庫。
函數庫一般分為靜態庫和動態庫兩種:- 靜態庫是指編譯鏈接時,把庫文件的代碼全部加入到可執行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了。Linux中后綴名為”.a”。
- 動態庫與之相反,在編譯鏈接時並沒有把庫文件的代碼加入到可執行文件中,而是在程序執行時由運行時鏈接文件加載庫。Linux中后綴名為”.so”,如前面所述的libc.so就是動態庫。gcc在編譯時默認使用動態庫。
靜態庫節省時間:不需要再進行動態鏈接,需要調用的代碼直接就在代碼內部。
動態庫節省空間:如果一個動態庫被兩個程序調用,那么這個動態庫只需要在內存中。
關於這兩者的區別咱們用一個具體的例子來形象的說明一下:
1、假如有一個靜態庫a.a,它里面包含一個test函數,然后有個源文件source.c,它里面有個test1函數,而這個源文件需要鏈接a.a這個靜態庫,當編譯完成之后source.a里面就擁有了test+test1兩個函數了,也就是編譯期就將所有的符號加入到.so庫中。
2、假如有一個動態庫a.so,它里面包含一個test函數,然后有個源文件source.c,它里面有個test1函數,而這個源文件需要鏈接a.o這個動態庫,當編譯完成之后source.a里面就只有一個test函數,在運行時會動態的加載a.so。注:Java中在不經過封裝的情況下只能直接使用動態庫,也就是說:

了解了上面的一大堆理論之后,下面來動手做實驗,先新建一個main.c文件:

然后用gcc命令來將其編譯運行一下:

那如果將main這個可執行文件放到Android手機上,能否也能正常執行輸出呢?咱們來將它放到手機的sdcard上【注意:該手機需要的文件的執行權限才行,否則會報無權限,反正我用的帶root的模擬器執行都不行,最后找了部徹底root的華為機子來做的實驗】,如下:

這是為啥呢?其實是Android手機的CPU不同,所以CPU的指令集也不同,要在mac上編譯出來的可執行文件能在Android上也能運行這里就需要涉及到交叉編譯相關的東東了,其實在NDK中提供有交叉編譯工具,先進咱們的SDK來瞅一下:


下面咱們手動嘗試通過NDK的交互編譯工具來嘗試一下,前提當然得下載Android NDK才行,具體直接上官網下就成了,我的電腦已經下載好了,所以下面關心的就是如何來進行交叉編譯啦,首先當然得用上圖中的NDK提供的gcc啦,為了方便使用咱們先將這個gcc的文件路徑配置成臨時的環境變量,省得每次編譯時需要寫一大堆的路徑,如下:

好,咱們來用它來對main.c進行編譯一下:

原因是由於此時需要用NDK提供的頭文件來進行鏈接編譯了, 那如何指定頭文件的查找路徑為NDK提供的頭文件呢,有如下方式可以指定,下面先來了解一下【非常重要,是編譯三方庫非常重要的知識點】:
- --sysroot=XX
使用XX作為這一次編譯的頭文件與庫文件的查找目錄,查找XX下面的 usr/include、usr/lib目錄。
- -isysroot XX
頭文件查找目錄,覆蓋--sysroot ,查找 XX/usr/include。什么意思,比如說"gcc --sysroot=目錄1 main.c",如果main.c中依賴於頭文件和庫文件,則會到目錄1中的user/include和user/lib目錄去查找,而如果"gcc --sysroot=目錄1 -isysroot 目錄2 main.c"意味着會查找頭文件會到目錄2中查找而非--sysroot所指定的目錄1下的/usr/include了,當然查找庫文件還是在目錄1下的user/lib目錄去查找。
- -isystem XX
指定頭文件查找路徑(直接查找根目錄)。比如"gcc --sysroot=目錄1 -isysroot 目錄2 -isystem 目錄3 -isystem 目錄4 main.c"意味着頭文件查找除了會到目錄2下的/usr/include,還會到isystem指定的目錄3和目錄4下進行查找,注意:這個isystem指定的目錄就是頭文件查找的全路徑,而非像isysroot所指定的目錄還需要定位到/usr/include目錄。
- -IXX
頭文件查找目錄。
其查找頭文件的優先級為:
-I -> -isystem -> sysroot
比如說:“gcc --sysroot=目錄1 -isysroot 目錄2 -isystem 目錄3 -isystem 目錄4 -I目錄5 main.c”,其頭文件首先會去目錄5找,如果沒找到則會到目錄3和4找,如果還沒找到則會到目錄2找。
- -LXX
指定庫文件查找目錄。
- -lxx.so
指定需要鏈接的庫名。
我們之前在寫JNI程序時用到了Android的日志,如下:
其這個頭文件中的具體實現庫其實就是在NDK中的這個目錄里面,如下:


其實在Android Sutdio創建支付C++工程時其實默認就將這個頭文件庫的查找在CMakeLists.txt已經進行聲明了,如下:

如果用參數的形式來指定庫查找目錄其實就可以這樣寫:“gcc -L/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/usr/lib -llog”, 當然還可以用--sysroot來指令庫文件的查找路徑,只是路徑指定需要在/usr/lib之前就成,如“gcc --sysroot/Users/xiongwei/android-sdks/Android/sdk/ndk-bundle/platforms/android-21/arch-arm/ -llog”。
明白了上面的參數之后,下面咱們采用交叉編譯的方式來對我們的main.c進行重新編譯,由於NDK的路徑比較深,所以還是采用臨時環境變量的方式來弄,現在就是要指定頭文件的查找目錄,所以頭文件的位置涉及到兩處:

所以先把這個路徑定義上:

基本上有這個路徑就可以了,但是在NDK16或NDK17還有如下頭文件查找路徑:

所以可以用-isystem參數來指定,如下:

另外還有一個子目錄需要指定:

當然還是可以用-isystem參數來指定,如下:

那咱們再來加上這個CFLAGS參數編譯一下:

呃,貌似配錯了,具體是因為寫得有問題,如下:

所以修改一下:

此時就正常編譯了,咱們此時用file命令來查看一下該生成的可執行程序的文件信息,可以有個新發現:

正好就是Android能運行的指令集,所以下面咱們再將這個main導到手機上,然后這次來運行看能否見證奇跡:

然后進入到adb shell中進行執行,如下:

pie 位置無關的可執行程序:
但是有可能在windows中既始用交叉編譯在手機上執行時也會報錯,例如:

此就就需要加一個-pie參數了,如下:

所以手動交叉編譯成Android手機能正常執行的統一加"-pie"參數就成了。
費了這么多功夫就為了能編一個能在Android手機上執行的可執行文件有啥作用么?作用其實是非常之大的哈,在之后的學習中會有體現滴,明白了這個手動編譯的原理,對於將來任務三方庫要編譯成能在Android運行庫,明白了以上內容就能讓自己變得得心應手,練內功滴!!
交叉編譯動態庫與as配置:
在上面咱們編譯出來的並非是一個動態庫而只是一個可執行文件,要想要在Android工程中來使用JNI就必須將期編譯成.so的動態庫,因為:

所以咱們接下來利用交叉編譯工具來編成動態庫,然后在Android Studio中進行使用,下面先來編寫一個新的.c源文件:

此時需要將它編譯成動態庫,就需要加一個“-fPIC -shared”參數,具體用法如下:

所以咱們來使用一下,注意還是使用NDK提供的交叉編譯的GCC命令哈:

注意一下so的名稱是以libXXX.so為規則,因為我們要在Android工程中來使用它,所以將它拷到咱們的Android工具當中,如下:

然后還需要建一個cpu架構相關的文件夾,類似於我們在平常引入三方.so時一樣,比如:

所以校仿:

那接下來咱們就是要在程序中來調用這個動態庫中的test()方法,由於沒有提供這個動態庫相關的頭文件,所以可以使用如下關鍵字:

代碼已經寫好了,不過要正式運行之前還得進行CMakeLists.txt一系列的配置才行,下面一步步來進行配置,首先我們的so的目錄在編譯時是需要依賴於它但是目前還沒有指令編譯時查找庫的路徑,根據之前介紹的交叉編譯的參數可以使用如下:

那如何在CMakeLists.txt中來設置呢,涉及到一些規則,記住就成了,如下:

其中還有一個小技巧,就是對於CPU架構文件夾的指定可以使用動態的方式而不用手動寫死,不是不同的CPU架構的.so都是不一樣的嘛:

涉及到需要修改的地方在它:

可以改用如下這種動態的方式:

這樣當我們要編譯其它的CPU架構時就可以動態的替換,這里先還原寫死的方式,待后面需要的時候再用這個動態的方式,接着來則需要指定要鏈接的.so庫,配置如下:

其中也就類似於寫了如下參數:

好了,接着還需要去build.gradle中進行NDK的配置,首先指定我們要編譯的CPU架構,目前只支持"armeabi-v7a",所以配置如下:

然后編譯運行一下:

這是為啥?其實有一個非常小的細節沒有注意造成:

再次編譯:

所以更改一下:

再次編譯,發現編譯終於木有問題了,接下來咱們在MainActivity中來調用一下jni:

然后再次編譯運行:

正常輸出啦,但是有可能在其它手機上會輸出如下異常:

此時就需要在調用之前先將咱們的libTest.so動態庫給加載進來,如下:

至於為啥要再加load一次我們的生成的Test動態庫,其實還是跟動靜態庫有關,如果是引用的靜態庫的話就不會有這個問題,這個在之后再做實驗來說明這個問題。下面還是回到gradle對ndk配置相關的東東,在上面做實現的工程是因為建項目時就已經勾選了支付NDK的環境,如下:

那如果對於一個沒有勾選這個支持的Android工程我們怎么來加入對NDK的支持呢?下面建一個全新的不支持NDK的Android工程:


然后接下來就是來在build.gradle中來進行配置將該工程變為支持NDK的,如下:

如果不確認寫得對不對,可以點擊看一下能否鏈接到源碼,如果能鏈接到源碼那寫得肯定是對滴,如下:

然后繼續:

這種語法其實看着不是很符合java的語法,其實也可以用另外一種面向對象的方式來配置,具體如下:

但是貌似在我的Android Studio中這種語法不支持,所以咱們還是以上面標紅的方式來配置,基本默認新建帶NDK的工程就是使用的這種方式,接下來來配置要編譯的CPU架構:

然后在外層還有一個externalNativeBuild配置,這次我們可以改用面向對象的方式,如下:

那這兩個NDK相關的配置有啥區別呢?其實是有區別的:

由於咱們想通過mk的方式來進行編譯,所以可以在外層這樣寫:

所以我們可以在src下新建一個Android.mk文件,如下:

Android.mk
微小 GNU makefile 片段。
將源文件分組為模塊。 模塊是靜態庫、共享庫或獨立可執行文件。 可在每個
Android.mk文件中定義一個或多個模塊,也可在多個模塊中使用同一個源文件。
關於Android.mk的編譯腳本的編寫先往后放,這里關於ndk配置還差一個東東,如下:

其實是這樣的:

好,as中關於ndk的配置相關的基本已經配好了,接下來就是新建一個c/c++的源文件咱們來嘗試着編譯一下:

那這個源代碼該要如何進行編譯呢?這里就需要用到了我們建的Android.mk這個編譯腳本文件啦,首先來熟悉一下大概的語法:
變量和宏
定義自己的任意變量。在定義變量時請注意,NDK 構建系統會預留以下變量名稱:
- 以
LOCAL_開頭的名稱,例如LOCAL_MODULE。 - 以
PRIVATE_、NDK_或APP開頭的名稱。構建系統在內部使用這些變量。 - 小寫名稱,例如
my-dir。構建系統也是在內部使用這些變量。
如果為了方便而需要在 Android.mk 文件中定義自己的變量,建議在名稱前附加 MY_。
常用內置變量
| 變量名 | 含義 | 示例 |
|---|---|---|
| BUILD_STATIC_LIBRARY | 構建靜態庫的Makefile腳本 | include $(BUILD_STATIC_LIBRARY) |
| PREBUILT_SHARED_LIBRARY | 預編譯共享庫的Makeifle腳本 | include $(PREBUILT_SHARED_LIBRARY) |
| PREBUILT_STATIC_LIBRARY | 預編譯靜態庫的Makeifle腳本 | include $(PREBUILT_STATIC_LIBRARY) |
| TARGET_PLATFORM | Android API 級別號 | TARGET_PLATFORM := android-22 |
| TARGET_ARCH | CUP架構 | arm arm64 x86 x86_64 |
| TARGET_ARCH_ABI | CPU架構 | armeabi armeabi-v7a arm64-v8a |
模塊描述變量
| 變量名 | 描述 | 例 |
|---|---|---|
| LOCAL_MODULE_FILENAME | 覆蓋構建系統默認用於其生成的文件的名稱 | LOCAL_MODULE := foo LOCAL_MODULE_FILENAME := libnewfoo |
| LOCAL_CPP_FEATURES | 特定 C++ 功能 | 支持異常:LOCAL_CPP_FEATURES := exceptions |
| LOCAL_C_INCLUDES | 頭文件目錄查找路徑 | LOCAL_C_INCLUDES := $(LOCAL_PATH)/include |
| LOCAL_CFLAGS | 構建 C 和 C++ 的編譯參數 | |
| LOCAL_CPPFLAGS | c++ | |
| LOCAL_STATIC_LIBRARIES | 當前模塊依賴的靜態庫模塊列表 | |
| LOCAL_SHARED_LIBRARIES | ||
| LOCAL_WHOLE_STATIC_LIBRARIES | --whole-archive | 將未使用的函數符號也加入編譯進入這個模塊 |
| LOCAL_LDLIBS | 依賴 系統庫 | LOCAL_LDLIBS := -lz |
導出給引入模塊的模塊使用:
LOCAL_EXPORT_CFLAGS
LOCAL_EXPORT_CPPFLAGS
LOCAL_EXPORT_C_INCLUDES
LOCAL_EXPORT_LDLIBS
上面大致了解之后接下來需要在Android.mk中加入編譯規則,具體如下:

所以依照上面的規則將其編寫到Android.mk中:

咱們可以將其路徑打印看一下:

不過在編譯前還得先把mk整個配置給填充完,所以:

然后咱們來編譯一下:


然后我們看一下編譯出來的動態庫:

這是因為我們在NDK這塊只配了它:

那如果我們增加一個"x86"呢?

然后咱們再看一下編出來的APK中包含的動態庫的類型:

以上就是通過手動的方式來給咱們的一個普通Android工程增加Ndk的支持,下面來繼續解讀一下咱們在.mk中編寫的腳本的含義:



假如要有多源文件則以空格分開,如果想換行的話可以以“\”,比如:


好了,這次學習的東東說實話有些雜,但是這些知識點是非常非常之重要的基礎,只有把基礎打牢了才能在未來的NDK學習之路走得更加的遠,堅持!!!
