JNI概述
JNI的全稱是Java Native Interface,它提供了若干的API實現了Java和其他語言的通信(主要是C&C++)。
使用Java與本地已編譯的代碼交互,通常會喪失平臺可移植性。但是,有些情況下這樣做是可以接受的,甚至是必須的。例如:
使用一些舊的庫
與硬件、操作系統進行交互
提高程序的性能
提高應用的安全性。
那麼怎麼使用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函數包含兩個固定的參數變量,分別是JNIEnv和jobject:
jobject就是當前與之鏈接的native方法隸屬的類對象(類似於Java中的this)。
JNIEnv是個結構體,它指向一個函數表,該函數表指向一系列的JNI函數,我們通過調用這些JNI函數可以實現與Java層的交互。
這兩個變量都是Java虛擬機生成並在調用時傳遞進來的。
然而靜態註冊有很多弊端,例如:
代碼編寫不方便,由於JNI層函數的名字必須遵循特定的格式,且名字特別長;
程序運行效率低,因爲初次調用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
參考: