Android平台JNI調用

來源:互聯網
上載者:User

 最近接觸了Android平台JNI調用,發現網路資料對此沒有從原理到具體實現有一份更為詳細的介紹。所以,參照了一些材料並根據項目開發加入自己的一些體會:

JNI 的由來 JNI是Java Native Interface的縮寫,中文為JAVA本地調用。它允許Java代碼和其他語言寫的代碼進行互動。JNI 是本地編程介面。它使得在 JAVA 虛擬機器 (VM) 內部啟動並執行 Java 代碼能夠與用其它程式設計語言(如 C、C++ 和組合語言)編寫的應用程式和庫進行互操作。
使用JNI的原因 1.平台相關性 標準JAVA庫不支援一些平台特性,你可以用別的語言,編寫代碼使得你的軟體支援這些平台特性,例如對IPC(Internet Process Connection )機制不支援(訊息佇列、共用記憶體、訊號量等)。
2.為提高效率,可能需要用低級語言編寫一些演算法以提高程式的效能。例如Java資料庫訪問和socket通訊的效率低。
3.Android 的Java層級是外殼架構,大部分的android本身的系統控制項都在Native層(C/C++),與Native層相關問題的解決。
4.欲在Android平台的java層利用原先已經用C/C++寫的庫檔案。 5.需要保密的應用邏輯使用C開發。畢竟,Java包都是可以反編譯的 。

 

