NDK筆記(關於Android中Jni的動態註冊)
Android app加載.c/cpp和.so/.a就必然要談到jni接口的編寫,jni接口註冊有倆種方式:動態和靜態註冊。靜態註冊的方式固然方便快捷,但是這樣的話簡單demo可以,爲了項目的工程化,還是有必要引入動態註冊的,好處會在下面講(恩,我已經說服了我自己,目前的工作中已經逐步替換爲動態註冊了)。下面就以一個Kotlin工程爲例,逐步梳理下詳細過程。
1.靜態註冊
新建工程創建一個Jni接口的class工具類。
class JniUtils {
external fun stringFromJNI(): String
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
這個在工程中external這一行會爆紅,鼠標放上去,alt+enter點擊creat jni…編譯器就會替你生成一個jni接口,這個冗長的函數就是一個靜態註冊的jni接口函數。同時編譯器的error也會消失。
extern "C"
JNIEXPORT jstring JNICALL
Java_com_heima_jnitest_JniUtils_stringFromJNI(JNIEnv *env, jobject thiz) {
// TODO: implement stringFromJNI()
}
2.動態註冊
相比於靜態註冊,動態註冊就有以下安全點:
- 被反編譯後安全性高(用着放心)
- native中函數名簡潔(看着舒服)
- 編譯後的函數標記較短一些(調用方便)
我們只有關注2個大點:JNI_OnLoad() 和jniNativeMethod
2.1 尋找方法簽名
關於動態註冊那個冗長的方法,在動態註冊時候我們需要把它變短,怎麼變短呢?這就牽扯到一個結構體jniNativeMethod
,需要用到類跟方法的簽名。我們找到生成的class的文件夾,通過命令javap來找到簽名,當然也可以用javah,道理都是一樣的。
詳細方法入上圖所示,找到class→javap→得到簽名。得到如下標識:
警告: 二進制文件Jniutils包含com.heima.jnitest.JniUtils
Compiled from "JniUtils.kt"
public final class com.heima.jnitest.JniUtils {
public static final com.heima.jnitest.JniUtils$Companion Companion;
descriptor: Lcom/heima/jnitest/JniUtils$Companion;
public final native java.lang.String stringFromJNI();
descriptor: ()Ljava/lang/String;
public com.heima.jnitest.JniUtils();
descriptor: ()V
static {};
descriptor: ()V
}
這個簽名有什麼用呢?這就牽扯到動態註冊中很重要的結構體JNINativeMethod
其中的signature就是我們通過命令行輸出的descriptor,關於網上所說的什麼簽名對照表什麼的,我是記不住,每次敲敲命令行就好,真的沒必要查表。
jstring stringFromJNI(JNIEnv *env, jobject thiz) {//冗長方法直接刪短
// TODO: implement stringFromJNI()
}
/*
1. typedef struct {
const char* name; //函數名字
const char* signature; //函數符號
void* fnPtr; //函數指針
} JNINativeMethod;
*/
static const JNINativeMethod jniNativeMethod[] = {
{"stringFromJNI", "(Ljava/lang/String;)V", (void *) (stringFromJNI)},
};
2.2 JNI_OnLoad
說到動態註冊就要說道JNI_OnLoad
這個方法,這個方法會在System.loadLibrary("native-lib")
執行的時候就會把方法註冊,所以說,提前加載,減少運行時間。我們要做的就是重寫他。
- 通過
jint GetEnv(void** env, jint version)
創建一個JavaVm。第一個參數爲創建的指針變量,第二個參數爲JNI的NDK版本,非JAVA版本。這個是我們動態註冊的關鍵,同時多線程也會用到它(後續補充)。所以我們升級JavaVm爲全局變量通過此方法寫入指針,得到指針變量。 - 注意這個jint返回值。點到jni.h裏面會有詳細的解釋。
#define JNI_FALSE 0 #define JNI_TRUE 1 //Jni版本 #define JNI_VERSION_1_1 0x00010001 #define JNI_VERSION_1_2 0x00010002 #define JNI_VERSION_1_4 0x00010004 #define JNI_VERSION_1_6 0x00010006 //返回值類型 #define JNI_OK (0) /* no error */ #define JNI_ERR (-1) /* generic error */ #define JNI_EDETACHED (-2) /* thread detached from the VM */ #define JNI_EVERSION (-3) /* JNI version error */ #define JNI_ENOMEM (-4) /* Out of memory */ #define JNI_EEXIST (-5) /* VM already created */ #define JNI_EINVAL (-6) /* Invalid argument */ #define JNI_COMMIT 1 /* copy content, do not free buffer */ #define JNI_ABORT 2 /* free buffer w/o copying back */
3.使用jclass FindClass(const char* name)
函數通過反射獲取到jclass
對象
4. 使用jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
動態註冊jni函數。
- 第一個參數爲上一步獲取的jclass;
- 第二個參數爲Jni方法體,也就是我們第一步通過方法簽名編寫的
JNINativeMethod
集合; - 第三個參數爲
JNINativeMethod
的長度,也就是要動態加載的函數的數量
直接貼代碼,一切盡在註釋中。
/**
* 1.設置jvm全局變量,多線程需要用到
* 2.nullptr: C++11後,要取代NULL,作用是可以給初始化的指針賦值
*/
JavaVM *jvm = nullptr;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVm, void *pVoid) {
jvm = javaVm;
// 1.通過JavaVM 創建全新的JNIEnv
JNIEnv *jniEnv = nullptr;
// 2.判斷創建是否成功
jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv),JNI_VERSION_1_6); // 參數2:是JNI的版本 NDK 1.6 JavaJni 1.8
if (result != JNI_OK) {
return -1; // 主動報錯
}
// 3.找到需要動態動態註冊的Jni類
jclass jniClass = jniEnv->FindClass("com/heima/jnitest/JniUtils");
//動態註冊(這裏就需要用到簽名後的方法了) 待註冊class 方法集合 方法數量
jniEnv->RegisterNatives(jniClass, jniNativeMethod,sizeof(jniNativeMethod) / sizeof(JNINativeMethod));
return JNI_VERSION_1_6;
}
大功告成,這是個最簡單的動態JNI接口的加載。最後附上代碼下載地址
趁着拔智齒在家休息,梳理一遍,準備把JNI基礎類型傳遞,反射,方法調用整理一下,方便自己複製粘貼,加油~