Android Native 代碼開發學習筆記

本文提供排版更佳的PDF版本下載。html

JNI,全稱Java Native Interface,是用於讓運行在JVM中的Java代碼和運行在JVM外的Native代碼(主要是C或者C++)溝通的橋樑。代碼編寫者便可以使用JNI從Java的程序中調用Native代碼,又能夠從Native程序中調用Java代碼。這樣,編程人員能夠將低階的代碼邏輯包裝到高階的程序框架中,得到高性能高效率的同時保證了代碼框架的高抽象性。java

在Android中,僅有如下類庫是容許在JNI中使用的:android

  • libc (C library) headers
  • libm (math library) headers
  • JNI interface headers
  • libz (Zlib compression) headers
  • liblog (Android logging) header
  • OpenGL ES 1.1 (3D graphics library) headers (since 1.6)
  • A Minimal set of headers for C++ support

JNI自己僅僅是一個把二者融合的工具,做爲編程者須要作的,就是在Java代碼和Native代碼中按照固定的格式告訴JNI如何調用對方。在Android中,有兩種方式能夠調用JNI,一種是Google release的專門針對Android Native開發的工具包,叫作NDK。去Android網站上下載該工具包後,就能夠經過閱讀裏面的文檔來setup一個新的包含Native代碼的工程,建立本身的Android.mk文件,編譯等等;另外一種是完整的源碼編譯環境 ,也就是經過git從官方網站獲取徹底的Android源代碼平臺。這個平臺中提供有基於make的編譯系統。更多細節請參考這裏。無論選擇以上兩種方法的哪個,都必須編寫本身的Android.mk文件,有關該文件的編寫請參考相關文檔。git

下面經過一個簡單的使用例子來說解JNI。Android給C和C++提供的是兩套不一樣的Native API,本文僅以C++舉例說明。假設這麼一個需求,Java代碼須要打印一個字符串,而該字符串須要Native代碼計算生成。對應的JNI流程是這樣的:web

1. 在準備打印字符串的Android類中,添加兩段代碼。編程

第一段是:數組

private native String getPrintStr();oracle

這一行代碼的目的是告訴JNI,這個Java文件中有這麼一個函數,該函數是在Native代碼中執行的,Native代碼會返回一個字符串供Java代碼來輸出。框架

第二段是:dom

try {System.loadLibrary(「LIBNAME」 }

catch (UnsatisfiedLinkError ule) {Log.e(TAG, 「Could not load native library」);}

這兩行代碼是告訴JNI,你須要找的全部Native函數都在libLIBNAME.so這個動態庫中。注意JNI會自動補全lib和so給LIBNAME,你只須要提供LIBNAME給loadLibrary就好了。在最後執行的時候,JNI會先找到這個動態庫,而後找裏面的OnLoad函數,具體註冊流程由OnLoad函數接管。

關於如何肯定這個LIBNAME,和如何定義OnLoad函數,下面就會講。

2. 上面的第一步是告訴JNI,java代碼須要和Native代碼交互,同時把在哪裏找,找什麼都通知了。接下來的事情就由Native端接管。若是把上面的getPrintString函數申明比做原型,那麼本地代碼中的具體函數定義就應該和該原型匹配,JNI才能知道具體在哪裏執行代碼。具體來講,應該有一個對應的Native函數,有和Java中定義的函數一樣的參數列表以及返回值。另外,還須要有某種機制讓JNI將二者相互映射,方便參數和返回值的傳遞。在老版的JNI中,這是經過醜陋的命名匹配實現的,好比說在Java中定義的函數名是getPrintStr, 該函數屬於package java.come.android.xxx,那麼中對應Native代碼中的函數名就應該是Java_com_android_xxx_getPrintStr。這樣給開發人員帶來了不少不便。能夠用javah命令來生成對應Java code中定義函數的Native code版本header文件,從中得知傳統的匹配方法是如何作的。具體過程以下:

  1. 經過SDK的方式編譯Java代碼。
  2. 找到Eclipse的工程目錄,進入bin目錄下。這裏是編譯出的java文件所對應的class文件所在。
  3. 假設包括Native函數調用的java文件屬於com.android.xxx package,名字叫test.java,那麼在bin下執行javah -jni com.android.xxx.test

執行完後,能夠看到一個新生成的header文件,名字爲com_android_xxx_test.h。打開後會發現已經有一個函數申明,函數名爲java_com_android_xxx_test_getPrintStr。這個名字就包括了該函數所對應Java版本所在的包,文件以及名稱。這就是JNI傳統的肯定名字的方法。

值得注意的是,header文件中不只包含了基於函數名的映射信息,還包含了另外一個重要信息,就是signature。一個函數的signature是一個字符串,描述了這個函數的參數和返回值。其中」()」 中的字符表示參數,後面的則表明返回值。例如」()V」 就表示void Func(); 「(II)V」 表示 void Func(int, int); 數組則以」["開始,用兩個字符表示。

具體的每個字符的對應關係以下:

字符

Java類型

C類型

V

void

void

I

jint

int

Z

jboolean

boolean

J

jlong

long

D

jdouble

double

F

jfloat

float

B

jbyte

byte

C

jchar

char

S

jshort

short

上面的都是基本類型。若是Java函數的參數是class,則以"L"開頭,以";"結尾,中間是用"/" 隔開的包及類名。而其對應的C函數名的參數則爲jobject。 一個例外是String類,其對應的類爲jstring。舉例:

Ljava/lang/String; String jstring

Ljava/net/Socket; Socket jobject

若是JAVA函數位於一個嵌入類,則用$做爲類名間的分隔符。例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"

這個signature很是重要,是下面要介紹的新版命名匹配方法的關鍵點之一。因此,即便傳統的命名匹配已經再也不使用,javah這一步操做仍是必須的,由於能夠從中獲得Java代碼中須要Native執行的函數的簽名,以供後面使用。

3. 在新版(版本號大於1.4)的JNI中,Android提供了另外一個機制來解決命名匹配問題,那就是JNI_OnLoad。正如前面所述,每一次JNI執行Native代碼,都是經過調用JNI_OnLoad實現的。下面的代碼是針對本例的OnLoad代碼:

/* Returns the JNI version on success, -1 on failure.

jint JNI_OnLoad(JavaVM* vm, void* reserved) {

JNIEnv* env = NULL;

jint result = -1;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {

LOGE("ERROR: GetEnv failed");

goto bail;

}

assert(env != NULL);

if (!register_Test(env)) {

LOGE("ERROR: Test native registration failed");

goto bail;

}

/* success -- return valid version number */

result = JNI_VERSION_1_4;

bail:??return result;

}

分析這個函數。首先,OnLoad經過GetEnv函數獲取JNI的環境對象,而後經過register_Test來註冊Native函數。register_Test的實現以下:

int register_Test(JNIEnv *env) {

const char* const ClassPathName ?= "com/android/xxx/test";

return registerNativeMethods(env, ClassPathName, TestMethods,

sizeof(TestMethods) / sizeof(TestMethods[0]));

}

在這裏,ClassPathName是Java類的全名,包括package的全名。只是用 「/」 代替 」.」 。而後咱們把類名以及TestMethods這個參數一同送到registerNativeMethods這個函數中註冊。這個函數是基於JNI_OnLoad的命名匹配方式的重點。

在JNI中,代碼編寫者經過函數signature名和映射表的配合,來告訴JNI_OnLoad,你要找的函數在Native代碼中是如何定義的(signature),以及在哪定義的(映射表)。關於signature的生成和含義,在上面已經介紹。而映射表,是Android使用的一種用於映射Java和C/C++函數的數組,這個數組的類型是JNINativeMethod,定義爲:

typedef struct {

const char* name;

const char* signature;

void* fnPtr;

} JNINativeMethod;

其中,第一個變量是Java代碼中的函數名稱。第二個變量是該函數對應的Native signature。第三個變量是該函數對應的Native函數的函數指針。例如,在上面register_Test的函數實現中,傳給registerNativeMethods的參數TestMethods就是映射表,定義以下:

static JNINativeMethod TestMethods[] = {

{「getPrintStr」, 「()Ljava/lang/String」, (void*)test_getPrintStr}

};

其中getPrintStr是在Java代碼中定義的函數的名稱,()Ljava/lang/String是簽名,由於該函數無參數傳入,並返回一個String。test_getPrintStr則是咱們即將在Native code中定義的函數名稱。該映射表和前面定義的類名ClassPathName一塊兒傳入registerNativeMethods:

static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* ????Methods, int numMethods) {

jclass clazz;

clazz = env->FindClass(className);

if (clazz == NULL) {

LOGE(「Native registration unable to find class ‘%s’」, className);

return JNI_FALSE;

}

if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {

LOGE(「RegisterNatives failed for ‘%s’」, className);

return JNI_FALSE;

}

return JNI_TRUE;

}

