Android中JNI編程的那些事兒



Android中JNI編程的那些事兒

http://www.cnblogs.com/keis/archive/2011/04/12/2013174.html

首先說明,Android系統不允許一個純粹使用C/C++的程序出現,它要求必須是通過Java代碼嵌入Native C/C++——即通過JNI的方式來使用本地(Native)代碼。因此JNI對Android底層開發人員非常重要。


如何將.so文件打包到.APK

讓我們  從最簡單的情況開始,假如已有一個JNI實現——libxxx.so文件,那麼如何在APK中使用它呢?

在我最初寫類似程序的時候,我會將libxxx.so文件push到/system/lib/目錄下,然後在Java代碼中執行System.loadLibrary(xxx),這是個可行的做法,但需要取得/system/lib 目錄 的寫權限(模擬器通過adb remount取得該權限)。但模擬器 重啓之 後libxxx.so文件會消失。現在 我找到了更好的方法,把.so文件打包到apk中分發給最終用戶,不管是模擬器 或者 真機 ,都不再需要system分區的寫權限。實現步驟如下:

1、在你的項目根目錄下建立libs/armeabi目錄;

2、將libxxx.so文件copy到 libs/armeabi/下;

3、此時ADT插件自動編譯輸出的.apk文件中已經包括.so文件了;

4、安裝APK文件,即可直接使用JNI中的方法;

我想還需要簡單說明一下libxxx.so的命名規則,沿襲Linux傳統,lib<something>.so是類庫文件名稱的格式,但在Java的System.loadLibrary(" something ")方法中指定庫名稱時,不能包括 前綴—— lib,以及後綴——.so。

準備編寫自己的JNI模塊

