Android JNI中巧妙的使用動態註冊

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")執行的時候就會把方法註冊,所以說,提前加載,減少運行時間。我們要做的就是重寫他。

  1. 通過jint GetEnv(void** env, jint version)創建一個JavaVm。第一個參數爲創建的指針變量,第二個參數爲JNI的NDK版本,非JAVA版本。這個是我們動態註冊的關鍵,同時多線程也會用到它(後續補充)。所以我們升級JavaVm爲全局變量通過此方法寫入指針,得到指針變量。
  2. 注意這個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基礎類型傳遞,反射,方法調用整理一下,方便自己複製粘貼,加油~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章