Android JNI編程入門

JNI概述

JNI的全稱是Java Native Interface,它提供了若干的API實現了Java和其他語言的通信(主要是C&C++)

使用Java與本地已編譯的代碼交互,通常會喪失平臺可移植性。但是,有些情況下這樣做是可以接受的,甚至是必須的。例如:

  1. 使用一些舊的庫

  2. 與硬件、操作系統進行交互

  3. 提高程序的性能

  4. 提高應用的安全性

那麼怎麼使用JNI呢,一般情況下我們首先是將寫好的C/C++代碼編譯成對應平臺的動態庫(windows一般是dll文件,linux一般是so文件等),這裏我們是針對Android平臺,所以只討論so庫

  • 打包成so庫會更安全一點,但肯定不是完全安全,只是相對反編譯Java的class字節碼文件來說,反彙編so動態庫來分析程序的邏輯要複雜得多,沒那麼容易被破解。很多SDK,都使用了so庫,例如百度SDK,微信SDK。

開始JNI編程之前,肯定要配置所支持的環境,請移步通過CMake在AndroidStudio項目中引入JNI編程

下面就開始介紹Android中JNI編程的入門知識。

JNI函數的註冊

其實就是Java的native方法與C/C++中的函數的連接,使二者能夠識別彼此。

先看兩個文件。

MainActivity.java

package com.tsnt.jni.androidjnidemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = (TextView) findViewById(R.id.sample_text);
        TextView tv1 = (TextView) findViewById(R.id.sample_text1);

        tv.setText(stringFromJNI());
        tv1.setText(getStringFromJNI());
    }

    //聲明兩個本地方法
    public native String stringFromJNI();

    public native String getStringFromJNI();

    //加載本地庫native-lib
    static {
        System.loadLibrary("native-lib");
    }
}

native-lib.cpp

//類似Java中的導包
#include <jni.h>
#include <string>

//extern "C"的主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼
//加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的
extern "C"

//靜態註冊的方法
//JNIEnv:它指向一個函數表,該函數表指向一系列的JNI函數,我們通過調用這些JNI函數可以實現與Java層的交互
jstring Java_com_tsnt_jni_androidjnidemo_MainActivity_getStringFromJNIStatically(
        JNIEnv *env,
        jobject obj) {
    //聲明一個string類型變量
    std::string hello = "Hello from native -- registered statically";
    return env->NewStringUTF(hello.c_str());
}

//動態註冊的方法
jstring nativeGetStringFromJNIDynamically(JNIEnv *env, jobject obj) {
    std::string hello = "Hello from native -- registered dynamically";
    return env->NewStringUTF(hello.c_str());
}

//用來保存方法信息的數組
JNINativeMethod nativeMethod[] = {{"getStringFromJNIDynamically", "()Ljava/lang/String;", (void *) nativeGetStringFromJNIDynamically},};

//當我們使用System.loadLibarary()方法加載so庫的時候,Java虛擬機就會找到這個函數並調用該函數
//因此可以在該函數中做一些初始化的動作
//其實這個函數就是相當於Activity中的onCreate()方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {

    JNIEnv *env;

    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {

        return -1;
    }

    //獲取MainActivity對象
    jclass clz = env->FindClass("com/tsnt/jni/androidjnidemo/MainActivity");

    //動態註冊方法
    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));

    //返回JNI版本
    return JNI_VERSION_1_4;
}

最後程序成功運行起來,就是這樣的:

這裏寫圖片描述

其實通過代碼中的註釋,大家已經可以理解個大概了,下面進行進一步分析。

靜態註冊

我們在MainActivity中聲明瞭getStringFromJNIStatically()爲native方法,他對應的JNI函數就是Java_com_tsnt_jni_androidjnidemo_MainActivity_getStringFromJNIStatically()

在Java虛擬機加載so庫時,會去尋找對應Java層的native方法。那它們兩個究竟是怎麼關聯的呢?

我們仔細觀察JNI函數名的構成形式是:Java_PkgName_ClassName_NativeMethodName,以Java爲前綴,並且用“_”下劃線將包名、類名以及native方法名連接起來就是對應的JNI函數了

這裏簡單介紹一下生成的JNI函數包含兩個固定的參數變量,分別是JNIEnvjobject

  • jobject就是當前與之鏈接的native方法隸屬的類對象(類似於Java中的this)

  • JNIEnv是個結構體,它指向一個函數表,該函數表指向一系列的JNI函數,我們通過調用這些JNI函數可以實現與Java層的交互。

這兩個變量都是Java虛擬機生成並在調用時傳遞進來的。

然而靜態註冊有很多弊端,例如:

  1. 代碼編寫不方便,由於JNI層函數的名字必須遵循特定的格式,且名字特別長;

  2. 程序運行效率低,因爲初次調用native函數時需要根據根據函數名在JNI層中搜索對應的本地函數,然後建立對應關係,這個過程比較耗時(靜態註冊是用到時加載,動態註冊一開始就加載好了)。

下面就來說動態註冊。

動態註冊

動態註冊的原理是這樣的:JNI 允許我們提供一個函數映射表,註冊給 JVM,這樣 JVM 就可以用函數映射表來調用相應的函數。

JNI_OnLoad()

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {

    JNIEnv *env;

    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {

        return -1;
    }

    //獲取MainActivity對象
    jclass clz = env->FindClass("com/tsnt/jni/androidjnidemo/MainActivity");

    //動態註冊方法
    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));

    //返回JNI版本
    return JNI_VERSION_1_4;
}

當我們使用System.loadLibarary()方法加載so庫的時候,Java虛擬機就會找到這個函數並調用該函數,因此可以在該函數中做一些初始化的動作,其實這個函數就是相當於Activity中的onCreate()方法

函數返回值表示當前使用的JNI的版本,其實類似於Android系統的API版本一樣,不同的JNI版本中定義的一些不同的JNI函數。

該函數有兩個參數,其中*jvm代表Java虛擬機實例

其中進行的操作主要就是,獲取Java對象,完成動態註冊

JNINativeMethod

註冊方法的時候利用到了JNINativeMethod這個結構體,來看代碼:

//用來保存方法信息的數組
JNINativeMethod nativeMethod[] = {{"getStringFromJNIDynamically", "()Ljava/lang/String;", (void *) nativeGetStringFromJNIDynamically},};

nativeMethod其實就是一個J**NINativeMethod的數組**,JNINativeMethod是這樣定義的:

typedef struct {
    const char* name;//Java層native方法的名字
    const char* signature;//Java層native方法的描述符
    void*       fnPtr;//對應JNI函數的指針
} JNINativeMethod;

JNI數據類型

上面我們提到JNI定義了一些自己的數據類型。這些數據類型是銜接Java層和C/C++層的,如果有一個對象傳遞下來,那麼對於C/C++來說是沒辦法識別這個對象的,同樣的如果C/C++的指針對於Java層來說它也是沒辦法識別的,那麼就需要JNI進行匹配,所以需要定義一些自己的數據類型。

基本數據類型

這裏寫圖片描述

引用數據類型

這裏寫圖片描述

描述符

類描述符

前面爲了獲取Java的AndroidJNI對象,是通過調用FindClass()函數獲取的,該函數參數只有一個字符串參數,我們發現該字符串如下所示:

"com/tsnt/jni/androidjnidemo/MainActivity"

其實這個就是JNI定義了對類的描述符,它的規則就是將"com.tsnt.jni.androidjnidemo.MainActivity"中的“.”用“/”代替

方法描述符

前面我們動態註冊native方法的時候結構體JNINativeMethod中含有方法描述符,就是確定native方法的參數和返回值,我們這裏定義native方法是這樣的:

    public native String getStringFromJNIDynamically();

對應的描述符“()Ljava/lang/String;”

  • 括號中的值表示方法參數,沒有參數,所以括號中爲空。

  • 括號後的值表示方法返回值,爲String類型。

再舉個例子:

    public native void Fun(int a, int b)

對應的描述符:“(II)V”

方法描述符中的基本數據類型

這裏寫圖片描述

方法描述符中的引用數據類型

這裏寫圖片描述

  • 對象類型:以”L”開頭,以”;”結尾,中間是用”/” 隔開,如上表第1個

  • 數組類型:以”[“開始,如上表第2個(n維數組的話,則是前面多少個”[“而已,如“[[[D”表示“double[][][]”)。

  • 對象數組類型:上述兩者結合,如上表第3個。

對象類型與數組類型的舉例

這裏寫圖片描述

demo地址:AndroidJNIDemo

參考:

  1. JNI/NDK開發指南(開山篇)
  2. Android JNI編程—JNI基礎
  3. Android的NDK開發(4)————JNI數據結構之JNINativeMethod
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章