Android中JNI編程的那些事兒
Android系統(tǒng)不允許一個純粹使用C/C++的程序出現(xiàn),它要求必須是通過Java代碼嵌入Native C/C++——即通過JNI的方式來使用本地(Native)代碼。因此JNI對Android底層開發(fā)人員非常重要。
如何將.so文件打包到.APK
讓我們 先 從最簡單的情況開始,假如已有一個JNI實現(xiàn)——libxxx.so文件,那么如何在APK中使用它呢?
在我最初寫類似程序的時候,我會將libxxx.so文件push到/system/lib/目錄下,然后在Java代碼中執(zhí)行System.loadLibrary(xxx),這是個可行的做法,但需要取得/system/lib 目錄 的寫權限(模擬器通過adb remount取得該權限)。但模擬器 重啟之 后libxxx.so文件會消失。現(xiàn)在 我找到了更好的方法,把.so文件打包到apk中分發(fā)給最終用戶,不管是模擬器 或者 真機 ,都不再需要system分區(qū)的寫權限。實現(xiàn)步驟如下:
1、在你的項目根目錄下建立libs/armeabi目錄;
2、將libxxx.so文件copy到 libs/armeabi/下;
3、此時ADT插件自動編譯輸出的.apk文件中已經(jīng)包括.so文件了;
4、安裝APK文件,即可直接使用JNI中的方法;
我想還需要簡單說明一下libxxx.so的命名規(guī)則,沿襲Linux傳統(tǒng),lib<something>.so是類庫文件名稱的格式,但在Java的System.loadLibrary(" something ")方法中指定庫名稱時,不能包括 前綴—— lib,以及后綴——.so。
準備編寫自己的JNI模塊
你一定想知道如何編寫自己的xxx.so,不過這涉及了太多有關JNI的知識。簡單的說:JNI是Java平臺定義的用于和宿主平臺上的本地代碼進行交互的“Java標準”,它通常有兩個使用場景:1.使用(之前使用c/c++、delphi開發(fā)的)遺留代碼;2.為了更好、更直接地與硬件交互并獲得更高性能 。
1、首先創(chuàng)建含有native方法的Java類:
- package com.okwap.testjni;
- public final class MyJNI {
- //native方法,
- public static native String sayHello(String name);
- }
2、通過javah命令生成.h文件,內(nèi)容如下(com_okwap_testjni.h文件):
- /* DO NOT EDIT THIS FILE - it is machine generated */
- #include <jni.h>
- /* Header for class com_okwap_testjni_MyJNI */
- #ifndef _Included_com_okwap_testjni_MyJNI
- #define _Included_com_okwap_testjni_MyJNI
- #ifdef __cplusplus
- extern "C" {
- #endif
- /*
- * Class: com_okwap_testjni_MyJNI
- * Method: sayHello
- * Signature: (Ljava/lang/String;)Ljava/lang/String;
- */
- JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello
- (JNIEnv *, jclass, jstring);
- #ifdef __cplusplus
- }
- #endif
- #endif
這是一個標準的C語言頭文件,其中的JNIEXPORT、JNICALL是JNI關鍵字(事實上它是沒有任何內(nèi)容的宏,僅用于指示性說明),而jint、jstring是JNI環(huán)境下對int及java.lang.String類型的映射。這些關鍵字的定義都可以在jni.h中看到。
3、在 com_okwap_testjni.c文件中實現(xiàn)以上方法:
- #include <string.h>
- #include <jni.h>
- #include "com_okwap_testjni.h"
- JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello(JNIEnv* env, jclass, jstring str){
- //從jstring類型取得c語言環(huán)境下的char*類型
- const char* name = (*env)->GetStringUTFChars(env, str, 0);
- //本地常量字符串
- char* hello = "你好,";
- //動態(tài)分配目標字符串空間
- char* result = malloc((strlen(name) + strlen(hello) + 1)*sizeof(char));
- memset(result,0,sizeof(result));
- //字符串鏈接
- strcat(result,hello);
- strcat(result,name);
- //釋放jni分配的內(nèi)存
- (*env)->ReleaseStringUTFChars(env,str,name);
- //生成返回值對象
- str = (*env)->NewStringUTF(env, "你好 JNI~!");
- //釋放動態(tài)分配的內(nèi)存
- free(result);
- //
- return str;
- }
#p#
4、編譯——兩種不同的編譯環(huán)境
以上的C語言代碼要編譯成最終.so動態(tài)庫文件,有兩種途徑:
Android NDK :全稱是Native Developer Kit,是用于編譯本地JNI源碼的工具,為開發(fā)人員將本地方法整合到Android應用中提供了方便。事實上NDK和完整源碼編譯環(huán)境一樣,都使用Android的編譯系統(tǒng)——即通過Android.mk文件控制編譯。NDK可以運行在Linux、Mac、Window(+cygwin)三個平臺上。有關NDK的使用方法及更多細節(jié)請參考以下資料:
eoe特刊第七期《NDK總結(jié)》http://blog.eoemobile.com/?p=27
http://androidappdocs.appspot.com/sdk/ndk/index.html ;
完整源碼編譯環(huán)境 :Android平臺提供有基于make的編譯系統(tǒng),為App編寫正確的Android.mk文件就可使用該編譯系統(tǒng)。該環(huán)境需要通過git從官方網(wǎng)站獲取完整源碼副本并成功編譯,更多細節(jié)請參考:http://source.android.com/index.html
不管你選擇以上兩種方法的哪一個,都必須編寫自己的Android.mk文件,有關該文件的編寫請參考相關文檔。
JNI組件的入口函數(shù)——JNI_OnLoad()、JNI_OnUnload()
JNI組件被成功加載和卸載時,會進行函數(shù)回調(diào),當VM執(zhí)行到System.loadLibrary(xxx)函數(shù)時,首先會去執(zhí)行JNI組件中的JNI_OnLoad()函數(shù),而當VM釋放該組件時會呼叫JNI_OnUnload()函數(shù)。先看示例代碼:
- //onLoad方法,在System.loadLibrary()執(zhí)行時被調(diào)用
- jint JNI_OnLoad(JavaVM* vm, void* reserved){
- LOGI("JNI_OnLoad startup~~!");
- return JNI_VERSION_1_4;
- }
- //onUnLoad方法,在JNI組件被釋放時調(diào)用
- void JNI_OnUnload(JavaVM* vm, void* reserved){
- LOGE("call JNI_OnUnload ~~!!");
- }
JNI_OnLoad()有兩個重要的作用:
指定JNI版本:告訴VM該組件使用那一個JNI版本(若未提供JNI_OnLoad()函數(shù),VM會默認該使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,則必須由JNI_OnLoad()函數(shù)返回常量JNI_VERSION_1_4(該常量定義在jni.h中) 來告知VM。
初始化設定,當VM執(zhí)行到System.loadLibrary()函數(shù)時,會立即先呼叫JNI_OnLoad()方法,因此在該方法中進行各種資源的初始化操作最為恰當。
JNI_OnUnload()的作用與JNI_OnLoad()對應,當VM釋放JNI組件時會呼叫它,因此在該方法中進行善后清理,資源釋放的動作最為合適。
使用registerNativeMethods方法
對Java程序員來說,可能我們總是會遵循:1.編寫帶有native方法的Java類;--->2.使用javah命令生成.h頭文件;--->3.編寫代碼實現(xiàn)頭文件中的方法,這樣的“官方” 流程,但也許有人無法忍受那“丑陋”的方法名稱,RegisterNatives方法能幫助你把c/c++中的方法隱射到Java中的native方法,而無需遵循特定的方法命名格式。來看一段示例代碼吧:
- //定義目標類名稱
- static const char *className = "com/okwap/testjni/MyJNI";
- //定義方法隱射關系
- static JNINativeMethod methods[] = {
- {"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},
- };
- jint JNI_OnLoad(JavaVM* vm, void* reserved){
- //聲明變量
- jint result = JNI_ERR;
- JNIEnv* env = NULL;
- jclass clazz;
- int methodsLenght;
- //獲取JNI環(huán)境對象
- if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
- LOGE("ERROR: GetEnv failed\n");
- return JNI_ERR;
- }
- assert(env != NULL);
- //注冊本地方法.Load 目標類
- clazz = (*env)->FindClass(env,className);
- if (clazz == NULL) {
- LOGE("Native registration unable to find class '%s'", className);
- return JNI_ERR;
- }
- //建立方法隱射關系
- //取得方法長度
- methodsLenght = sizeof(methods) / sizeof(methods[0]);
- if ((*env)->RegisterNatives(env,clazz, methods, methodsLenght) < 0) {
- LOGE("RegisterNatives failed for '%s'", className);
- return JNI_ERR;
- }
- //
- result = JNI_VERSION_1_4;
- return result;
- }
#p#
建立c/c++方法和Java方法之間映射關系的關鍵是 JNINativeMethod 結(jié)構,該結(jié)構定義在jni.h中,具體定義如下:
- typedef struct {
- const char* name;//java方法名稱
- const char* signature; //java方法簽名
- void* fnPtr;//c/c++的函數(shù)指針
- } JNINativeMethod
參照上文示例中初始化該結(jié)構的代碼:
- //定義方法隱射關系
- static JNINativeMethod methods[] = {
- {"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},
- };
其中比較難以理解的是第二個參數(shù)——signature字段的取值,實際上這些字符與函數(shù)的參數(shù)類型/返回類型一一對應,其中"()" 中的字符表示參數(shù),后面的則代表返回值。例如"()V" 就表示void func(),"(II)V" 表示 void func(int, int),具體的每一個字符的對應關系如下:
字符 Java類型 C/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
數(shù)組則以"["開始,用兩個字符表示:
字符 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[]
上面的都是基本類型,如果參數(shù)是Java類,則以"L"開頭,以";"結(jié)尾,中間是用"/"隔開包及類名,而其對應的C函數(shù)的參數(shù)則為jobject,一個例外是String類,它對應C類型jstring,例如:Ljava/lang /String; 、Ljava/net/Socket; 等,如果JAVA函數(shù)位于一個嵌入類(也被稱為內(nèi)部類),則用$作為類名間的分隔符,例如:"Landroid/os/FileUtils$FileStatus;"。
使用registerNativeMethods方法不僅僅是為了改變那丑陋的長方法名,最重要的是可以提高效率,因為當Java類別透過VM呼叫到本地函數(shù)時,通常是依靠VM去動態(tài)尋找.so中的本地函數(shù)(因此它們才需要特定規(guī)則的命名格式),如果某方法需要連續(xù)呼叫很多次,則每次都要尋找一遍,所以使用RegisterNatives將本地函數(shù)向VM進行登記,可以讓其更有效率的找到函數(shù)。
registerNativeMethods方法的另一個重要用途是,運行時動態(tài)調(diào)整本地函數(shù)與Java函數(shù)值之間的映射關系,只需要多次調(diào)用registerNativeMethods()方法,并傳入不同的映射表參數(shù)即可。
JNI中的日志輸出
你一定非常熟悉在Java代碼中使用Log.x(TAG,“message”)系列方法,在c/c++代碼中也一樣,不過首先你要include相關頭文件。遺憾的是你使用不同的編譯環(huán)境( 請參考上文中兩種編譯環(huán)境的介紹) ,對應的頭文件略有不同。。
如果是在完整源碼編譯環(huán)境下,只要include <utils/Log.h>頭文件,就可以使用對應的LOGI、LOGD等方法了,同時請定義LOG_TAG,LOG_NDEBUG等宏值,示例代碼如下:
- #define LOG_TAG "HelloJni"
- #define LOG_NDEBUG 0
- #define LOG_NIDEBUG 0
- #define LOG_NDDEBUG 0
- #include <string.h>
- #include <jni.h>
- #include <utils/Log.h>
- jstring Java_com_inc_android_ime_HelloJni_stringFromJNI(JNIEnv* env,jobject thiz){
- LOGI("Call stringFromJNI!\n");
- return (*env)->NewStringUTF(env, "Hello from JNI (中文)!");
- }
與日志相關的.h頭文件,在以下源碼路徑:
- myeclair\frameworks\base\include\utils\Log.h
- myeclair\system\core\include\cutils\log.h
如果你是在NDK環(huán)境下編譯,則需要#include <android/log.h>,示例代碼如下:
- /*
- * 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_ */
你可以下載以上頭文件,來統(tǒng)一兩種不同環(huán)境下的使用差異。另外,不要忘了在你的Android.mk文件中加入對類庫的應用,兩種環(huán)境下分別是
ifeq ($(HOST_OS),windows)
#NDK環(huán)境下
LOCAL_LDLIBS := -llog
else
#完整源碼環(huán)境下
LOCAL_SHARED_LIBRARIES := libutils
endif
Android為JNI提供的助手方法
- myeclair\dalvik\libnativehelper\include\nativehelper
在完整源碼編譯環(huán)境下,Android在myeclair\dalvik\libnativehelper\include\nativehelper\JNIHelp.h頭文件中 提供了助手函數(shù) ,用于本地方法注冊、異常處理等任務,還有一個用于計算方法隱射表長度的宏定義:
- #ifndef NELEM
- # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
- #endif
- //有了以上宏定義后,注冊方法可以按如下寫,該宏定義可以直接copy到NDK環(huán)境下使用:
- (*env)->RegisterNatives(env,clazz, methods,NELEM(methods));
【編輯推薦】