JNI在Android的層級關係圖: Android平台利用java調用C/C++ 一、編寫帶有native聲明的方法的java類1.定義帶有native關鍵詞的java的類方法(method),且不能在java中實現;   package sam.test.jnitest; public class JniTest extends Activity {     /** Called when the activity is first created. */     @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);     }         public native void test();     public native String  stringcat(String str1,String str2);     public native int  reduce(int x,int y); }  2.在java代碼中load用C/C++實現的庫檔案,例如在java類中加入:                      static {                              System.loadLibrary("jni-test");                        }      load一個名為jni-test的C/C++庫,沒有尾碼,以便在不同操作系 統間相容(誇平台性),因為各個平台的庫的尾碼不同,例如libjni-test.so或libjni-test.dll;3.在java中調用此native方法(與調用其他類方法相同)。 二、編譯成class檔案首先確保已下載安裝JDK,並配置JDK的系統內容變數 1.一種方法是通過javac編譯,可以通過命令列中的 javac JniTest.java進行編譯: E:\workspace\JniTest\src>javac –classpath E:\Working\Android \SDK\android-sdk-windows\platforms\android-6/android.jar sam /test/jnitest/jnitest.java最終在jnitest.java的目錄產生jnitest.class檔案 另外,可以利用-classpath 選項加入類檔案所依賴的jar包,此例中JniTest繼承Activity 類,javac編譯過程需加入android.jar包。2.另一種利用Eclipse編譯。可以直接在Eclipse項目下的bin目錄中找到編譯後的JniTest.class檔案. 三、使用javah命令產生副檔名為h的標頭檔1.處理javac產生的class檔案:      E:\workspace\JniTest\src>javah sam.test.jnitest.JniTest 2. 處理Eclipse編譯成的class檔案      E:\workspace\JniTest\src>javah -classpath ../bin     sam.test.jnitest.JniTest 3.注意類要包含包名,上例中類名是JniTest,包名是sam.test.jnitest。 4.路徑檔案夾下要包含所有包中的類,否則會報找不到類的錯誤。上例中對於javac編譯的情況,工作目錄是workspace\JniTest\src,該路徑已包含類sam.test.jnitest(在Java 中包的階層類似於檔案夾的階層, 類sam.test.jnitest對應的目錄為sam/test/jnitest)5.classpath參數指定到包名前一級檔案夾。    對於Eclipse編譯的情況,指定bin為包名的前一級目錄 最終,產生sam_test_jnitest_JniTest.h檔案定義了從java語言映射到C/C++語言的native函數: JNIEXPORT void JNICALL Java_sam_test_jnitest_JniTest_play   (JNIEnv *, jobject); JNIEXPORT jstring JNICALL Java_sam_test_jnitest_JniTest_stringcat   (JNIEnv *, jobject, jstring, jstring); JNIEXPORT jint JNICALL Java_sam_test_jnitest_JniTest_reduce   (JNIEnv *, jobject, jint, jint); 函數的參數如下: JNIEnv *:JNI環境的指標,虛擬機器中當前線程的一個控制代碼,包含了映射資訊及其它操作資訊 (A pointer to the JNI environment. This pointer is a handle to the current thread in the Java virtual machine, and contains mapping and other hosuekeeping information ) 。VM是多線程執行環境,每個線程在調用JNI函數是傳入進來的線程ID都不同。 Jobject:調用該本地代碼的函數引用。(A reference to the method that called this native code ),可以由此參數並結合JNIEnv*得到調用該函數對應的java類。JNIEnv * Jobject之後參數 :與java代碼調用native函數時具體傳入的參數對應。 從產生的標頭檔可以看出JNI的一些文法規則,例如註冊的native函數總是以Java_開頭,後面跟包名_類名,最後是函數名。 詳細的規則內容可參考官方文檔: http://download.oracle.com/docs/cd/E17476_01/javase/1.4.2/docs/guide/jni/spec/jniTOC.html 四、使用C/C++實現本地方法1.根據步驟3產生的.h檔案中對C/C++的函式宣告,補充具體方法 以hello.c為例: JNIEXPORT jstring JNICALL Java_sam_test_jnitest_JniTest_stringcat   (JNIEnv *env, jobject thiz, jstring jstrSrc, jstring jstrDes)   {     char buffer[512];     android_log_print(ANDROID_LOG_INFO, "JniTest", "stringcat Begin......");    const char* pSrc; = (*env)->GetStringUTFChars(env,jstrSrc, NULL);     if(pSrc == NULL)         return NULL;     const char* pDes = (*env)->GetStringUTFChars(env,jstrDes, NULL);     if(pDes == NULL)         return NULL; strcpy(buffer,pSrc);     strcat(buffer,pDes);         (*env)->ReleaseStringUTFChars(env,jstrSrc, pSrc);     (*env)->ReleaseStringUTFChars(env,jstrDes, pDes);         return (*env)->NewStringUTF(env, buffer); } JNIEXPORT jint JNICALL Java_sam_test_jnitest_JniTest_reduce   (JNIEnv *env, jobject, jint i, jint j)   {   return i-j;   } 如果是用C++寫的檔案,我們可以用更簡潔的方式實現此部分:  extern “C” JNIEXPORT jstring JNICALL Java_sam_test_jnitest_JniTest_stringcat  (JNIEnv *env, jobject thiz, jstring jstrSrc, jstring jstrDes)   {     …     const char* pSrc; = env->GetStringUTFChars(jstrSrc, NULL);     ... return env->NewStringUTF(buffer);    } 2. 添加入口函數JNI_OnLoad() 當Android的VM(Virtual Machine)執行到System.loadLibrary()函數時,首先會去執行C組件中的JNI_OnLoad()函數。它的用途有二:•告訴VM此C組件使用那一個JNI版本。如果你的*.so檔沒有提供JNI_OnLoad()函數,VM會預設該*.so檔是使用最老的JNI 1.1版本。由於新版的JNI做了許多擴充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer,就必須由JNI_OnLoad()函數來告訴VM。•由於VM執行到System.loadLibrary()函數時,就會立即先調用JNI_OnLoad(),所以C組件的開發人員可以在JNI_OnLoad()中進行C組件內的初期值設定(Initialization)1).建立jni與JniTest類的映射表static JNINativeMethod gJniTestMethods[] = {     /* name, signature, funcPtr */     {"test","()V",     (void*)Java_sam_test_jnitest_JniTest_test},     {" stringcat","(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",    (void*)Java_sam_test_jnitest_JniTest_stringcat},     {"reduce","(II)I",     (void*)Java_sam_test_jnitest_JniTest_reduce}, }; Andoird 中使用了一種不同傳統Java JNI的方式來定義其native的函數。其中很重要的區別是Andorid使用了一種Java 和 C 函數的映射表數組,並在其中描述了函數的參數和傳回值。這個數組的類型是JNINativeMethod,定義如下:typedef struct { const char* name;            /*Java中函數的名字*/          const char* signature;      /*描述了函數的參數和傳回值*/ void* fnPtr;               /*函數指標,指向C函數*/ } JNINativeMethod; 第一個參數對應java中函數名字,其中比較難以理解的是第二個參數,例如 "()V" "(II)V" "(Ljava/lang/String;Ljava/lang/String;)V" 實際上這些字元是與函數的參數類型一一對應的。 “()” 中的字元表示參數,後面的則代表傳回值。例如"()V" 就表示void Func(); "(II)V" 表示 void Func(int, int); 具體的每一個字元的對應關係如下 字元    Java類型      C類型 V       void         void Z      jboolean     boolean I       jint         int J        jlong        long D      jdouble       double F      jfloat            float B      jbyte            byte C      jchar           char S      jshort          short 數組則以"["開始,用兩個字元表示 [I     jintArray       int[] [F     jfloatArray     float[] [B     jbyteArray     byte[] [C    jcharArray      char[] [S    jshortArray      short[] [D    jdoubleArray    double[] [J     jlongArray      long[] [Z    jbooleanArray    boolean[] 上面的都是基本類型。如果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" 2).為類中不同的native函數註冊 •自訂registerNativeMethods函數: static int registerNativeMethods(JNIEnv* env, const char* className,     JNINativeMethod* gMethods, int numMethods) {     jclass clazz;     clazz = (*env)->FindClass(env, className);     if (clazz == NULL)         return  0;     if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)         return 0;     return 1; } 這種做法一般用於NDK(Native Develepment kit,參見5.1)編譯的code,在NDK中registerNativeMethods介面目前還沒有開放出來,所以需要自訂實現,該部分code與Android 源碼中內建的registerNativeMethods的code一致,具體可參考..\dalvik\libnativehelper\JNIHelp.c中jniRegisterNativeMethods的實現。•如果源碼是在Android 源碼中編譯,則可以包含AndroidRuntime.h後直接調用AndroidRuntime::registerNativeMethods函數進行註冊。•RegisterNatives函數的作用: 應用程式層級的Java程式調用本地函數時,通過虛擬機器尋找庫檔案中的本地函數。如果某函數需要頻繁調用,每一次調用都尋找一遍,會花很多不必要的時間,那麼,開發人員可以自行將本地函數向虛擬機器進行註冊,以達到更有效率的尋找函數。3).調用JNI_OnLoad,返回JNI 版本號碼虛擬機器 jint JNI_OnLoad(JavaVM* vm, void* reserved) {     JNIEnv* env = NULL;     __android_log_print(ANDROID_LOG_INFO, "JniTest", "JNI_OnLoad......");     if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)  return JNI_FALSE;     if (!registerNativeMethods(env, "sam/test/jnitest/JniTest",             gJniTestMethods, sizeof(gJniTestMethods) / sizeof(gJniTestMethods[0])))        return JNI_FALSE; return JNI_VERSION_1_4; } 4). 根據需要添加JNI_Unload 。 JNI_Unload 與JNI_Load 對應,當虛擬機器釋放該c組件時,會調用 JNI_Unload 做一些善後清理工作。 五、編譯C/C++源檔案成庫檔案1. 利用NDK進行編譯 1).  NDK介紹 •NDK全稱是Native Development Kit ,它提供了一系列的工具,協助開發人員快速開發C(或C++)的動態庫,並能自動將so庫檔案和java應用一起打包成apk。•NDK整合了交叉編譯器,並提供了相應的mk檔案隔離CPU、平台、ABI等差異,開發人員只需要簡單修改mk檔案(指出“哪些檔案需要編譯”、“編譯特性要求”等),就可以建立出so。•NDK提供了一份穩定、功能有限的API標頭檔聲明 ,這些API支援的功能非常有限,包含有:C標準庫(libc)、標準數學庫(libm)、壓縮庫(libz)、Log庫(liblog)。2). 環境配置 Windows: a).下載android-ndk-r4-windows.zip。 b).安裝cygwin 1.7以上版本,一個類比的linux環境,安裝需要的組件。成功之後,配置環境變數: 在windows安裝目錄中修改 home\<你的使用者名稱>\.bash_profile 檔案,添加環境變數 NDK=/cygdrive/<你的盤符>/<android ndk 目錄> 例如:NDK=/cygdrive/d/android/android-ndk-r4-windows

