歡迎光臨
每天分享高質量文章

Java 中 JNI 的使用 ( 下 )

(點選上方公眾號,可快速關註)


來源:Young_Blog ,

landerlyoung.github.io/blog/2014/10/16/java-zhong-jnide-shi-yong/

陣列的操作

陣列是一個很常用的資料型別,在但是在 JNI 中並不能直接操作 jni 陣列(比如 jshortArray、jfloatArray)。使用方法是:

  1. 獲取陣列長度:jsize GetArrayLength(jarray array)

  2. 建立新陣列: ArrayType NewArray(jsize length);

  3. 透過JNI陣列獲取一個C/C++陣列:* GetArrayElements(jshortArray array, jboolean *isCopy)

  4. 指定原陣列的範圍獲取一個C/C++陣列(該方法只針對於原始資料陣列,不包括Object陣列):void GetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);

  5. 設定陣列元素:void SetArrayRegion(jshortArray array, jsize start, jsize len,const *buf)。again,如果是Object陣列需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);

  6. 使用完之後,釋放陣列:void ReleaseArrayElements(jshortArray array, jshort *elems, jint mode)

有點要說明的:

1、上面的3中的 isCopy:當你呼叫 getArrayElements 時 JVM(Runtime)可以直接傳回陣列的原始指標,或者是 copy 一份,傳回給你,這是由 JVM 決定的。所以 isCopy 就是用來記錄這個的。他的值是 JNI_TURE 或者 JNI_FALSE。

2、6釋放陣列。一定要釋放你所獲得陣列。其中有一個mode引數,其有三個可選值,分別表示:

  • 0

  • 原始陣列:允許原陣列被垃圾回收。

  • copy: 資料會從get傳回的buffer copy回去,同時buffer也會被釋放。

  • JNI_COMMIT

  • 原始陣列:什麼也不做

  • copy: 資料會從get傳回的buffer copy回去,同時buffer不會被釋放。

  • JNI_ABORT

  • 原始陣列:允許原陣列被垃圾回收。之前由JNI_COMMIT提交的對陣列的修改將得以保留。

  • copy: buffer會被釋放,同時buffer中的修改將不會copy回陣列!

關於取用與垃圾回收

比如上面有個方法傳了一個 jobject 進來,然後我把她儲存下來,方便以後使用。這樣做是不行噠!因為他是一個 LocalReference,所以不能保證 jobject 指向的真正的實體不被回收。也就是說有可能你用的時候那個指標已經是個野指標的。然後你的程式就直接 Segment Fault 了,呵呵。

在JNI中提供了三種型別的取用:

  1. Local Reference:即本地取用。在JNI層的函式,所有非全域性取用物件都是Local Reference, 它包括函式呼叫是傳入的jobject和JNI成函式建立的jobject。Local Reference的特點是一旦JNI層的函式傳回,這些jobject就可能被垃圾回收。

  2. Glocal Reference:全域性取用,這些物件不會主動釋放,永遠不會被垃圾回收。

  3. Weak Glocal Reference:弱全域性取用,一種特殊的Global Reference,在執行過程中有可能被垃圾回收。所以使用之前需要使用jboolean IsSameObject(jobject obj1, jobject obj2)判斷它是否已被回收。

Glocal Reference:

  • 建立:jobject NewGlobalRef(jobject lobj);

  • 釋放:void DeleteGlobalRef(jobject gref);

Local Reference:

LocalReference也有一個釋放的函式:void DeleteLocalRef(jobject obj),他會立即釋放Local Reference。 這個方法可能略顯多餘,其實也是有它的用處的。剛才說Local Reference會再函式傳回後釋放掉,但是假如函式傳回前就有很多取用佔了很多記憶體,最好函式內就儘早釋放不必要的記憶體。

關於JNI_OnLoad

開頭提到 JNI_OnLoad 是 Java1.2 中新增加的方法,對應的還有一個 JNI_OnUnload,分別是動態庫被 JVM 載入、解除安裝的時候呼叫的函式。有點類似於 Windows 裡的 DllMain。

前面提到的實現對應 native 的方法是實現 javah 生成的頭檔案中定義的方法,這樣有幾個弊端:

  1. 函式名太長。很長,相當長。

  2. 函式會被匯出,也就誰說可以在動態庫的匯出函式表裡面找到這些函式。這將有利於別人對動態庫的逆向工程,因此帶來安全問題。

