Android JNI編程

原文:http://blog.csdn.net/xiaoyu_93/article/details/52870395
一、前言
● NDK
Native Development Kit(NDK)是一系列工具的集合。它提供了一系列的工具,幫助開發(fā)者快速開發(fā)C/C++的動態(tài)庫,并能自動將so和java一起打包成apk。

● JNI
Java Native Interface(JNI)標準是java平臺的一部分,JNI是Java語言提供的Java和C/C++相互溝通的機制,Java可以通過JNI調(diào)用C/C++代碼,C/C++的代碼也可以調(diào)用java代碼。

● JNI與NDK的關(guān)系
NDK可以為我們生成了C/C++的動態(tài)鏈接庫,JNI是java和C/C++溝通的接口,兩者與android沒有半毛錢關(guān)系,只因為安卓是java程序語言開發(fā),然后通過JNI又能與C/C++溝通,所以我們可以使用NDK+JNI來實現(xiàn)“Java+C”的開發(fā)方式。

● 為什么要NDK開發(fā)
NDK開發(fā)具有以下優(yōu)點:

  1. 項目需要調(diào)用底層的一些C/C++的一些東西(java無法直接訪問到操作系統(tǒng)底層(如系統(tǒng)硬件等)),或者已經(jīng)在C/C++環(huán)境下實現(xiàn)了功能代碼(大部分現(xiàn)存的開源庫都是用C/C++代碼編寫的。),直接使用即可。NDK開發(fā)常用于驅(qū)動開發(fā)、無線熱點共享、數(shù)學(xué)運算、實時渲染的游戲、音視頻處理、文件壓縮、人臉識別、圖片處理等。
  2. 為了效率更加高效些。將要求高性能的應(yīng)用邏輯使用C/C++開發(fā),從而提高應(yīng)用程序的執(zhí)行效率。但是C/C++代碼雖然是高效的,在java與C/C++相互調(diào)用時卻增大了開銷;
  3. 基于安全性的考慮。防止代碼被反編譯,為了安全起見,使用C/C++語言來編寫重要的部分以增大系統(tǒng)的安全性,最后生成so庫(用過第三方庫的應(yīng)該都不陌生)便于給人提供方便。(任何有效的代碼混淆對于會smail語法反編譯你apk是分分鐘的事,即使你加殼也不能幸免高手的攻擊)
  4. 便于移植。用C/C++寫得庫可以方便在其他的嵌入式平臺上再次使用。

二、安裝與配置
首先我們在Android Studio下新建一個安卓項目。然后打開Project Structure界面,如下:


image.png

在SDK Location目錄下,有SDK和NDK的路徑,而這里我們暫時還未下載配置過NDK,故我們需要點擊Download Android NDK來進行下載(Android Studio還是很強大的,相比Eclipse能省不少事)。這里Android Studio會下載最新版本的NDK進行安裝,默認會下載保存在SDK的路徑下。我們在上圖中還能看到有一段介紹文字,說SDK以及NDK的路徑配置會保存在local.properties文件內(nèi),安裝完成后我們刷新Project,進local.properties文件查看也能看到SDK與NDK的路徑。

image.png

image.png

NDK下載配置完成之后,需要在gradle.properties文件中加上一行:
android.useDeprecatedNdk=true(為了解決版本低不能自動編譯jni,不一定需要 )

接下來,我們借助強大的Android Studio的插件功能,在External Tools下配置兩個非常有用的插件。進入Settings–>Tools–>ExternalTools,點擊+號增加。

image.png
image.png

javah -jni命令,是根據(jù)java文件生成.h頭文件的,會自動根據(jù)java文件中的類名(包含包名)與方法名生成對應(yīng)的C/C++里面的方法名。下面是參數(shù)配置及其含義:

  1. Program: $JDKPath$\bin\javah.exe 這里配置的是JDK目錄下的javah.exe的路徑。
  2. Parametes: -classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$ 這里$FileClass$指的是要執(zhí)行操作的類名(即我們操作的文件),$ModuleFileDir$/src/main/jni表示生成的文件保存在這個module目錄的src/main/jni目錄下。
  3. Working: $ModuleFileDir$\src\main\java module目錄下的src\main\java目錄(不是很理解)。
    使用方式:選中java文件—>右鍵—>External Tools—>javah-jni,將生成jni文件夾以及文件夾下的 包名.類名的.h頭文件 (名字過長,我們可以自己重命名)。
image.png

