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

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

一、前言
本篇主要以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:Userswanglei55Desktopmain.c
將C:Userswanglei55Desktopmain.c文件編譯爲可執行文件,輸出到d盤名稱爲main,整個編譯過程就包括預處理、編譯、彙編、和連接過程。

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

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

我們編寫如下C文件:
main.c

複製代碼
1 #include
2 int main()
3 {
4 int nn = 55;
5 printf("nn = %dn", nn);
6 return 0;
7 }
複製代碼
很簡單,就是輸出一些信息,接下來我們將main.c用gcc編譯器編譯爲可執行文件,執行如下命令:

1 gcc -o d:main C:Userswanglei55Desktopmain.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:Userswanglei55Desktopmain.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_64android-ndk-r17csysrootusrinclude

既然也知道頭文件的目錄了,我們就可以指定了,這裏通過-isystem方式指定:

1 arm-linux-androideabi-gcc.exe -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrinclude -o d:main C:Userswanglei55Desktopmain.c
執行上面命令,又會報如下錯誤:

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

所以,我們還需要指定上面的頭文件目錄,頭文件所在目錄爲:android-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrincludearm-linux-androideabi

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

1 arm-linux-androideabi-gcc.exe -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrinclude -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrincludearm-linux-androideabi -o d:main C:Userswanglei55Desktopmain.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:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17cplatformsandroid-22arch-arm -lc -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrinclude -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrincludearm-linux-androideabi -o d:main C:Userswanglei55Desktopmain.c
到這裏我們就可以正常編譯了,但是要編譯出安卓平臺可執行文件,編譯時還需要加入 -pie ,完整命令如下:

1 arm-linux-androideabi-gcc.exe --sysroot=C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17cplatformsandroid-22arch-arm -lc -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrinclude -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrincludearm-linux-androideabi -pie -o d:main C:Userswanglei55Desktopmain.c
到此我們就可以編譯出在安卓平臺的可執行文件了:

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

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

源文件爲:
test.c

1 #include
2 int test(){
3 return 999;
4 }
就是定義了一個test方法,返回int值999,我們將這個源文件在電腦上先編譯爲動態庫,然後在安卓平臺使用。

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

1 arm-linux-androideabi-gcc.exe --sysroot=C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17cplatformsandroid-22arch-arm -lc -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrinclude -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrincludearm-linux-androideabi -fPIC -shared C:Userswanglei55Desktoptest.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
2 int staticTest(){
3 return 666;
4 }
我們將staticTest.c編譯爲靜態庫,編譯靜態庫需要分兩步:
第一步:先將源文件使用gcc編譯爲.o文件,命令如下:

1 arm-linux-androideabi-gcc.exe --sysroot=C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17cplatformsandroid-22arch-arm -lc -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrinclude -isystem C:Userswanglei55Desktopandroid-ndk-r17c-windows-x86_64android-ndk-r17csysrootusrincludearm-linux-androideabi -fPIC -c C:Userswanglei55DesktopstaticTest.c -o d:staticTest.o
接下來使用ar工具將上一步生成的staticTest.o 文件生成libStaticTest.a靜態庫,命令如下(第一步生成的staticTest.o文件我自己又拷貝到桌面了):

1 arm-linux-androideabi-ar.exe r d:libStaticTest.a C:Userswanglei55DesktopstaticTest.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等三方開源庫又是十分重要的基礎知識,好了,本篇到此爲止。
原文地址https://www.cnblogs.com/leipDao/p/10685302.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章