現在有了JNI_OnLoad,情況好多了。你不光能在其中完成動態註冊 native 函式的工作還可以完成一些初始化工作。Java 對應的有了 jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)函式。引數分別是:

  • jclass clazz,於native層對應的java class

  • const JNINativeMethod *methods這是一個陣列,陣列的元素是JNI定義的一個結構體JNINativeMethod

  • 上面的陣列的長度

JNINativeMethod:程式碼中的定義如下

/*

 * used in RegisterNatives to describe native method name, signature,

 * and function pointer.

 */

 

typedef struct {

    char *name;

    char *signature;

    void *fnPtr;

} JNINativeMethod;

所以他有三個欄位,分別是

於是現在你可以不用匯出 native 函式了,而且可以隨意給函式命名,唯一要保證的是引數及傳回值的統一。然後需要一個 const JNINativeMethod *methods 陣列來完成對映工作。

看起來大概是這樣的:

//只需匯出JNI_OnLoad和JNI_OnUnload(這個函式不實現也行)

/**

 * These are the exported function in this library.

*/

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);

 

//為了在動態庫中不用匯出函式,全部宣告為static

//native methods registered by JNI_OnLoad

static jint native_newInstance (JNIEnv *env, jclass);

 

//實現native方法

/*

* Class:     com_young_soundtouch_SoundTouch

* Method:    native_newInstance

* Signature: ()I

*/

static jint native_newInstance

(JNIEnv *env, jclass ) {

    int instanceID = ++sInstanceIdentifer;

    SoundTouchWrapper *instance = new SoundTouchWrapper();

    if (instance != NULL) {

        sInstancePool[instanceID] = instance;

        ++sInstanceCount;

    }

    LOGDBG(“create new SouncTouch instance:%d”, instanceID);

    return instanceID;

}

 

//構造JNINativeMethod陣列

static JNINativeMethod gsNativeMethods[] = {

        {

            “native_newInstance”,

            “()I”,

            reinterpret_cast (native_newInstance)

        }

};

//計算陣列大小

static const int gsMethodCount = sizeof(gsNativeMethods) / sizeof(JNINativeMethod);

 

//JNI_OnLoad,註冊native方法。

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

    JNIEnv* env;

    jclass clazz;

    LOGD(“JNI_OnLoad called”);

    if (vm->GetEnv(reinterpret_cast(&env;), JNI_VERSION_1_6) != JNI_OK) {

        return -1;

    }

    //FULL_CLASS_NAME是個宏定義,定義了對應java類的全名(要把包名中的點(.)_替換成斜槓(/))

    clazz = env->FindClass(FULL_CLASS_NAME);

    LOGDBG(“register method, method count:%d”, gsMethodCount);

    //註冊JNI函式

    env->RegisterNatives(clazz, gsNativeMethods,

        gsMethodCount);

    //必須傳回一個JNI_VERSION_1_1以上(不含)的版本號,否則直接載入失敗

    return JNI_VERSION_1_6;

}

實戰技巧篇

這裡主要是巧用 C 中的宏來減少重覆工作:

迅速生成全名

//修改包名時只需要改以下的宏定義即可

#define FULL_CLASS_NAME “com/young/soundtouch/SoundTouch”

#define func(name) Java_ ## com_young_soundtouch_SoundTouch_ ## name

#define constance(cons) com_young_soundtouch_SoundTouch_ ## cons

比如func(native_1newInstance)展開成:Java_com_young_soundtouch_SoundTouch_native_1newInstance即JNI中需要匯出的函式名(不過用動態註冊方式沒太大用了)

constance(AUDIO_FORMAT_PCM16)展開成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16這個著實有用。

而且如果包名改了也可以很方便的適應之。

安卓的log

//define __USE_ANDROID_LOG__ in makefile to enable android log

#if defined(__ANDROID__) && defined(__USE_ANDROID_LOG__)

#include

#define LOGV(…)   __android_log_print((int)ANDROID_LOG_VERBOSE, “ST_jni”, __VA_ARGS__)

#define LOGD(msg)  __android_log_print((int)ANDROID_LOG_DEBUG, “ST_jni_dbg”, “line:%3d %s”, __LINE__, msg)

#define LOGDBG(fmt, …) __android_log_print((int)ANDROID_LOG_DEBUG, “ST_jni_dbg”, “line:%3d ” fmt, __LINE__, __VA_ARGS__)

#else

#define LOGV(…) 

#define LOGD(fmt) 

#define LOGDBG(fmt, …) 

#endif

透過這樣的宏定義在打 LOGD 或者 LOGDBG 的時候還能自動加上行號!除錯起來爽多了!

C++中清理記憶體的方式

由於 C++ 裡面需要手動清除記憶體,因此我的解決方案是定義一個 map,給每個實體一個 id,用 id 把 Java 中的物件和 native 中的物件系結起來。在 Java 層定義一個 release 方法,用來釋放本地的物件。 本地的 KEY-物件 對映 static std::map sInstancePool;

關於NDK

因為安卓的約定是把原生代碼放到 jni 目錄下麵,但是假如有多個 jni lib 的時候會比較混亂,所以方案是每一個 lib 都在 jni 裡面建一個子目錄,然後 jni 裡面的 Android.mk 就可以去構建子目錄中的 lib 了。

jni/Android.mk 如下(超級簡單):

LOCAL_PATH := $(call my-dir)

include $(call all-subdir-makefiles)

然後在子目錄 soundtouch_module 中的 Android.mk 就可以像一般的 Android.mk 一樣書寫規則了。

同時記錄一下在 Andoroid.mk 中使用 makefile 內建函式 wildcard 的方法。 有時候源檔案是一個目錄下的所有 .cpp/.c 檔案,這時候 wildcard 來統配會很方便。但是 Android.mk 與普通的 Makefile 不同在於:

  1. 呼叫 Android.mkmingling 的 ${CWD} 並不是 Android.ml 所在的目錄。所以 Android.mk 中有一個變數 LOCAL_PATH := $(call my-dir) 來記錄當前 Android.mk 所在的目錄。

  2. 同時還會把所有的 LOCAL_SRC_FILES 前面加上 $(LOCAL_PATH)。這樣寫 makefile 的時候就可以用相對路徑了,提供了方便。但是這也導致了坑!

因為1,直接使用相對路徑會導致wildcard匹配不到源檔案。所以最好這麼寫 FILE_LIST := $(wildcard $(LOCAL_PATH)/soundtouch_source/source/SoundTouch/*.cpp)。然而又因為2,這樣還是不行的。所以還需要匹配之後把$(LOCAL_PATH)的部分去掉,因此還得這樣 $(FILE_LIST:$(LOCAL_PATH)/%=%).

還有個小tip:LOCAL_CFLAGS 中最好加上這個定義 -fvisibility=hidden 這樣就不會在動態庫中匯出不必要的函式了。

附錄簽名

Java 中的函式簽名包括了函式的引數型別,傳回值型別。因此即使是多載了的函式,其函式簽名也不一樣。java編譯器就會根據函式簽名來判斷你呼叫的到地址哪個方法。 簽名中表示型別是這樣的

1.基本型別都對應一個大寫字母,如下:

2. 如果是類則是: L + 類全名(報名中的點(.)用(/)代替)+ ; 比如java.lang.String 對應的是 Ljava/lang/String;

3. 如果是陣列,則在前面加[然後加型別簽名,幾位陣列就加幾個[ 比如int[]對應[I,boolean[][] 對應 [[Z,java.lang.Class[]對應[Ljava/lang/Class;

可以透過 javap 命令來獲取簽名(javah 生成的頭檔案註釋中也有簽名):javap -x -p 坑爹的是java中並不能透過反射來獲取方法簽名,需要自己寫一個幫助類。 (其實我還寫了個小程式可以自動生成簽名,和 JNI_OnLoad 中註冊要用到的 JNINativeMethod 陣列,從此再也不用糟心的去寫那該死的陣列了。LOL~~~)

參考資料

  1. Oracle Java SE documents

    http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html

  2. 深入理解Android 捲 1 第二章 ,鄧凡平著,機械工業出版社

  3. Google Android documents – JNI Tips

    http://developer.android.com/training/articles/perf-jni.html

上篇Java 中 JNI 的使用 ( 上 )

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