Android Studio NDK JNI 編程最小白最簡(jiǎn)單入門Demo

Android Studio 編寫JNI有兩種方式

  1. 通過(guò)ndk-build編寫,和eclipse類似,需要配置Android.mk、Application.mk文件。之前的一些開(kāi)源庫(kù)還是使用此種方式編寫.so,因此還是需要了解此種方式。
  2. 通過(guò)cmake 編寫,Android Studio 2.2(含)之后引入更方便的cmake,需要配置CMakeLists.txt。
    下面我們就通過(guò)實(shí)例一步步了解這兩種方式異同點(diǎn)。

ndk-build

首先下載ndk,可以在單獨(dú)下載ndk包,解壓到本地目錄,再將工程里配置ndk路徑至解壓的目錄,或者直接在Android Studio里下載,下載解壓成功后,自動(dòng)配置路徑,無(wú)需手動(dòng)配置。Android Studio里下載方式如下:


下載ndk

配置路徑:


配置ndk路徑

ndk準(zhǔn)備好后,接下來(lái)開(kāi)始專注于工程本身。
首先創(chuàng)建一個(gè)Android Library Module
創(chuàng)建一個(gè)加法器功能的類:CalNum

public class CalNum {
    //暴露給外界的接口
    public float testAdd(float a, float b) {
        return addFloat(a, b);
    }

    //通過(guò)jni,調(diào)用c/c++ 函數(shù)
    private static native float addFloat(float a, float b);
}

上面的addFloat是本地方法,該怎么實(shí)現(xiàn)呢?我們知道C語(yǔ)言需要一個(gè)頭文件(.h)聲明函數(shù)原型,需要一個(gè)源文件(.c)實(shí)現(xiàn)函數(shù)功能,因此需要?jiǎng)?chuàng)建兩個(gè)文件。

  1. 創(chuàng)建頭文件
    先了解下我們的工程目錄結(jié)構(gòu)
TestJni/testnum/src/main/java

TestJni是工程名,testnum是Module名,java目錄下存放的是純java代碼。
頭文件需要聲明addFloat函數(shù),jni函數(shù)名比較特殊,通過(guò)javah -jni命令生成。
進(jìn)入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文件所在目錄是臨時(shí)的,我們一般會(huì)將c/c++文件放入一個(gè)特定目錄:jni,因此我們需要?jiǎng)?chuàng)建jni目錄,右鍵點(diǎn)擊Module:


    創(chuàng)建jni目錄

    將之前的.h文件拷貝到j(luò)ni目錄下(生成的目錄名為"jni",Android Studio 展示時(shí)為"cpp",下面提到的jni等同cpp)。有了.h文件,現(xiàn)在我們來(lái)編寫.c文件,在jni目錄下創(chuàng)建.c文件,右鍵點(diǎn)擊jni:
    創(chuàng)建.c文件

    實(shí)現(xiàn).c文件函數(shù)功能:
    .c文件功能

    該函數(shù)功能實(shí)際就是計(jì)算兩個(gè)數(shù)加結(jié)果。
    好了,現(xiàn)在已經(jīng)文成.h和.c文件的編寫,那么如何將c文件編寫為.so文件呢?這個(gè)時(shí)候就需要借助Android.mk和Application.mk文件了,這兩個(gè)文件通過(guò)特定語(yǔ)法配置一些參數(shù),這些文件將決定如何生成一個(gè)makefile文件,編譯器就會(huì)依據(jù)makefile文件編譯c源文件,最終生成.so文件。
    如何編寫Android.mk文件呢?在jni目錄下新建Android.mk文件:


    Android.mk
  • LOCAL_PATH 指的是當(dāng)前目錄
  • include $(CLEAR_VARS) 指的是清空變量
  • LOCAL_MODULE := calnum 指的是生成.so文件的名稱,全稱:libcalnum.so
  • LOCAL_SRC_FILES 指的是待編譯的源文件
  • include $(BUILD_SHARED_LIBRARY) 指的是生成的庫(kù)類型,這里是動(dòng)態(tài)庫(kù)
    Android.mk 還有其它語(yǔ)法參數(shù),這里就不展開(kāi)說(shuō)明了。
    如何編寫Application.mk文件呢?在jni目錄下新建Application.mk文件。


    Application.mk
  • APP_ABI 指的是生成哪些cpu架構(gòu)支持的.so文件,all表示所有支持的架構(gòu),如果只需要生成某一種或幾種平臺(tái)支持的.so,填相應(yīng)的名字即可,比如x86、armeabi-v7a等。

ps:經(jīng)測(cè)試,這里無(wú)論怎么填,都默認(rèn)生成所有支持平臺(tái)的.so。

至此,jni目錄下的文件已經(jīng)準(zhǔn)備齊全:


jni目錄

這時(shí)候我們開(kāi)始make module,然而令人失望的是卻是報(bào)錯(cuò),原因是我們僅僅準(zhǔn)備了jni相關(guān)文件,編譯器并不知道如何去操作jni文件,而我們又知道Android.mk記錄這編譯相關(guān)東西,因此應(yīng)當(dāng)先讓編譯器找到Android.mk文件。
在module的build.gradle里,android層級(jí)內(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/ 目錄下,因此需要通過(guò)上級(jí)目錄索引到Android.mk

這個(gè)時(shí)候我們?cè)賛ake module,成功了!那么生成的.so文件在哪呢?首先定位到app build/intermediates 目錄下,搜索".so"文件,經(jīng)過(guò)篩選,找到如下目錄:

build/intermediates/ndkBuild/debug/obj/local

該目錄下文件為:


生成.so

每個(gè)目錄下有對(duì)應(yīng)平臺(tái)的.so庫(kù),如下:


.so

我們應(yīng)該注意到了,這里只是生成了4種平臺(tái)下的.so庫(kù),我們明明記得一般是支持7種平臺(tái)呢?沒(méi)錯(cuò),這里確實(shí)少了 armeabi,mips,mips64平臺(tái),因?yàn)樵趎dk17開(kāi)始不再支持這三種平臺(tái),而我們這里使用的ndk版本是20。那么如果想所有平臺(tái)都支持呢?那么使用的ndk版本需要低于17(經(jīng)過(guò)測(cè)試,ndk16也不行,最好是15及其以下)。

要查看ndk支持的abi,定位到ndk目錄下,執(zhí)行ndk-which命令,即可輸出該版本ndk支持的abi。

上面我們說(shuō)了支持的平臺(tái)不夠多,但是我們還想減少支持的平臺(tái)數(shù)呢,這時(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"
    }

想要生成哪個(gè)平臺(tái),填其名字即可。
現(xiàn)在.so文件已經(jīng)生成,另一個(gè)模塊如何調(diào)用呢?記得我們創(chuàng)建模塊的時(shí)候是創(chuàng)建了Android Library,也就是說(shuō)我們module編譯成了.jar文件,該.jar文件負(fù)責(zé)調(diào)用.so文件里的函數(shù),并且.jar文件暴露給外界模塊接口,外界模塊間接調(diào)用了.so,整個(gè)流程下來(lái)就完成了一個(gè)最簡(jiǎn)單的jni編程、實(shí)例調(diào)用。那具體怎么配置module調(diào)用呢?有兩種方法:

1、調(diào)用者工程內(nèi)直接依賴Android Library module,優(yōu)點(diǎn)是方便調(diào)試,前提是我們有Android Library module源碼
2、 調(diào)用者工程內(nèi)依賴.jar包,現(xiàn)成的第三方庫(kù)一般以jar包形式提供。

下面分別簡(jiǎn)要說(shuō)明兩者的配置方式

  • 直接依賴module:


    選擇依賴

    選中Module dependency,選擇需要依賴的module,確定后,再到調(diào)用者的module build.gradle里查看:


    依賴module

    最后一項(xiàng)就是之前界面操作的結(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包,依然可以通過(guò)界面操作依賴:


    選擇依賴

    選中jar dependency,選擇需要依賴的jar,確定后,再到調(diào)用者的module build.gradle里查看:


    依賴jar

    最后一項(xiàng)就是之前界面操作的結(jié)果。
    jar包依賴已經(jīng)搞定,還有so庫(kù)呢?也是有兩種方式,對(duì)應(yīng)上面和兩種依賴jar包方式:
  • 直接依賴module
    這種方式下不用配置so庫(kù)位置
  • 間接依賴module
    定位到調(diào)用者module src/main 目錄下,新建文件夾,名為:"jniLibs“,然后將之前生成的各個(gè)平臺(tái)的so庫(kù)放入該文件夾下:


    jniLibs

    當(dāng)然,如果不想放在該目錄下,也可以和jar包一起放在"libs"文件夾下,前提是需要在build.gradle android 層級(jí)下指明so庫(kù)的位置

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

至此,jar包和so庫(kù)都準(zhǔn)備好了,調(diào)用者就可以調(diào)用暴露出來(lái)的接口進(jìn)行訪問(wèn)了。

        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包種的類


jar包里的類

cmake

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


cmake工具

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


native-lib.cpp

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

我們來(lái)看看該文件里邊的語(yǔ)法:


cmakelists.txt 內(nèi)容
  • add_library
    native-lib 指的是要生成的庫(kù)名稱
    SHARED 指的是生成的庫(kù)為動(dòng)態(tài)庫(kù)
    src/main/cpp/native-lib.cpp 指的是需要編譯的源文件
  • find_library 配置的是需要依賴的外部庫(kù)
  • target_link_libraries 配置的是最終將多個(gè)庫(kù)鏈接起來(lái)
    3、build.gradle 新增了幾項(xiàng)配置:


    build.gradle

    配置 cmake 編譯參數(shù)等。

        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }

讓編譯器知道CMakeLists.txt 位置

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

注:第三點(diǎn)是手動(dòng)新增的,新建的project并沒(méi)有,編譯會(huì)報(bào)錯(cuò),加上第三點(diǎn)解決編譯報(bào)錯(cuò)問(wèn)題。

至此,cmake方式編譯jni配置工作就完成了,只需要簡(jiǎn)單的勾選就可以支持ndk編程,是不是覺(jué)得比之前方便多了。也許你會(huì)問(wèn),創(chuàng)建工程時(shí)忘了添加c++支持,后面有需要編譯jni怎么辦呢?還是按照上面的方法,手動(dòng)添加:

1、下載cmake工具
2、新建jni,編寫.c/.h 、c++源文件
3、新建CMakeLists.txt,配置其中參數(shù)
4、配置build.gradle

ndk-build方式和cmake方式編寫簡(jiǎn)單入門jni程序已經(jīng)梳理完畢,總結(jié)幾個(gè)比較關(guān)鍵的點(diǎn):

1、兩種方式需要哪些配置文件容易搞混亂(相對(duì)來(lái)說(shuō),具體配置文件語(yǔ)法都可以查得到,反而比較簡(jiǎn)單)
2、配置時(shí)路徑容易搞糊涂,實(shí)際上只要厘清當(dāng)前配置文件所在的位置、待指向的配置的文件所在的位置,相對(duì)位置就整明白了,進(jìn)而填上相對(duì)索引即可訪問(wèn)

最后,整個(gè)效果圖:


效果

本文基于Android Studio 3.2 NDK 20

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容