在這裏,先load目標類,而後註冊Native函數,而後返回狀態。

能夠看出,經過映射表方式,Java code中的函數名不須再和Native code中的函數名呆板對應。只須要將函數註冊進映射表中,Native code的函數編寫就有了很大的靈活性。雖然說和前一種傳統的匹配方法比,這種方式並無效率上的改進,由於二者本質上都是從JNI load開始作函數映射。可是這一種register的方法極大下降了兩邊的耦合性,因此實際使用中會受歡迎得多。好比說,因爲映射表是一個<名稱,函數指針>對照表,在程序執行時,可屢次調用registerNativeMethods()函數來更換本地函數指針,而達到彈性抽換本地函數的目的。

4. 接下來本應介紹test_getPrintStr。但在此以前,簡單介紹Android.mk,也就是編譯NDK所須要的Makefile,從而完成JNI信息鏈的講解。Android.mk能夠基於模版修改,裏面重要的變量包括:

  • LOCAL_C_INCLUDES:包含的頭文件。這裏須要包含JNI的頭文件。
  • LOCAL_SRC_FILES: 包含的源文件。
  • LOCAL_MODULE:當前模塊的名稱,也就是第一步中咱們提到的LIBNAME。注意這個須要加上lib前綴,但不須要加.so後綴,也就是說應該是libLIBNAME。
  • LOCAL_SHARED_LIBRARIES:當前模塊須要依賴的共享庫。
  • LOCAL_PRELINK_MODULE:該模塊是否被啓動就加載。該項設置依具體程序的特性而定。

5. 至此,JNI做爲橋樑所須要的全部信息均已就緒。JNI知道在調用Java代碼中的getPrintStr函數時,須要執行Native代碼。因而經過System.loadLibrary所加載的libLIBNAME.so找到OnLoad入口。在OnLoad中,JNI發現了函數映射表,發現getPrintStr對應的Native函數是test_getPrintStr。因而JNI將參數(若是有的話)傳遞給test_getPrintStr並執行,再將返回值(若是有的話)傳回Java中的getPrintStr。

6. 用於最後測試的test_getPrintStr函數實現以下:

const jstring testStr = env->NewStringUTF(「hello, world」);

return testStr;

而後在Java代碼中打印出返回的字符串便可。這個網頁詳細介紹了env能夠調用的全部方法。

7. 關於測試時使用Log。調用JNI進行Native Code的開發有兩種環境,完整源碼環境以及NDK。兩種環境對應的Log輸出方式也並不相同,差別則主要體如今須要包含的頭文件中。若是是在完整源碼編譯環境下,只要include 頭文件(位於Android-src/system/core/include/cutils),就可使用對應的LOGI、LOGD等方法了,固然LOG_TAG,LOG_NDEBUG等宏值須要自定義。若是是在NDK環境下編譯,則須要include 頭文件(位於ndk/android-ndk-r4/platforms/android-8/arch-arm/usr/include/android/),另外本身定義宏映射,例如:

#include

#ifndef LOG_TAG

#define LOG_TAG 「MY_LOG_TAG」

#endif

#define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

#define LOGI(…) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)

#define LOGW(…) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)

#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define LOGF(…) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)

另外,在Android.mk文件中對類庫的應用在兩種環境下也不相同。若是是NDK環境下,須要包括

LOCAL_LDLIBS := -llog

而在完整源碼環境下,則須要包括

LOCAL_SHARED_LIBRARIES := libutils libcutils

8. 若是但願知道如何在Native中訪問Java類的私有域和方法,請參考這篇文章

Random Posts

    visit the website for more great content.
    相關文章
    相關標籤/搜索