一、什麼是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程序的一般步驟如下所示。
- 編寫Java中的調用類
- 用 javah 生成C/C++ 原生函數的頭文件
- 在 C/C++ 中調用需要的其他函數功能實現原生函數,原則上可以調用任何資源
- 將項目依賴生成的所有原生庫和資源加入到 Java 項目
- 生成 Java程序
3.1 開發 JNI 程序
- 使用 Android Studio創建工程,在項目目錄下創建 cpp 文件夾用來存放 C/C++ 文件
- 創建 C/C++ 源文件,並將原文件關聯到項目,根據不同的構建工具選擇構建文件,如果採用的是ndk-build,則需要編寫Android.mk 文件,並將它指向項目,如果採用的是 CMake 工具,則需要編寫 CMakeLists.txt 文件,並將它指向項目。具體詳細操作可參考:向您的項目添加 C 和 C++ 代碼一文。
- 編寫 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");
}
}
- 註冊 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;
}
- 編寫 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/)
- 使用 ndk-build 編譯生成 .so 文件,將最後生成 .so 文件拷貝到 jniLibs 目錄下。
- 編寫 Java 調用代碼,並測試自己寫的程序。
static {
System.loadLibrary("allong");
}
參考內容
- 向您的項目添加 C 和 C++ 代碼
- 《深入理解 Android 系統》 張元亮 編著 清華大學出版社