(二)JNI基礎

一、什麼是JNI?

JNI是 Java Native Interface 的縮寫,表示 Java 本地接口。從 Java 1.1 開始,JNI 標準便成爲了 Java 平臺的一部分,它允許Java 代碼和其他語言寫的代碼進行交互。

二、JNI基礎

2.1 JNI的功能結構

JNI 最初是由 Sun 提供的Java 與本地系統中的原生方法交互的技術,用於在Windows/Linux 系統中實現 Java 與 Native Method (本地方法) 的相互調用。JVM(Java虛擬機)在封裝各種操作系統實際的差異性的同時提供了JNI 技術,使得開發者可以通過Java程序(代碼)調用到操作系統相關的技術實現的庫函數,從而與其他技術和系統交互,使用其他技術實現的功能。同時,其他技術和系統也可以通過JNI提供的相應原生接口調用Java應用系統內部實現的功能。

2.2 JNI的調用層次

JNI的調用層次主要風味3層,在Android系統中這三層從上到下依次爲Java→JNI→C/C++(.so庫),Java可以訪問到C/C++中的方法,同樣C/C++也可以修改Java對象,三者間關係如下圖:
在這裏插入圖片描述
由圖可知,JNI的調用關係爲 Java→JNI→Native。

2.3 分析JNI的本質

要想弄明白JNI的本質,還要從Java的本質說起。從本質上說,Java語言的運行完全依賴與腳本引擎對Java的代碼進行解釋和執行。因爲現代的Java可以從源代碼編譯成 .class 之類的中間格式的二進制文件,所以這種處理會加快 Java 腳本的運行速度。儘管如此,基本的執行方式仍然不變,有腳本引擎(被稱之爲 JVM)來執行。與Python、perl之類的純腳本相比,只是把腳本變成了二進制格而已。另外,Java本身就是一門面向對象的編程語言,可以調用完善的哦功能庫。當把這個腳本引擎移植到所在的平臺上之後,這個腳本就很自然的實現“跨平臺”了。絕大多數的腳本引擎都支持一個很顯著的特性,就是可以通過C/C++編寫模塊,並在腳本中調用這些模塊。Java也是如此,Java 一定要提供一種在腳本中調用 C/C++編寫的模塊的機制,才能稱得上是一個完善的腳本引擎。

從本質上來看,Android平臺是由 arm-linux 操作系統和一個 Dalvik 虛擬機組成的。所有在 Android 模擬器上看到的界面效果都是用 Java 語言編寫的,具體請看源代碼中的 framenworks/base 目錄。Dalvik 虛擬機只是提供了一個標準的支持 JNI 調用的 Java 虛擬機環境。

在 Android 平臺中,使用 JNI 技術封裝了所有和硬件相關的操作,通過 Java 去調用 JNI 模塊,而 JNI 模塊使用 C/C++ 調用 Android 系統本身的 arm-linux 底層驅動,這樣便實現了對硬件的調用。

2.4 Java 和 JNI基本數據類型的轉換

在Android 5.0中,Java 和 JNI 基本數據類型轉換信息表如下所示。

Java Native類型 本地C類型 字長
boolean jboolean 無符號 8位
byte jbyte 無符號 8位
char jchar 無符號 16位
short jshort 有符號 16位
int jint 有符號 32位
long jlong 有符號 64位
float jfloat 有符號 32位
double jdouble 有符號 64位

數組類型的對應關係如下表所示。

