打算對Android的NDK的開發做一總結,首先是JNI部分,接下來是NDK的內容。今天首先介紹一下JNI的第一部分:注冊native函數。
當java代碼中執行native的代碼時候,首先是通過一定的方法來找到這些native方法。而注冊native函數的具體方法的不同,會導致系統在運行時采用不同的方式來尋找這些native方法。
JNI有如下兩種注冊native方法的途徑:靜態和動態。其中:
靜態:先由Java得到本地方法的聲明,然后再通過JNI實現該聲明方法。
動態:先通過JNI重載JNI_OnLoad()實現本地方法,然后直接在Java中調用本地方法。
靜態注冊
根據函數名找到對應的JNI函數:Java層調用函數時,會從對應的JNI中尋找該函數,如果沒有就會報錯,如果存在則會建立一個關聯聯系,以后在調用時會直接使用這個函數,這部分的操作由虛擬機完成。
靜態方法就是根據函數名來遍歷java和jni函數之間的關聯,而且要求jni層函數的名字必須遵循
特定的格式。
具體的實現很簡單,首先在java代碼中聲明native函數,然后通過javah來生成native函數的具體形式,接下來在JNI代碼中實現這些函數即可。
看個例子就更明了了:
Java層:
static {
System.loadLibrary("samplelib_jni");
registerNatives();
}
private native void nativeFunc1();
private native void nativeFunc2();
private native void nativeFunc3();
接下來通過javah來產生jni代碼聲明:
假設你的java文件的包名是com.jni.samle 而類名是JniSample。
javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.jni.samle.JniSample
然后就會得到一個JNI的.h文件,里面包含這幾個native函數的聲明,觀察一下文件名以及函數名,會有一定的規律,我會在下一篇文章中對此做一詳細介紹,在此不再贅述。
最后實現jni層的native方法即可。
動態注冊
對Java程序員來說,可能我們總是會遵循:1.編寫帶有native方法的Java類;—->2.使用javah命令生成.h頭文件;—->3.編寫代碼實現頭文件中的方法,這樣的標準流程,但也許有人無法忍受那“丑陋”的方法名稱,所以我們可以采用動態注冊方法,也就是通過RegisterNatives方法把c/c++中的方法隱射到Java中的native方法,而無需遵循特定的方法命名格式。
JNI 允許你提供一個函數映射表,注冊給Jave虛擬機,這樣Jvm就可以用函數映射表來調用相應的函數,
就可以不必通過函數名來查找需要調用的函數了。
Java與JNI通過JNINativeMethod的結構來建立聯系,它在jni.h中被定義,其結構內容如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一個變量name是Java中函數的名字。
第二個變量signature,用字符串是描述了函數的參數和返回值
第三個變量fnPtr是函數指針,指向C函數。
當java通過System.loadLibrary加載完JNI動態庫后,緊接著會查找一個JNI_OnLoad的函數,如果有,就調用它, 而動態注冊的工作就是在這里完成的。
一起來看一下具體的實現方法:
Java code:
比較簡單,僅僅是加載so庫。
static {
System.loadLibrary("samplelib_jni");
}
JNI code:
在JNI中實現
jint JNI_OnLoad(JavaVM* vm, void* reserved)
并且在這個函數里面去動態的注冊native方法,完整的參考代碼如下:
#include
#include "Log4Android.h"
#include
#include
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
static const char *className = "com/zhixin/jnisample/JniManager";
static void sayHello(JNIEnv *env, jobject, jlong handle) {
LOGI("JNI", "native: say hello ###");
}
static JNINativeMethod gJni_Methods_table[] = {
{"sayHello", "(J)V", (void*)sayHello},
};
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGI("JNI","Registering %s natives\n", className);
clazz = (env)->FindClass( className);
if (clazz == NULL) {
LOGE("JNI","Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
LOGE("JNI","RegisterNatives failed for '%s'\n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGI("JNI", "enter jni_onload");
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
比較
下面我們來比較一下這兩種不同的實現方法。
首先是靜態方法:
優點實現起來比較簡單,直接用javah就能將Java代碼中的native函數的聲明轉化為native代碼的函數。
缺點在于:javah生成的jni層函數特別長;
初次調用native函數時要根據名字搜索對應的jni層函數來建立關聯聯系,這樣影響效率。
而動態方法的優缺點剛好和靜態方法相反。
通過上面的介紹我們發現,盡管靜態注冊實現起來比較簡單,但是會導致效率相對來說比較低。
所以在JNI層提供JNI_OnLoad是一個推薦的做法。如果不提供JNI_OnLoad,那么JNI函數的命名就需要符合規范,并且需要導出該函數。這樣做很容易被別人反編譯。相反,如果提供JNI_OnLoad,就可以在里面自己注冊JNI函數給Dalvik VM,這樣JNI函數的命名就可以按照自己的習慣了,而且也不用導出。
一個巧妙的合作
在實際的應用中,我們可以巧妙的將靜態注冊和動態注冊結合起來:也就是在java代碼中仍然聲明一個native函數,但是這個函數僅僅是用來去觸發在JNI層的native函數的動態注冊。說的有些繞,看看代碼就明白了:
Java層:
static {
System.loadLibrary("samplelib_jni");
registerNatives();
}
private static native void registerNatives();
JNI層:
通過javah生成java層聲明的native函數的文件,并且在實現代碼中去動態注冊JNI函數:
JNIEXPORT void JNICALL Java_com_zhixin_jni_JniSample_registerNatives
(JNIEnv *env, jclass clazz){
(env)->RegisterNatives(clazz, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
}
后續
關于JNI的函數注冊就到這里了,在下一篇文章中會總結JNI的簽名規則問題。