Android Studio 編寫JNI有兩種方式
- 通過ndk-build編寫,和eclipse類似,需要配置Android.mk、Application.mk文件。之前的一些開源庫還是使用此種方式編寫.so,因此還是需要了解此種方式。
- 通過cmake 編寫,Android Studio 2.2(含)之后引入更方便的cmake,需要配置CMakeLists.txt。
下面我們就通過實例一步步了解這兩種方式異同點。
ndk-build
首先下載ndk,可以在單獨下載ndk包,解壓到本地目錄,再將工程里配置ndk路徑至解壓的目錄,或者直接在Android Studio里下載,下載解壓成功后,自動配置路徑,無需手動配置。Android Studio里下載方式如下:

配置路徑:

ndk準備好后,接下來開始專注于工程本身。
首先創(chuàng)建一個Android Library Module
創(chuàng)建一個加法器功能的類:CalNum
public class CalNum {
//暴露給外界的接口
public float testAdd(float a, float b) {
return addFloat(a, b);
}
//通過jni,調(diào)用c/c++ 函數(shù)
private static native float addFloat(float a, float b);
}
上面的addFloat是本地方法,該怎么實現(xiàn)呢?我們知道C語言需要一個頭文件(.h)聲明函數(shù)原型,需要一個源文件(.c)實現(xiàn)函數(shù)功能,因此需要創(chuàng)建兩個文件。
- 創(chuàng)建頭文件
先了解下我們的工程目錄結(jié)構(gòu)
TestJni/testnum/src/main/java
TestJni是工程名,testnum是Module名,java目錄下存放的是純java代碼。
頭文件需要聲明addFloat函數(shù),jni函數(shù)名比較特殊,通過javah -jni命令生成。
進入Android Studio Terminal,cd 到 java 目錄,執(zhí)行如下命令:
javah -jni com.fish.testnum.CalNum
- com.fish.testnum 是包名
- CalNum 是本地方法所在類的類名
-
javah 命令需要配置java jre環(huán)境變量
該命令成功在java目錄下生成.h文件
.h文件
之前生成的.h文件所在目錄是臨時的,我們一般會將c/c++文件放入一個特定目錄:jni,因此我們需要創(chuàng)建jni目錄,右鍵點擊Module:
創(chuàng)建jni目錄
將之前的.h文件拷貝到j(luò)ni目錄下(生成的目錄名為"jni",Android Studio 展示時為"cpp",下面提到的jni等同cpp)。有了.h文件,現(xiàn)在我們來編寫.c文件,在jni目錄下創(chuàng)建.c文件,右鍵點擊jni:
創(chuàng)建.c文件
實現(xiàn).c文件函數(shù)功能:
.c文件功能
該函數(shù)功能實際就是計算兩個數(shù)加結(jié)果。
好了,現(xiàn)在已經(jīng)文成.h和.c文件的編寫,那么如何將c文件編寫為.so文件呢?這個時候就需要借助Android.mk和Application.mk文件了,這兩個文件通過特定語法配置一些參數(shù),這些文件將決定如何生成一個makefile文件,編譯器就會依據(jù)makefile文件編譯c源文件,最終生成.so文件。
如何編寫Android.mk文件呢?在jni目錄下新建Android.mk文件:
Android.mk - LOCAL_PATH 指的是當前目錄
- include $(CLEAR_VARS) 指的是清空變量
- LOCAL_MODULE := calnum 指的是生成.so文件的名稱,全稱:libcalnum.so
- LOCAL_SRC_FILES 指的是待編譯的源文件
-
include $(BUILD_SHARED_LIBRARY) 指的是生成的庫類型,這里是動態(tài)庫
Android.mk 還有其它語法參數(shù),這里就不展開說明了。
如何編寫Application.mk文件呢?在jni目錄下新建Application.mk文件。
Application.mk - APP_ABI 指的是生成哪些cpu架構(gòu)支持的.so文件,all表示所有支持的架構(gòu),如果只需要生成某一種或幾種平臺支持的.so,填相應(yīng)的名字即可,比如x86、armeabi-v7a等。
ps:經(jīng)測試,這里無論怎么填,都默認生成所有支持平臺的.so。
至此,jni目錄下的文件已經(jīng)準備齊全:

這時候我們開始make module,然而令人失望的是卻是報錯,原因是我們僅僅準備了jni相關(guān)文件,編譯器并不知道如何去操作jni文件,而我們又知道Android.mk記錄這編譯相關(guān)東西,因此應(yīng)當先讓編譯器找到Android.mk文件。
在module的build.gradle里,android層級內(nèi),指定Android.mk位置:
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
這里需要注意的是路徑的確定
"src/main/jni/Android.mk"
build.gradle在app 目錄下,而Android.mk 在/src/main/jni/ 目錄下,因此需要通過上級目錄索引到Android.mk
這個時候我們再make module,成功了!那么生成的.so文件在哪呢?首先定位到app build/intermediates 目錄下,搜索".so"文件,經(jīng)過篩選,找到如下目錄:
build/intermediates/ndkBuild/debug/obj/local
該目錄下文件為:

每個目錄下有對應(yīng)平臺的.so庫,如下:

我們應(yīng)該注意到了,這里只是生成了4種平臺下的.so庫,我們明明記得一般是支持7種平臺呢?沒錯,這里確實少了 armeabi,mips,mips64平臺,因為在ndk17開始不再支持這三種平臺,而我們這里使用的ndk版本是20。那么如果想所有平臺都支持呢?那么使用的ndk版本需要低于17(經(jīng)過測試,ndk16也不行,最好是15及其以下)。
要查看ndk支持的abi,定位到ndk目錄下,執(zhí)行ndk-which命令,即可輸出該版本ndk支持的abi。
上面我們說了支持的平臺不夠多,但是我們還想減少支持的平臺數(shù)呢,這時候需要在module build.gradle android { {defaultConfig xxx}} 添加如下代碼:
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
abiFilters "x86", "x86_64", "armeabi-v7a",
"arm64-v8a"
}
想要生成哪個平臺,填其名字即可。
現(xiàn)在.so文件已經(jīng)生成,另一個模塊如何調(diào)用呢?記得我們創(chuàng)建模塊的時候是創(chuàng)建了Android Library,也就是說我們module編譯成了.jar文件,該.jar文件負責(zé)調(diào)用.so文件里的函數(shù),并且.jar文件暴露給外界模塊接口,外界模塊間接調(diào)用了.so,整個流程下來就完成了一個最簡單的jni編程、實例調(diào)用。那具體怎么配置module調(diào)用呢?有兩種方法:
1、調(diào)用者工程內(nèi)直接依賴Android Library module,優(yōu)點是方便調(diào)試,前提是我們有Android Library module源碼
2、 調(diào)用者工程內(nèi)依賴.jar包,現(xiàn)成的第三方庫一般以jar包形式提供。
下面分別簡要說明兩者的配置方式
-
直接依賴module:
選擇依賴
選中Module dependency,選擇需要依賴的module,確定后,再到調(diào)用者的module build.gradle里查看:
依賴module
最后一項就是之前界面操作的結(jié)果。 -
間接依賴module:
先找到j(luò)ar包,定位到libary module app/build/intermediates 目錄下,搜索".jar",
最后定位到:build/intermediates/packaged-classes/debug 目錄下的class.jar,就是我們要找的jar包,可以將之改為比較好記名字,這里改為calnum.jar,將該文件拷貝至調(diào)用者module app/lib目錄下,再在調(diào)用者module里引入該.jar包,依然可以通過界面操作依賴:
選擇依賴
選中jar dependency,選擇需要依賴的jar,確定后,再到調(diào)用者的module build.gradle里查看:
依賴jar
最后一項就是之前界面操作的結(jié)果。
jar包依賴已經(jīng)搞定,還有so庫呢?也是有兩種方式,對應(yīng)上面和兩種依賴jar包方式: - 直接依賴module
這種方式下不用配置so庫位置 -
間接依賴module
定位到調(diào)用者module src/main 目錄下,新建文件夾,名為:"jniLibs“,然后將之前生成的各個平臺的so庫放入該文件夾下:
jniLibs
當然,如果不想放在該目錄下,也可以和jar包一起放在"libs"文件夾下,前提是需要在build.gradle android 層級下指明so庫的位置
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
至此,jar包和so庫都準備好了,調(diào)用者就可以調(diào)用暴露出來的接口進行訪問了。
btnNdk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CalNum calNum = new CalNum();
String toast = calNum.testAdd(20, 30) + " nukbuild";
Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
}
});
其中calNum 類就是jar包種的類

cmake
通過上面ndk-build方式可知,需要我們配置Android.mk Application.mk文件,比較繁瑣。Google為此推出了新的編譯方式-cmake,那么cmake需要怎么做呢?
首先下載cmake工具

其次,新建project的時候,會有一個c++支持選項,勾選即可。project創(chuàng)建完畢后,會發(fā)現(xiàn)比沒勾選時多了幾個文件:
1、src/main 目錄下新建了jni文件夾,并且預(yù)先放置了一個cpp文件:

和ndk-build jni目錄一致的,當然也可以放.c 和 .h文件。
2、app 目錄下多了個CMakeLists.txt,該文件的作用和ndk-build時使用的.mk文件類似。

我們來看看該文件里邊的語法:

- add_library
native-lib 指的是要生成的庫名稱
SHARED 指的是生成的庫為動態(tài)庫
src/main/cpp/native-lib.cpp 指的是需要編譯的源文件 - find_library 配置的是需要依賴的外部庫
-
target_link_libraries 配置的是最終將多個庫鏈接起來
3、build.gradle 新增了幾項配置:
build.gradle
配置 cmake 編譯參數(shù)等。
externalNativeBuild {
cmake {
cppFlags ""
}
}
讓編譯器知道CMakeLists.txt 位置
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
注:第三點是手動新增的,新建的project并沒有,編譯會報錯,加上第三點解決編譯報錯問題。
至此,cmake方式編譯jni配置工作就完成了,只需要簡單的勾選就可以支持ndk編程,是不是覺得比之前方便多了。也許你會問,創(chuàng)建工程時忘了添加c++支持,后面有需要編譯jni怎么辦呢?還是按照上面的方法,手動添加:
1、下載cmake工具
2、新建jni,編寫.c/.h 、c++源文件
3、新建CMakeLists.txt,配置其中參數(shù)
4、配置build.gradle
ndk-build方式和cmake方式編寫簡單入門jni程序已經(jīng)梳理完畢,總結(jié)幾個比較關(guān)鍵的點:
1、兩種方式需要哪些配置文件容易搞混亂(相對來說,具體配置文件語法都可以查得到,反而比較簡單)
2、配置時路徑容易搞糊涂,實際上只要厘清當前配置文件所在的位置、待指向的配置的文件所在的位置,相對位置就整明白了,進而填上相對索引即可訪問
最后,整個效果圖:

本文基于Android Studio 3.2 NDK 20