export NDK 其中"NDK"這個名字隨便起,因為後面要用經常使用,建議不要太長。 重啟cygwin,輸入cd $NDK可以進入對應目錄,就成功了 c). 確保系統已經安裝JDK 5以上版本。Linux: 在Windows環境也可安裝VMWare,類比帶圖形介面的Linux開發環境,它的環境配置與下相同: a). 下載NDK開發包(例如android-ndk-1.6_r1-linux-x86.zip)。 b).  終端中運行: gedit ~/.bashrc 在開啟的設定檔中為目前使用者添加環境變數: NDK=<android ndk 目錄> 例如: NDK=/home/Android_ndk_1.6/android-ndk-1.6_r1
export NDK c).  確保系統已經安裝JDK 5以上版本。 3).編輯編譯指令碼 在android項目目錄下建立jni目錄,將C/C++源檔案複製到該目錄,產生並編輯Android.mk: LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE    := jni-test LOCAL_SRC_FILES := hell.c LOCAL_LDLIBS :=-llog include $(BUILD_SHARED_LIBRARY) 指定模組名稱(jni-test), 需編譯的檔案(hell.c), 編譯方式(動態連結程式庫),以及根據需要添加程式依賴的庫檔案(例如-llog使用android 列印輸出資訊)。4).編譯 (windows環境需開啟cygwin)cd至項目目錄,運行$NDK/ndk-build, NDK編譯並產生庫檔案(libjni-hello.so), 可到項目目錄libs/armeabi驗證庫檔案的存在。5).運行 直接在模擬器或者裝置運行編譯成功後的.apk程式。 利用NDK編譯成的.apk檔案已經打包了.so檔案,如果不想自動打包,可將android工程目錄libs/armeabi下的.so庫刪除並編譯產生一個未打包庫檔案的apk,然後手動啟動adb, 將之前的.so庫檔案push到模擬器或裝置的system/libs目錄下。但是,模擬器或者裝置的system/lib目錄預設是read-only的,必須首先改變它的屬性為可寫,才能利用adb將庫檔案push到system/lib目錄。首先進入adb shell,運行mount查看檔案系統: ... /dev/block/mtdblock0 /system yaffs2 ro 0 0/dev/block/mtdblock1 /data yaffs2 rw,nosuid,nodev 0 0 ... 可以看到,/system是掛靠在/dev/block/mtdblock0(不同裝置可能不同),參照紅線部分,運行:# mount -o remount -rw /dev/block/mtdblock0 system即可改變系統目錄屬性。 2. 在Android 源碼環境下編譯 1).  將需要編譯的C/C++源檔案添加至android源碼目錄,例如添加hello.c到Android_SDK/frameworks/base/JniTest2).  修改android.mk檔案 LOCAL_MODULE    := libjni-test LOCAL_SRC_FILES := hello.c #LOCAL_PRELINK_MODULE := false LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) LOCAL_SHARED_LIBRARIES := libcutils #LOCAL_LDLIBS :=-llog include $(BUILD_SHARED_LIBRARY) •指定動態庫是否需要提前添加映射資訊。Android系統為動態庫提供了一種映射模式,在該模式下能以更快的方式載入庫檔案。 具體需修改build/core/prelink-linux-arm.map中的資訊,指定動態庫的地址,例如 libjni-test.so      0x9A000000 如果不需要進行地址映射,需要在android.mk中做如下設定: LOCAL_PRELINK_MODULE := false •添加編譯需要的JNI標頭檔路徑及其它所依賴的動態庫 LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) LOCAL_SHARED_LIBRARIES := libcutils(列印log資訊) 3). 修改C/C++源碼 可以包含AndroidRuntime.h後直接調用AndroidRuntime::registerNativeMethods進行函數註冊以及使用android內部logcat進行資訊輸出等。4). 編譯android整個源碼,如果編譯成功,則可以在out/target/product/generic/system/lib 下找到編譯成功的libjni-test.so檔案 5). 運行 將編譯成功的system.img替換模擬器或者裝置使用的system.img 例如模擬器替換路徑:android-sdk-windows\platforms\android-7\images)  利用java調用C/C++流程總結:圖2  利用C/C++調用java在android庫檔案中,實現C/C++調用java,有以下步驟: 一、擷取指定對象的類定義(jclass) 有兩種途徑來擷取對象的類定義: 1.  在已知類名的情況下使用FindClass來尋找對應的類。但是要注意類名並不同於平時寫的Java代碼,例如要得到類jni.test.Demo的定義必須調用如下代碼  ://把點號換成斜杠 jclass cls = (*env)->FindClass(env, "sam/test/jnitest/JniTest"); 2. 通過傳入的參數對象直接得到其所對應的類定義  //其中obj是要引用的對象, 類型是jobject jclass cls = (*env)-> GetObjectClass(env, obj); 二、讀取要調用方法的定義(jmethodID) 我們先來看看Android jni.h中擷取方法定義的函數: jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);這兩個函數的區別在於GetStaticMethodID是用來擷取靜態方法的定義,GetMethodID則是擷取非靜態方法定義。 env就是JNI環境;第二個參數class是對象的類定義;第三個參數是方法名稱;第四個參數,是方法的定義,方法定義的規則可以參照前面章節【建立jni與JniTest類的映射表】中的介紹。•/* 假設我們已經有一個 sam.test.jnitest. JniTest的執行個體obj */   //擷取執行個體的類定義   jclass Javacls = (*env)-> GetObjectClass (env, obj);   jmethodID mid=(*env)->GetMethodID (env, Javacls, "GetJavaMessage", "()Ljava/lang/String; ");if(mid ==0)return; 三、調用對象方法和屬性 1.調用對象方法。 為了調用對象的某個方法,可以使用函數CallxxxxMethod或者CallStaticxxxxMethod(訪問類的靜態方法),根據不同的傳回型別而定。例如:CallIntMethod,CallCharMethod,CallStaticVoidMethod 在該例中調用一個返回string類型的方法,調用如下: jstring msg = (*env)-> CallObjectMethod(env, obj, mid); /* 如果該方法是靜態方法,只需要將 最後一句代碼改為以下寫法: jstring msg = (*env)-> CallStaticObjectMethod (env, Javacls , mid); */2.讀取和設定屬性值

訪問類的屬性與訪問類的方法大體上是一致的,只不過是把方法變成屬性而已,有幾個方法用來讀取和設定類的屬性,它們是:

     GetField、SetField、GetStaticField、SetStaticField。比如讀取JniTest類的strMsg屬性就可以用GetObjectField,相關代碼如下 :  jclass Javacls = (*env)->GetObjectClass (env, obj);          jfieldID field = (*env)->GetFieldID(env,Javacls,"strMsg", "Ljava/lang/String;");jstring msgField = (*env)->GetObjectField(env, obj, field); 也可以改變類的屬性: (* env)->SetObjectField( env, obj, field,(* env)->NewStringUTF( env, "Changed to C string")); 四、處理異常 C/C++中調用Java時,注意捕獲並處理Java方法拋出的異常資訊。 異常應在每個方法調用後檢查:   msg = (jstring)env->CallObjectMethod(obj, mid);        if (env->ExceptionOccurred())        {            env->ExceptionDescribe();                      env->ExceptionClear();            return;         }   結束 
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.