ndk -build命令,是根據(jù)C/C++文件生成so文件的。下面是參數(shù)配置及其含義:

  1. Program: F:\apk\sdk\ndk-bundle\ndk-build.cmd 這里配置的是ndk下的ndk-build.cmd的路徑(根據(jù)實際情況填寫)。
  2. Working: $ModuleFileDir$\src\main\
    使用方式:選中C/C++文件—>右鍵—>ExternalTools—>ndk-build,將在main文件夾下生成libs文件夾以及多個so文件,我們可以移動至jniLibs目錄下去。(放在libs目錄下也可以但是要在gradle文件中新增以下代碼
 sourceSets {
        main {
            jni.srcDirs = []
            jniLibs.srcDirs = ['src/main/libs/']
        }
    }

三、簡單實例

接下來我們創(chuàng)建一個訪問本地C/C++方法的java類

public class JniTest {
    /**
     * 將用C++代碼實現(xiàn),在android代碼中調(diào)用的方法:獲取當(dāng)前app的包名
     * @param o
     * @return
     */
    public static native String getPackname(Object o);

    /**
     * 加載so庫或jni庫,在使用到該庫之前加載就行,不一定非要寫在這個類內(nèi)
     * 系統(tǒng)自己會判斷擴展名是dll還是so,這里加載libJNI_ANDROID_TEST.so
     */
    static {
        System.loadLibrary("JNI_ANDROID_TEST");
    }
}

注意JNI_ANDROID_TEST這個Library名字,之后還會需要用到,要保持一致。該類提供了一個static的native方法,該方法將用來獲取app的包名。然后對該文件執(zhí)行javah -jni操作,生成對應(yīng)的.h頭文件。

image.png

如圖,已經(jīng)根據(jù)我們的java類生成了對應(yīng)的.h文件,文件名為包名類名.h,我們可以手動改名為jnitest.h,里面只有一個方法,返回值為String(jstring),方法名為Java類的包名類名方法名(包名中的分級不是用.而是_),前面兩個參數(shù)是C++里面必須有的(JNIEnv代表指向JVM的指針,jclass是調(diào)用該方法的java對象),第三個就是我們java類的方法里面的參數(shù)Object。注意,這是java函數(shù)與C++函數(shù)對應(yīng)的靜態(tài)注冊方法,即通過特定的規(guī)則來寫,此處方法名可以隨意起名字,然后還可以用動態(tài)注冊的方式關(guān)聯(lián)兩個方法(顯然,靜態(tài)注冊要簡單一些)。
然后我們新建一個C++文件,取名為jnitest.cpp,寫上需要include的文件,從.h文件中復(fù)制方法過來(方法名、參數(shù)類型、返回值等必須一致!血與淚的教訓(xùn))。

image.png

至此,.h文件和c++文件均已完成,接下來還需要在這個jni目錄下增加兩個文件,Android.mk和Application.mk。
Android.mk,注意LOCAL_MODULE的值與之前的名字相對應(yīng),LOCAL_SRC_FILES的值寫c++文件的名字,這兩個值成對設(shè)置,可設(shè)置多組。(:=是賦值的意思,$是引用某變量的值。)

LOCAL_PATH := $(call my-dir)     // 設(shè)置當(dāng)前的編譯目錄(Android.mk所在的目錄) 
include $(CLEAR_VARS)            // 清除LOCAL_XX變量(LOCAL_PATH除外)
LOCAL_MODULE := JNI_ANDROID_TEST  // 指定當(dāng)前編譯模塊的名稱  
LOCAL_SRC_FILES := jnitest.cpp    // 編譯模塊需要的源文件
include $(BUILD_SHARED_LIBRARY) // 指定編譯出的庫類型,BUILD_SHARED_LIBRARY:動態(tài)庫;BUILD_STATIC_LIBRARY:靜態(tài)庫, BUILD_EXECUTEABLE指:可執(zhí)行文件
image.png

Application.mk,APP_ABI有四種類型(默認armeabi),armeabi、armeabi-v7a、x86、mips,設(shè)置時以空格隔開,all表示所有。該文件中有個可選配置的APP_MODULES,類似于上面Android.mk文件中的LOCAL_MODULE,以空格隔開,且會覆蓋掉Android.mk文件中的LOCAL_MODULE設(shè)置(比如Android.mk文件中的寫了兩個jni庫的配置,LOCAL_MODULE := JNI1、LOCAL_MODULE := JNI2,而Application.mk中設(shè)置的APP_MODULES := JNI1,則只能生成JNI1的so文件,要生成JNI2的so文件的時候會報錯,除非寫成APP_MODULES := JNI1 JNI2,這里我們直接省略默認使用Android.mk中的)。

APP_ABI := all

接下來我們需要對C++文件執(zhí)行ndk-build操作,生成相應(yīng)的so文件。

image.png

如圖,在main/libs目錄下生成了多個so文件,名字為lib+我們指定的庫名(同時還生成了obj文件夾,不知是什么東西)。
這時候我們可以在main目錄下新建jniLibs文件夾,把生成的libs文件夾內(nèi)的東西均復(fù)制過去,刪除新生成的jni、libs、obj三個文件夾。然后在Activity中測試調(diào)用,在TextView上顯示我們通過C++代碼實現(xiàn)的方法getPackname獲取app的包名了。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = (TextView) findViewById(R.id.tv_app_package_name);
        tv.setText("packageName: " + JniTest.getPackname(MainActivity.this));
    }
}
image.png

測試能正確得到包名,說明調(diào)用成功了。我們可以把JniTest類以及so文件給別人去使用,這樣別人是看不到我們的代碼實現(xiàn)的,能很好的保護我們的源碼。

java類的包名一定要和C文件實現(xiàn)的方法名一致,不然在低版本上會報method not found的錯誤

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容