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編寫大致過程

  1. 首先聲明一個native方法
  2. 通過javah生成相應的頭文件(有的會先javac生成class文件,試了直接javah好像也沒有問題= 。=)
  3. 實現頭文件中聲明函數的方法
  4. 編譯生成so文件
  5. 在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目錄,過程同上。

2、在jni目錄下創建Android.mk

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");
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章