字符 Java類型 C類型
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
[S jshortArray short[]
[Z jbooleanArray boolean[]

對象數據類型的對應關係如下。

對象 Java類型 C類型
LJava/lang/String String jstring
LJava/net/Socket Socket jobject

2.5 JNIEnv接口

在Android 5.0中,Native Method(本地方法)中的JNIEnv作爲第一個參數被傳入。JNIEnv的內部結構如下圖所示。
在這裏插入圖片描述

當JNIEnv不作爲參數傳入時,JNI提供瞭如下兩個函數來獲得JNIEnv口。

(*jvm)->AttachCurrentThread(jvm,(void **)&env,NULL);
(*jvm)->GetEnv(jvm,(void**)&env,JNI_VERSION_1_2);
#上述兩個函數都利用 Java VM 接口獲得了 JNIEnv接口,並且 JNI 可以將獲得的 JNIEnv 封裝成一個函數。

Java通過JNI機制調用C/C++寫的程序,C/C++開發的Native程序需要遵循一定的 JNI 規範。例如,下面就是一個 JNI 函數聲明的例子。

JNIEXPORT jint JNICALL  Java_jniTest_MyTest_test(JNIEnv * env,jobject obj,jint arg);

JVM 負責從 Java Stack 轉入C/C++ Native Stack。當 Java 進入 JNI調用,除了函數本身的參數(arg)外,會多多出兩個參數:JNIEnv 指針和 Jobject 指針。其中,JNIEnv 是 JVM 創造的,被 Native 的 C/C++ 方法用來操縱 Java 執行棧中的數據,例如 Java Class、Java Method 等。

三、開發JNI程序

開發JNI程序的一般步驟如下所示。

  1. 編寫Java中的調用類
  2. 用 javah 生成C/C++ 原生函數的頭文件
  3. 在 C/C++ 中調用需要的其他函數功能實現原生函數,原則上可以調用任何資源
  4. 將項目依賴生成的所有原生庫和資源加入到 Java 項目
  5. 生成 Java程序

3.1 開發 JNI 程序

  1. 使用 Android Studio創建工程,在項目目錄下創建 cpp 文件夾用來存放 C/C++ 文件
    在這裏插入圖片描述
  2. 創建 C/C++ 源文件,並將原文件關聯到項目,根據不同的構建工具選擇構建文件,如果採用的是ndk-build,則需要編寫Android.mk 文件,並將它指向項目,如果採用的是 CMake 工具,則需要編寫 CMakeLists.txt 文件,並將它指向項目。具體詳細操作可參考:向您的項目添加 C 和 C++ 代碼一文。
  3. 編寫 JNI 函數
public class JNIInterface {
    public static native int NativeInit();
    public static native int NativeUninit();
    public static native int Start(int type);
    public static native int Stop();
    private static native byte[] GetMessage(int type, int defaultLen);//注:defaultLen要足夠大,要能放下可能的最長的命令
    public static native int FreeData(byte[] data);
    public static native int Test(int param);

    public static native boolean SendDataToDev(int type, byte[] data, int len);

    static {
        System.loadLibrary("allong");
    }
}
  1. 註冊 JNI 函數
    在現實應用中,Java 的 Native 函數與JNI 函數是一一對應關係。在Android 系統中,使用 JNINativeMethod 的結構體來記錄這種對應關係。
    在 Android 系統中,使用一種特定的方式來定義其 Native 函數,這與傳統定義的 Java JNI 的方式有所差別。其中很重要的區別是在 Android 中使用了一種 Java 和 C 函數的映射表數組,並在其中描述了函數的參數和返回值。這個數組類型是 JNINativeMethod ,具體定義如下所示。
typedef struct {
    const char* name; 	//Java 中函數的名字
    const char* signature; // 描述了函數的參數和返回值
    void*       fnPtr;	// 函數指針,指向C 函數
} JNINativeMethod;

示例如下:

JNINativeMethod methods[] = {
        {"NativeInit",    "()I",     (void *) NativeInit},
        {"NativeUninit",  "()I",     (void *) NativeUninit},
        {"Start",         "(I)I",    (void *) Start},
        {"Stop",          "()I",     (void *) Stop},
        {"GetMessage",    "(II)[B",  (void *) GetMessage},
        {"FreeData",      "([B)I",   (void *) FreeData},
        {"SendDataToDev", "(I[BI)Z", (void *) SendDataToDev}
};

上述代碼中,比較難以理解的是第二個參數,例如:

"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/Striing;)V"

實際上這些字符是與函數的參數一一對應的,具體說明如下所示。

  • ()中的字符表示參數,後面的則代表返回值。例如,"()V"就表示 void Func() ;
  • (II)V 表示 void Func(int ,int)。
    具體的每一個字符的對應關係如下表所示
字符 Java 類型 C類型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short

而數組則以 [ 開始,用兩個字符表示,例如 “[F”,“[B”等。
上面的都是基本數據類型,如果 Java 函數的參數是 class ,則以 L 開始,以 “;”結尾,中間部分使用 “/”隔離開的包名及類名。而其對應的 C 函數名的參數則爲 jobject 。一個例外是 String 類,其對應的類爲 jstring,即:

  • Ljava/lang/String 中的 String jstring ;
  • Ljava/net/Socket 中的Socket jobject 。
    如果 Java 函數位於一個嵌入類,則使用“$”作爲類名間的分隔符。例如:
(Ljava/lang/String$Landroid/os/FileUtils$FileStatus;)

實現註冊工作

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    UnionJNIEnvToVOid uenv;
    uenv.env = NULL;
    jint result = -1;
    JNIEnv *env = NULL;

    LOGI("JNI_OnLoad");

    if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {

        LOGE("Error:GetEnv failed");
        return JNI_VERSION_1_4;
    }

    env = uenv.env;

    if (registerNatives(env) != JNI_TRUE) { //註冊函數
        LOGE("ERROR: registerNatives failed");
        goto bail;
    }

    result = JNI_VERSION_1_4;

    bail:
    return result;
}

Java 虛擬機在加載 JNI 函數時,首先會調用 JNI_OnLoad 方法,在其中實現註冊邏輯。

const char * classPathName = "com/yj/example/natives/JNIInterface" //native 方法所在的類名

static int registerNatives(JNIEnv *env) {
    if (!registerNativeMethods(env, classPathName, methods, sizeof(methods) / sizeof(methods[0]))) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods,
                                 int numMehthods) {
    jclass clazz = env->FindClass(className);
    if (clazz == NULL) {
        LOGE("Native register unable to find class '%s'", className);
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMehthods)) {
        LOGE("RegisterNatives failed for '%s'", className);
        return JNI_FALSE;
    }
    return JNI_TRUE;
}
  1. 編寫 CMakeLists.txt ,可參考 向您的項目添加 C 和 C++ 代碼 中 CMakeLists.txt 的編寫規則。
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.

cmake_minimum_required(VERSION 3.4.1)

# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

file(GLOB native_src "src/main/cpp/*.cpp")

add_library( # Specifies the name of the library.
             allong

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             ${native_src} )
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )
#Specifies a path to native header files
target_link_libraries( # Specifies the target library.
                       allong

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
include_directories(src/main/cpp/include/)
  1. 使用 ndk-build 編譯生成 .so 文件,將最後生成 .so 文件拷貝到 jniLibs 目錄下。
    在這裏插入圖片描述
  2. 編寫 Java 調用代碼,並測試自己寫的程序。
   static {
        System.loadLibrary("allong");
    }

參考內容

  1. 向您的項目添加 C 和 C++ 代碼
  2. 《深入理解 Android 系統》 張元亮 編著 清華大學出版社
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章