Android零基礎進階 | JNI
JNI背景
Java的一個顯著的特點就是跨平臺,但是正因如此,Java和本地機器的交互就很少。但是在某些情況下,比如提升性能或實現特定需求,需要我們不得不使用調用C/C++來實現,因此JNI應運而生。
JNI和NDK的關係
Jni全稱:Java Native Interface,Java原生接口,是爲Java和C/C++等語言相互訪問提供的接口。
NDK全稱:Native Develop Kit ,原生開發工具集,爲了實現JNI所需要使用的工具。
一般都是編寫C/C++ Code,通過NDK生成.so文件,然後再通過JAVA調用其中的接口來實現相應的功能。
如果是IOS的小夥伴,個人感覺這種關係有點類似於Swift調用OC?
JNI的實現流程
Jni編寫大致過程
- 首先聲明一個native方法
- 通過javah生成相應的頭文件(有的會先javac生成class文件,試了直接javah好像也沒有問題= 。=)
- 實現頭文件中聲明函數的方法
- 編譯生成so文件
- 在java中進行調用so文件
聲明Native方法
public class Native{
public static native void Hello();
}
通過javah生成相應的Native頭文件
進入main目錄下。執行javah -encoding utf-8 包名命令,會看到生成了一個.h的頭文件
實現相應的方法
創建一個C/Cpp文件,然後將頭文件賦值到裏面進行少許修改,例如:
public static native int send_message(byte[] buffer, int size);
//.h 自動生成
JNIEXPORT jint JNICALL Java_com_vpn_NativeClass_send_1message
(JNIEnv *, jclass, jbyteArray, jint);
//.c
JNIEXPORT jint JNICALL Java_com_vpn_NativeClass_send_1message
(JNIEnv * env, jclass, jbyteArray byteData, jint size) {
return 0;
}
關於JNIEnv
JNIEnv 是一個與線程相關的結構體,是c和java相互調用的橋樑,由於線程相關,所以線程B中不能使用線程A中的JNIEnv函數。
JNIEnv 與 JavaVM : 注意區分這兩個概念;
– JavaVM : JavaVM 是 Java虛擬機在 JNI 層的代表, JNI 全局僅僅有一個該虛擬機映射,所有線程共用一個;
– JNIEnv : JavaVM 在線程中的代表, 每一個線程都有一個, JNI 中可能有非常多個 JNIEnv;
JNIEnv 作用 :
– 調用 Java 函數 : JNIEnv 代表 Java 執行環境, 能夠使用 JNIEnv 調用 Java 中的代碼;
– 操作 Java 對象 : Java 對象傳入 JNI 層就是 Jobject 對象, 須要使用 JNIEnv 來操作這個 Java 對象;
數據類型
基礎數據類型
類型簽名
與JNIEnv相關的常用函數
//獲取jclass對象,參數:this的意思,就是native方法所在的類
1.GetObjectClass(jobject)
//獲取普通屬性id,第一個參數:類對象, 第二個參數:屬性名,第三個參數:屬性簽名
2.GetFieldID(jclass clazz, const char* name, const char* sig)
//設置int屬性的值, 第一個參數:this的意思, 第二個參數:獲取屬性id, 第三個參數:要設置的值
3.SetIntField(jobject obj, jfieldID fieldID, jint value)
當然這裏就只列舉SetIntField函數了,同理還有很多,比如:SetCharField,SetFloatField,SetObjectField …。有Set函數肯定也會有Get函數,與之對應的就是GetIntField(jobject obj, jfieldID fieldID),這個函數是獲取指定屬性的值,參數含義同SetIntField函數
//獲取靜態屬性Id, 第一個參數:類對象, 第二個參數: 屬性名, 第三個參數: 屬性簽名
4.GetStaticFieldID(jclass clazz, const char* name, const char* sig)
//設置靜態屬性的值, 第一個參數: 類對象, 第二個參數: 屬性id, 第三個參數: 要設置的值
5.SetStaticIntField(jclass clazz, jfieldID fieldID, jint value)
//獲取函數id, 第一參數:類對象, 第二個參數:函數名, 第三個參數: 函數簽名
6.GetMethodID(jclass clazz, const char* name, const char* sig)
//調用java中的無返回值函數, 第一個參數: this的意思, 第二個參數: 函數id, 第三個參數:需要傳入的實參
7.CallVoidMethod(jobject obj, jmethodID methodID, …)
//獲取靜態函數id, 第一個參數: 類對象, 第二個參數: 函數名, 第三個參數: 函數簽名
8.GetStaticMethodID(jclass clazz, const char* name, const char* sig)
//調用java中無返回值的靜態函數, 第一個參數: 類對象, 第二個參數: 函數id, 第三個參數: 需要傳入的實參
9.CallStaticVoidMethod(jclass clazz, jmethodID methodID, …)
//生成一個jstring類型的方法轉換,該方法會返回一個jstring類型
10.NewStringUTF(const char* bytes)
//調用java中的對象類型(String類型被認爲對象類型),第一參數:this的意思, 第二個參數:函數Id, 第三個參數:需要傳入的實參
11.CallObjectMethod(jobject, jmethodID, …);
//獲取類中的對象屬性,第一個參數:this的意思 , 第二個參數:屬性id
12.GetObjectField(jobject obj, jfieldID fieldID)
//根據子類的類對象,獲取父類的類對象, 第一參數:子類類對象
13.GetSuperclass(jclass clazz)
//調用java中父類的方法,第一個參數:子類的對象, 第二個參數:父類的類對象, 第三個參數:父類的函數id, 第四個參數:需要傳入的實參
14.CallNonvirtualVoidMethod(jobject obj, jclass clazz, jmethodID methodID, …)
NDK生成so的流程
編譯生成so的方式有:ndk-build和cmake,本處只介紹NDK的方式。
從上圖可知,如果只是使用SDK即只可以使用JAVA,如果要調用底層則需要NDK通過JNI來實現。
so生成具體流程如下:
1、在defaultConfig中添加NDK配置代碼:
externalNativeBuild {
ndk {
abiFilters "x86", "armeabi-v7a" //最終要生成的.so所要支持的架構。
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
}
各CPU和ABI之間的關係:
CPU\ABI | armeabi | Armeabi-v7a | Arm64-v8a | X86 | X86_64 | Mips | Mips_64 |
---|---|---|---|---|---|---|---|
Armeabi | 支持 | ||||||
Armeabi-v7a | 支持 | 支持 | |||||
Arm64-v8a | 支持 | 支持 | 支持 | ||||
X86 | 支持 | ||||||
X86_64 | 支持 | 支持 | |||||
Mips | 支持 | ||||||
Mips_64 | 支持 | 支持 |
64位設備(arm64-v8a, x86_64, mips64)能夠運行32位的函數庫,但是是以32位模式運行,在64位平臺上運行32位版本的ART和Android組件,將丟失專爲64位優化過的性能(ART,webview,media等等)。
向後兼容(向下兼容):即armeabi-v7a CPU支持armeabi-v7a 和armeabi ABI,兼容老版本。
Android如何加載So庫?
每一個CPU架構對應一個ABI目錄,會自動到相應目錄下進行查找。比如Armeabi-v7a的設備會先找armeabi-v7a目錄,如果目錄存在但是so不存在則會直接報錯。如果目錄存在且so也存在,則會適配成功。如果沒有armeabi-v7a目錄,則會向下查找armeabi目錄,過程同上。
LOCAL_PATH := $(call my-dir) //設置本地路徑,my-dir即包含Android.mk的目錄,當前目錄
include $(CLEAR_VARS) //負責清理很多LOCAL_xxx,再進行新的設置前需要先清除
LOCAL_MODULE := myTest //生成庫的名字libmyTest.so
LOCAL_SRC_FILES := native-lib.cpp //包含需要編譯的源文件
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY) //生成動態庫
3、clean->build後在build\intermediates\ndkBuild下查看
Java調用.so步驟:
1、將so放到jniLibs目錄下或者放在libs目錄下然後在build.gradle中添加:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
2、在java中加載庫後進行調用相應的接口。
static {
System.loadLibrary("myTest");
}