你一定想知道如何編寫自己的xxx.so,不過這涉及了太多有關JNI的知識。簡單的說:JNI是Java平臺定義的用於和宿主平臺上的本地代碼進行交互的“Java標準”,它通常有兩個使用場景:1.使用(之前使用c/c++、delphi開發的)遺留代碼;2.爲了更好、更直接地與硬件交互並 獲得更高性能 。你可以通過以下鏈接瞭解JNI的更多資料:

  • Book:JNI Programmer's Guide and Specification
  • JNI之Hello World

    1、首先創建含有native方法的Java類:

    1.	package com.okwap.testjni;     
    2.	 public final class MyJNI {    
    3.	    //native方法,    
    4.	     public static native String sayHello(String name);    
    5.	}   
    

    2、通過javah命令生成.h文件,內容如下(com_okwap_testjni.h文件):

    1.	/* DO NOT EDIT THIS FILE - it is machine generated */     
    2.	 #include <jni.h>      
    3.	 /* Header for class com_okwap_testjni_MyJNI */     
    4.	 #ifndef _Included_com_okwap_testjni_MyJNI      
    5.	 #define _Included_com_okwap_testjni_MyJNI      
    6.	 #ifdef __cplusplus      
    7.	 extern "C" {      
    8.	 #endif      
    9.	 /*     
    10.	 * Class:     com_okwap_testjni_MyJNI     
    11.	  * Method:    sayHello     
    12.	  * Signature: (Ljava/lang/String;)Ljava/lang/String;     
    13.	  */     
    14.	 JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello      
    15.	   (JNIEnv *, jclass, jstring);      
    16.	 #ifdef __cplusplus      
    17.	 }      
    18.	 #endif     
    19.	 #endif  
    

    這是一個標準的C語言頭文件,其中的JNIEXPORT、JNICALL是JNI關鍵字(事實上它是沒有任何內容的宏,僅用於指示性說明),而jint、jstring是JNI環境下對int及java.lang.String類型的映射。這些關鍵字的定義都可以在jni.h中看到。

    3、在 com_okwap_testjni.c文件中實現以上方法:

    1.	#include <string.h>     
    2.	 #include <jni.h>     
    3.	 #include "com_okwap_testjni.h"     
    4.	 JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello(JNIEnv* env, jclass, jstring str){     
    5.	     //從jstring類型取得c語言環境下的char*類型     
    6.	      const char* name = (*env)->GetStringUTFChars(env, str, 0);     
    7.	     //本地常量字符串     
    8.	      char* hello = "你好,";    
    9.	     //動態分配目標字符串空間    
    10.	     char* result = malloc((strlen(name) + strlen(hello) + 1)*sizeof(char));    
    11.	     memset(result,0,sizeof(result));    
    12.	     //字符串鏈接    
    13.	      strcat(result,hello);    
    14.	     strcat(result,name);    
    15.	     //釋放jni分配的內存    
    16.	     (*env)->ReleaseStringUTFChars(env,str,name);    
    17.	     //生成返回值對象    
    18.	     str = (*env)->NewStringUTF(env, "你好 JNI~!");    
    19.	     //釋放動態分配的內存    
    20.	     free(result);    
    21.	    //   
    22.	    return str;    
    23.	 }   
    

    4、編譯——兩種不同的編譯環境

    以上的C語言代碼要編譯成最終.so動態庫文件,有兩種途徑:

    AndroidNDK :全稱是Native DeveloperKit,是用於編譯本地JNI源碼的工具,爲開發人員將本地方法整合到Android應用中提供了方便。事實上NDK和完整源碼編譯環境一樣,都使用Android的編譯系統——即通過Android.mk文件控制編譯。NDK可以運行在Linux、Mac、Window(+cygwin)三個平臺上。有關NDK的使用方法及更多細節請參考以下資料:
     
    eoe特刊第七期《NDK總結》http://blog.eoemobile.com/?p=27

    http://androidappdocs.appspot.com/sdk/ndk/index.html;

    完整源碼編譯環境 :Android平臺提供有基於make的編譯系統,爲App編寫正確的Android.mk文件就可使用該編譯系統。該環境需要通過git從官方網站獲取完整源碼副本併成功編譯,更多細節請參考:http://source.android.com/index.html

    不管你選擇以上兩種方法的哪一個,都必須編寫自己的Android.mk文件,有關該文件的編寫請參考相關文檔。

    JNI組件的入口函數——JNI_OnLoad()、JNI_OnUnload()

    JNI組件被成功加載和卸載時,會進行函數回調,當VM執行到System.loadLibrary(xxx)函數時,首先會去執行JNI組件中的JNI_OnLoad()函數,而當VM釋放該組件時會呼叫JNI_OnUnload()函數。先看示例代碼:

    1.	//onLoad方法,在System.loadLibrary()執行時被調用     
    2.	jint JNI_OnLoad(JavaVM* vm, void* reserved){     
    3.	    LOGI("JNI_OnLoad startup~~!");     
    4.	        return JNI_VERSION_1_4;     
    5.	}        
    6.	     
    7.	//onUnLoad方法,在JNI組件被釋放時調用     
    8.	void JNI_OnUnload(JavaVM* vm, void* reserved){     
    9.	    LOGE("call JNI_OnUnload ~~!!");    
    10.	}   
    

    JNI_OnLoad()有兩個重要的作用:

    指定JNI版本:告訴VM該組件使用那一個JNI版本(若未提供JNI_OnLoad()函數,VM會默認該使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,則必須由JNI_OnLoad()函數返回常量JNI_VERSION_1_4(該常量定義在jni.h中) 來告知VM。

    初始化設定,當VM執行到System.loadLibrary()函數時,會立即先呼叫JNI_OnLoad()方法,因此在該方法中進行各種資源的初始化操作最爲恰當。

    JNI_OnUnload()的作用與JNI_OnLoad()對應,當VM釋放JNI組件時會呼叫它,因此在該方法中進行善後清理,資源釋放的動作最爲合適。

    使用registerNativeMethods方法

    對Java程序員來說,可能我們總是會遵循:1.編寫帶有native方法的Java類;--->2.使用javah命令生成.h頭文件;--->3.編寫代碼實現頭文件中的方法,這樣的“官方”流程,但也許有人無法忍受那“醜陋”的方法名稱,RegisterNatives方法能幫助你把c/c++中的方法隱射到Java中的native方法,而無需遵循特定的方法命名格式。來看一段示例代碼吧:

    1.	//定義目標類名稱     
    2.	static const char *className = "com/okwap/testjni/MyJNI";     
    3.	//定義方法隱射關係    
    4.	static JNINativeMethod methods[] = {     
    5.	  {"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},     
    6.	};     
    7.	jint JNI_OnLoad(JavaVM* vm, void* reserved){    
    8.	  //聲明變量    
    9.	  jint result = JNI_ERR;    
    10.	  JNIEnv* env = NULL;    
    11.	  jclass clazz;    
    12.	 int methodsLenght;    
    13.	  //獲取JNI環境對象    
    14.	  if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {    
    15.	 LOGE("ERROR: GetEnv failed\n");    
    16.	    return JNI_ERR;    
    17.	  }    
    18.	  assert(env != NULL);    
    19.	  //註冊本地方法.Load 目標類    
    20.	  clazz = (*env)->FindClass(env,className);    
    21.	  if (clazz == NULL) {    
    22.	    LOGE("Native registration unable to find class '%s'", className);    
    23.	   return JNI_ERR;    
    24.	  }    
    25.	  //建立方法隱射關係    
    26.	  //取得方法長度    
    27.	  methodsLenght = sizeof(methods) / sizeof(methods[0]);    
    28.	  if ((*env)->RegisterNatives(env,clazz, methods, methodsLenght) < 0) {    
    29.	    LOGE("RegisterNatives failed for '%s'", className);    
    30.	    return JNI_ERR;    
    31.	  }    
    32.	  //    
    33.	 result = JNI_VERSION_1_4;    
    34.	  return result;     
    

    建立c/c++方法和Java方法之間映射關係的關鍵是 JNINativeMethod結構,該結構定義在jni.h中,具體定義如下:

    1.	typedef struct {     
    2.	   const char* name;//java方法名稱    
    3.	   const char* signature; //java方法簽名    
    4.	   void*       fnPtr;//c/c++的函數指針    
    5.	 } JNINativeMethod 
    

    參照上文示例中初始化該結構的代碼:

    1.	//定義方法隱射關係    
    2.	 static JNINativeMethod methods[] = {    
    3.	   {"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},    
    4.	 }; 
    

  • 其中比較難以理解的是第二個參數——signature字段的取值,實際上這些字符與函數的參數類型/返回類型一一對應,其中"()" 中的字符表示參數,後面的則代表返回值。例如"()V" 就表示void func(),"(II)V" 表示 void func(int, int),具體的每一個字符的對應關係如下:

    方法簽名就是一個方法參數和返回值的聲明。A和B兩個部分組成。規則如下(A)BA中是參數的類型,B是返回值類型。

    字符     Java類型     C/C++類型V           void          voidZ         jboolean      booleanI            jint            intJ           jlong          longD         jdouble       doubleF          jfloat          floatB          jbyte          byteC          jchar           charS          jshort         short

    數組則以"["開始,用兩個字符表示:

    字符     java類型          c/c++類型[Z     jbooleanArray      boolean[][I        jintArray            int[][F       jfloatArray         float[][B      jbyteArray          byte[][C      jcharArray          char[][S      jshortArray         short[][D     jdoubleArray       double[][J        jlongArray          long[]

    上面的都是基本類型,如果參數是Java類,則以"L"開頭,以";"結尾,中間是用"/"隔開包及類名,而其對應的C函數的參數則爲jobject,一個例外是String類,它對應C類型jstring,例如:Ljava/lang /String; 、Ljava/net/Socket; 等,如果JAVA函數位於一個嵌入類(也被稱爲內部類),則用$作爲類名間的分隔符,例如:"Landroid/os/FileUtils$FileStatus;"。

    使用registerNativeMethods方法不僅僅是爲了改變那醜陋的長方法名,最重要的是可以提高效率,因爲當Java類別透過VM呼叫到本地函數時,通常是依靠VM去動態尋找.so中的本地函數(因此它們才需要特定規則的命名格式),如果某方法需要連續呼叫很多次,則每次都要尋找一遍,所以使用RegisterNatives將本地函數向VM進行登記,可以讓其更有效率的找到函數。

    registerNativeMethods方法的另一個重要用途是,運行時動態調整本地函數與Java函數值之間的映射關係,只需要多次調用registerNativeMethods()方法,並傳入不同的映射表參數即可。

    JNI中的日誌輸出

    你一定非常熟悉在Java代碼中使用Log.x(TAG,“message”)系列方法,在c/c++代碼中也一樣,不過首先你要include相關頭文件。遺憾的是你使用不同的編譯環境( 請參考上文中兩種編譯環境的介紹) ,對應的頭文件略有不同。。

    如果是在完整源碼編譯環境下,只要include <utils/Log.h>頭文件,就可以使用對應的LOGI、LOGD等方法了,同時請定義LOG_TAG,LOG_NDEBUG等宏值,示例代碼如下:

    /* 
      * jnilogger.h 
      * 
      *  Created on: 2010-11-15 
      *      Author: INC062805 
      */  
       
     #ifndef __JNILOGGER_H_  
     #define __JNILOGGER_H_  
       
     #include <android/log.h>  
       
     #ifdef _cplusplus  
     extern "C" {  
     #endif  
       
     #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__)  
       
       
     #ifdef __cplusplus  
     }  
     #endif  
       
     #endif /* __JNILOGGER_H_ */

    你可以下載以上頭文件,來統一兩種不同環境下的使用差異。另外,不要忘了在你的Android.mk文件中加入對類庫的應用,兩種環境下分別是:

    ifeq ($(HOST_OS),windows)  
     #NDK環境下  
         LOCAL_LDLIBS := -llog  
     else  
     #完整源碼環境下  
         LOCAL_SHARED_LIBRARIES := libutils  
     endif

    Android爲JNI提供的助手方法

    myeclair\dalvik\libnativehelper\include\nativehelper

    在完整源碼編譯環境下,Android在myeclair\dalvik\libnativehelper\include\nativehelper\JNIHelp.h頭文件中 提供了助手函數 ,用於本地方法註冊、異常處理等任務,還有一個用於計算方法隱射表長度的宏定義:

    #ifndef NELEM  
     # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))  
     #endif  
       
     //有了以上宏定義後,註冊方法可以按如下寫,該宏定義可以直接copy到NDK環境下使用:  
     (*env)->RegisterNatives(env,clazz, methods,NELEM(methods));

    更多關於JNI在Android中的信息:

    JNI系列(1):基礎篇

    JNI系列(2):jstring操作

    JN系列(3):如何得到JavaVM,JNIEnv接口

    JNI系列(4):如何訪問自定義類對象




發佈了10 篇原創文章 · 獲贊 3 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章