1. 程式人生 > >註冊JNI函式的兩種方式

註冊JNI函式的兩種方式

前言

前面介紹過如何實現在Android Studio中製作我們自己的so庫,相信大家看過之後基本清楚如何在Android studio建立JNI函式並最終編譯成不同cpu架構的so庫,但那篇文章介紹註冊JNI函式的方法(靜態方法)存在一些弊端,本篇將介紹另外一種方法(動態註冊)來克服這些弊端。

註冊JNI函式的兩種方法

靜態方法

這種方法我們比較常見,但比較麻煩,大致流程如下:
- 先建立Java類,宣告Native方法,編譯成.class檔案。
- 使用Javah命令生成C/C++的標頭檔案,例如:javah -jni com.devilwwj.jnidemo.TestJNI,則會生成一個以.h為字尾的檔案com_devilwwj_jnidemo_TestJNI.h


- 建立.h對應的原始檔,然後實現對應的native方法,如下圖所示:

這裡寫圖片描述

說一下這種方法的弊端:

  • 需要編譯所有聲明瞭native函式的Java類,每個所生成的class檔案都得用javah命令生成一個頭檔案。
  • javah生成的JNI層函式名特別長,書寫起來很不方便
  • 初次呼叫native函式時要根據函式名字搜尋對應的JNI層函式來建立關聯關係,這樣會影響執行效率
    摘自:深入理解Android卷I

既然有這麼多弊端,我們自然要考慮一下有沒有其他更好的方法下一節就是我要講的替代方法,Android用的也是這種方法。

動態註冊

我們知道Java Native函式和JNI函式時一一對應的,JNI中就有一個叫JNINativeMethod的結構體來儲存這個對應關係,實現動態註冊方就需要用到這個結構體。舉個例子,你就一下子明白了:

宣告native方法還是一樣的:

public class JavaHello {
    public static native String hello();
}

建立jni目錄,然後在該目錄建立hello.c檔案,如下:

//
// Created by DevilWwj on 16/8/28.
//
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>

/**
 * 定義native方法
 */
JNIEXPORT jstring JNICALL native_hello(JNIEnv *env, jclass clazz)
{
    printf("hello in c native code./n"
); return (*env)->NewStringUTF(env, "hello world returned."); } // 指定要註冊的類 #define JNIREG_CLASS "com/devilwwj/library/JavaHello" // 定義一個JNINativeMethod陣列,其中的成員就是Java程式碼中對應的native方法 static JNINativeMethod gMethods[] = { { "hello", "()Ljava/lang/String;", (void*)native_hello}, }; static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) { jclass clazz; clazz = (*env)->FindClass(env, className); if (clazz == NULL) { return JNI_FALSE; } if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { return JNI_FALSE; } return JNI_TRUE; } /*** * 註冊native方法 */ static int registerNatives(JNIEnv* env) { if (!registerNativeMethods(env, JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) { return JNI_FALSE; } return JNI_TRUE; } /** * 如果要實現動態註冊,這個方法一定要實現 * 動態註冊工作在這裡進行 */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; jint result = -1; if ((*vm)-> GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { return -1; } assert(env != NULL); if (!registerNatives(env)) { //註冊 return -1; } result = JNI_VERSION_1_4; return result; }

先仔細看一下上面的程式碼,看起來好像多了一些程式碼,稍微解釋下,如果要實現動態註冊就必須實現JNI_OnLoad方法,這個是JNI的一個入口函式,我們在Java層通過System.loadLibrary載入完動態庫後,緊接著就會去查詢一個叫JNI_OnLoad的方法。如果有,就會呼叫它,而動態註冊的工作就是在這裡完成的。在這裡我們會去拿到JNI中一個很重要的結構體JNIEnv,env指向的就是這個結構體,通過env指標可以找到指定類名的類,並且呼叫JNIEnv的RegisterNatives方法來完成註冊native方法和JNI函式的對應關係。

我們在上面看到聲明瞭一個JNINativeMethod陣列,這個陣列就是用來定義我們在Java程式碼中宣告的native方法,我們可以在jni.h檔案中檢視這個結構體的宣告:

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

結構體成員變數分別對應的是Java中的native方法的名字,如本文的hello;Java函式的簽名信息、JNI層對應函式的函式指標。

以上就是動態註冊JNI函式的方法,上面只是一個簡單的例子,如果你還想再實現一個native方法,只需要在JNINativeMethod陣列中新增一個元素,然後實現對應的JNI層函式即可,下次我們載入動態庫時就會動態的將你宣告的方法註冊到JNI環境中,而不需要你做其他任何操作。

總結

關於JNI技術,在Android中使用是非常多的,我們在實際開發中或多或少可能會使用到第三方或者需要自己開發相應的so庫,所以學習和理解JNI中的一些實現原理還是很有必要的,從以前在Eclipse來實現so庫開發到現在可以通過Android Studio來開發so庫,會發現會方便很多,這個也是技術的發展帶來的一些便捷。筆者也計劃學習NDK開發的相關技術,後續也會將自己學到的內容總結分享出來。