Android JNI(一)——NDK與JNI基礎

原文鏈接:https://www.jianshu.com/p/87ce6f565d37

https://www.jianshu.com/p/87ce6f565d37

 

本系列文章如下:

本片文章大綱如下:

  • 1、導讀
  • 2、什麼是NDK
  • 3、爲什麼使用NDK
  • 4、NDK到SO
  • 5、JNI

大綱.png

一、導讀

在Android OS上開發應用程序,Google提供了兩種開發包:SDK和NDK。你可以從Google官方查閱到有許多關於SDK的優秀書籍、文章作爲參考,但是Google提供的NDK資源,相對於SDK還是比較少的。本系列文章主要是用於,自己記錄自學NDK的經驗,並且希望能夠幫助到哪些想學習NDK的朋友。

Android 平臺從一開就已經支持了C/C++了。我們知道Android的SDK主要是基於Java的,所以導致了在用Android SDK進行開發的工程師們都必須使用Java語言。不過,Google從一開始就說明Android也支持JNI編程方式,也就是第三方應用完成可以通過JNI調用自己的C動態度。於是NDK就應運而生了。

二、什麼是NDK

NDK 其中NDK的全拼是:Native Develop Kit。

那我們先來看下Android NDK官網是對NDK怎麼解釋的

NDK官網.png

 

關鍵文字如下:

Android NDK 是一套允許您使用原生代碼語言(例如C和C++) 實現部分應用的工具集。在開發某些類型應用時,這有助於您重複使用以這些語言編寫的代碼庫。

簡單的來說:

Android NDK 就是一套工具集合,允許你使用C/C++語言來實現應用程序的部分功能。

NDK 是Native Develop Kit的含義,從含義很容易理解,本地開發。大家都知道,Android 開發語言是Java,不過我們也知道,Android是基於Linux的,其核心庫很多都是C/C++的,比如Webkit等。那麼NDK的作用,就是Google爲了提供給開發者一個在Java中調用C/C++代碼的一個工作。NDK本身其實就是一個交叉工作鏈,包含了Android上的一些庫文件,然後,NDK爲了方便使用,提供了一些腳本,使得更容易的編譯C/C++代碼。總之,在Android的SDK之外,有一個工具就是NDK,用於進行C/C++的開發。一般情況,是用NDK工具把C/C++編譯爲.co文件,然後在Java中調用。

NDK不適用於大多數初學的Android工程師,對於許多類型的Android應用沒有什麼價值。因爲它不可避免地會增加開發過程的複雜性,所以一般很少使用。那爲什麼Google還提供NDK,我們就一起研究下

三、爲什麼使用NDK

上面提及了 NDK不適合大多數初級Android 工程師,由於它增加了開發的複雜度,所以對許多類型的Android其實也沒有大的作用。不過在下面的需求下,NDK還是有很大的價值的:

  • 1、在平臺之間移植其應用
  • 2、重複使用現在庫,或者提供其自己的庫重複使用
  • 3、在某些情況下提性能,特別是像遊戲這種計算密集型應用
  • 4、使用第三方庫,現在許多第三方庫都是由C/C++庫編寫的,比如Ffmpeg這樣庫。
  • 5、不依賴於Dalvik Java虛擬機的設計
  • 6、代碼的保護。由於APK的Java層代碼很容易被反編譯,而C/C++庫反編譯難度大。

四、NDK到so

ndk到so.png

從上圖這個Android系統框架來看,我們上層通過JNI來調用NDK層的,使用這個工具可以很方便的編寫和調試JNI的代碼。因爲C語言的不跨平臺,在Mac系統的下使用NDK編譯在Linux下能執行的函數庫——so文件。其本質就是一堆C、C++的頭文件和實現文件打包成一個庫。目前Android系統支持以下七種不用的CPU架構,每一種對應着各自的應用程序二進制接口ABI:(Application Binary Interface)定義了二進制文件(尤其是.so文件)如何運行在相應的系統平臺上,從使用的指令集,內存對齊到可用的系統函數庫。對應關係如下:

  • ARMv5——armeabi
  • ARMv7 ——armeabi-v7a
  • ARMv8——arm64- v8a
  • x86——x86
  • MIPS ——mips
  • MIPS64——mips64
  • x86_64——x86_64

五、JNI

(一) 什麼是JNI?

oracle中關於JNI的指導

Java調用C/C++在Java語言裏面本來就有的,並非Android自創的,即JNI。JNI就是Java調用C++的規範。當然,一般的Java程序使用的JNI標準可能和android不一樣,Android的JNI更簡單。

JNI,全稱爲Java Native Interface,即Java本地接口,JNI是Java調用Native 語言的一種特性。通過JNI可以使得Java與C/C++機型交互。即可以在Java代碼中調用C/C++等語言的代碼或者在C/C++代碼中調用Java代碼。由於JNI是JVM規範的一部分,因此可以將我們寫的JNI的程序在任何實現了JNI規範的Java虛擬機中運行。同時,這個特性使我們可以複用以前用C/C++寫的大量代碼JNI是一種在Java虛擬機機制下的執行代碼的標準機制。代碼被編寫成彙編程序或者C/C++程序,並組裝爲動態庫。也就允許非靜態綁定用法。這提供了一個在Java平臺上調用C/C++的一種途徑,反之亦然。

PS:
開發JNI程序會受到系統環境限制,因爲用C/C++ 語言寫出來的代碼或模塊,編譯過程當中要依賴當前操作系統環境所提供的一些庫函數,並和本地庫鏈接在一起。而且編譯後生成的二進制代碼只能在本地操作系統環境下運行,因爲不同的操作系統環境,有自己的本地庫和CPU指令集,而且各個平臺對標準C/C++的規範和標準庫函數實現方式也有所區別。這就造成了各個平臺使用JNI接口的Java程序,不再像以前那樣自由的跨平臺。如果要實現跨平臺, 就必須將本地代碼在不同的操作系統平臺下編譯出相應的動態庫。

(二) 爲什麼需要JNI

因爲在實際需求中,需要Java代碼與C/C++代碼進行交互,通過JNI可以實現Java代碼與C/C++代碼的交互

(三) JNI的優勢

與其它類似接口Microsoft的原始本地接口等相比,JNI的主要競爭優勢在於:它在設計之初就確保了二進制的兼容性,JNI編寫的應用程序兼容性以及其再某些具體平臺上的Java虛擬機兼容性(當談及JNI時,這裏並不特比針對Davik虛擬機,JNI適用於所有JVM虛擬機)。這就是爲什麼C/C++編譯後的代碼無論在任何平臺上都能執行。不過,一些早期版本並不支持二進制兼容。二進制兼容性是一種程序兼容性類型,允許一個程序在不改變其可執行文件的條件下在不同的編譯環境中工作。

(四) JNI的三個角色

JNI的三個角色.png

 

JNI下一共涉及到三個角色:C/C++代碼、本地方法接口類、Java層中具體業務類。

JNI簡要流程

 

簡要流程.png

(五) JNI的命名規則

隨便舉例如下:

JNIExport jstring JNICALL Java_com_example_hellojni_MainActivity_stringFromJNI( JNIEnv* env,jobject thiz ) 

jstring 是返回值類型
Java_com_example_hellojni 是包名
MainActivity 是類名
stringFromJNI 是方法名

其中JNIExportJNICALL是不固定保留的關鍵字不要修改

(六) 如何實現JNI

JNI開發流程的步驟:

  • 第1步:在Java中先聲明一個native方法
  • 第2步:編譯Java源文件javac得到.class文件
  • 第3步:通過javah -jni命令導出JNI的.h頭文件
  • 第4步:使用Java需要交互的本地代碼,實現在Java中聲明的Native方法(如果Java需要與C++交互,那麼就用C++實現Java的Native方法。)
  • 第5步:將本地代碼編譯成動態庫(Windows系統下是.dll文件,如果是Linux系統下是.so文件,如果是Mac系統下是.jnilib)
  • 第6步:通過Java命令執行Java程序,最終實現Java調用本地代碼。

PS:javah 是JDK自帶的一個命令,-jni參數表示將class 中用到native 聲明的函數生成JNI 規則的函數

如下圖:

JNI開發流程.png

(七) JNI結構

JNI結構.png

這張JNI函數表的組成就像C++的虛函數表。虛擬機可以運行多張函數表,舉例來說,一張調試函數表,另一張是調用函數表。JNI接口指針僅在當前線程中起作用。這意味着指針不能從一個線程進入另一個線程。然而,可以在不同的咸亨中調用本地方法。

JNI接口.png

示例代碼

jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s)
{
     const char *str = (*env)->GetStringUTFChars(env, s, 0); 
     (*env)->ReleaseStringUTFChars(env, s, str); 
     return 10;
}

裏面的方法有三個入參,我們就依次來看下:

  • *env:一個接口指針
  • obj:在本地方法中聲明的對象引用
  • i和s:用於傳遞的參數

關於obj、i和s的類型大家可以參考下面的JNI數據類型,JNI有自己的原始數據類型和數據引用類型如下:

JNI的原始數據類型.png

關於 env,會在下面JNI原理*中講解。

(八) JNI原理

在計算機系統中,每一種編程語言都有一個執行環境(Runtime),執行環境用來解釋執行語言中的語句,不同的編程語言的執行環境就好比神話世界中的"陰陽兩界"一樣,一般人不能同時生存在陰陽兩界中,只有一些特殊的仙人——"黑白無常"才能自由穿梭在"陰陽兩界",而"黑白無常"往返於陰陽兩界時手持生日薄,"黑白無常"按生死薄上記錄的任命來"索魂"。

Java語言的執行環境是Java虛擬機(JVM),JVM其實是主機環境中的一個進程,每個JVM虛擬機都在本地環境中有一個JavaVM結構體,該結構體在創建Java虛擬機時被返回,在JNI環境中創建JVM的函數爲JNI_CreateJavaVM。

JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);

1、JavaVM

JVM與JavaVM.png

其中JavaVM是Java虛擬機在JNI層的代表,JNI全局僅僅有一個JavaVM結構中封裝了一些函數指針(或叫函數表結構),JavaVM中封裝的這些函數指針主要是對JVM操作接口。另外,在C和C++中的JavaVM的定義有所不同,在C中JavaVM是JNIInvokeInterface_類型指針,而在C++中有對JNIInvokeInterface_進行了一次封裝,比C中少了一個參數,這也是爲什麼JNI代碼更推薦使用C++來編寫的原因。

下面我們來重點說一下JNIEnv

2、JNIEnv

JNIEnv是當前Java線程的執行環境,一個JVM對應一個JavaVM結構,而一個JVM中可能創建多個Java線程,每個線程對應一個JNIEnv結構,它們保存在線程本地存儲TLS中。因此,不同的線程的JNIEnv是不同,也不能相互共享使用。JNIEnv結構也是一個函數表,在本地代碼中通過JNIEnv的函數表來操作Java數據或者調用Java方法。也就是說,只要在本地代碼中拿到了JNIEnv結構,就可以在本地代碼中調用Java代碼。

JVM與JNIEnv.png

2.1JNIEnv是什麼?

JNIEnv是一個線程相關的結構體,該結構體代表了Java在本線程的執行環境

2.2、JNIEnv和JavaVM的區別:

  • JavaVM:JavaVM是Java虛擬機在JNI層的代表,JNI全局僅僅有一個
  • JNIEnv:JavaVM 在線程中的代碼,每個線程都有一個,JNI可能有非常多個JNIEnv;

2.3、JNIEnv的作用:

  • 調用Java 函數:JNIEnv代表了Java執行環境,能夠使用JNIEnv調用Java中的代碼
  • 操作Java代碼:Java對象傳入JNI層就是jobject對象,需要使用JNIEnv來操作這個Java對象

2.4、JNIEnv的創建與釋放

2.4.1、JNIEnv的創建

JNIEnv 創建與釋放:從JavaVM獲得,這裏面又分爲C與C++,我們就依次來看下:

  • C 中——JNIInvokeInterface:JNIInvokeInterface是C語言環境中的JavaVM結構體,調用 (AttachCurrentThread)(JavaVM, JNIEnv*, void) 方法,能夠獲得JNIEnv結構體
  • C++中 ——_JavaVM:_JavaVM是C++中JavaVM結構體,調用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 方法,能夠獲取JNIEnv結構體;

2.4.2、JNIEnv的釋放

  • C 中釋放:調用JavaVM結構體JNIInvokeInterface中的(DetachCurrentThread)(JavaVM)方法,能夠釋放本線程的JNIEnv
  • C++ 中釋放:調用JavaVM結構體_JavaVM中的jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 方法,就可以釋放 本線程的JNIEnv

2.5、JNIEnv與線程

JNIEnv是線程相關的,即在每一個線程中都有一個JNIEnv指針,每個JNIEnv都是線程專有的,其他線程不能使用本線程中的JNIEnv,即線程A不能調用線程B的JNIEnv。所以JNIEnv不能跨線程。

  • JNIEnv只在當前線程有效:JNIEnv僅僅在當前線程有效,JNIEnv不能在線程之間進行傳遞,在同一個線程中,多次調用JNI層方便,傳入的JNIEnv是同樣的
  • 本地方法匹配多個JNIEnv:在Java層定義的本地方法,能夠在不同的線程調用,因此能夠接受不同的JNIEnv

2.6、JNIEnv結構

JNIEnv是一個指針,指向一個線程相關的結構,線程相關結構,線程相關結構指向JNI函數指針數組,這個數組中存放了大量的JNI函數指針,這些指針指向了詳細的JNI函數。

 

JNIEnv結構.png

2.7、與JNIEnv相關的常用函數

2.7.1 創建Java中的對象

  • jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...):
  • jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args):
  • jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args):

第一個參數jclass class 代表的你要創建哪個類的對象,第二個參數,jmethodID methodID代表你要使用那個構造方法ID來創建這個對象。只要有jclass和jmethodID,我們就可以在本地方法創建這個Java類的對象。

2.7.2 創建Java類中的String對象

  • jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len):

通過Unicode字符的數組來創建一個新的String對象。
env是JNI接口指針;unicodeChars是指向Unicode字符串的指針;len是Unicode字符串的長度。返回值是Java字符串對象,如果無法構造該字符串,則爲null。

那有沒有一個直接直接new一個utf-8的字符串的方法呢?答案是有的,就是jstring NewStringUTF(JNIEnv *env, const char *bytes)這個方法就是直接new一個編碼爲utf-8的字符串。

2.7.3 創建類型爲基本類型PrimitiveType的數組

  • ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);
    指定一個長度然後返回相應的Java基本類型的數組
方法 返回值
New<PrimitiveType>Array Routines Array Type
NewBooleanArray() jbooleanArray
NewByteArray() jbyteArray
NewCharArray() jcharArray
NewShortArray() jshortArray
NewIntArray() jintArray
NewLongArray() jlongArray
NewFloatArray() jfloatArray
NewDoubleArray() jdoubleArray

用於構造一個新的數組對象,類型是原始類型。基本的原始類型如下:

方法 返回值
New<PrimitiveType>Array Routines Array Type
NewBooleanArray() jbooleanArray
NewByteArray() jbyteArray
NewCharArray() jcharArray
NewShortArray() jshortArray
NewIntArray() jintArray
NewLongArray() jlongArray
NewFloatArray() jfloatArray
NewDoubleArray() jdoubleArray

2.7.4 創建類型爲elementClass的數組

  • jobjectArray NewObjectArray(JNIEnv *env, jsize length,
    jclass elementClass, jobject initialElement);

造一個新的數據組,類型是elementClass,所有類型都被初始化爲initialElement。

2.7.5 獲取數組中某個位置的元素

jobject GetObjectArrayElement(JNIEnv *env,
jobjectArray array, jsize index);

返回Object數組的一個元素

2.7.6 獲取數組的長度

jsize GetArrayLength(JNIEnv *env, jarray array);

獲取array數組的長度.

關於JNI的常用方法,我們會在後面一期詳細介紹。文檔可以參考https://docs.oracle.com

(九) JNI的引用

Java內存管理這塊是完全透明的,new一個實例時,只知道創建這個類的實例後,會返回這個實例的一個引用,然後拿着這個引用去訪問它的成員(屬性、方法),完全不用管JVM內部是怎麼實現的,如何爲新建的對象申請內存,使用完之後如何釋放內存,只需要知道有個垃圾回收器在處理這些事情就行了,然而,從Java虛擬機創建的對象傳到C/C++代碼就會產生引用,根據Java的垃圾回收機制,只要有引用存在就不會觸發該該引用所指向Java對象的垃圾回收。

在JNI規範中定義了三種引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。區別如下:

在JNI中也同樣定義了類似與Java的應用類型,在JNI中,定義了三種引用類型:

  • 局部引用(Local Reference)
  • 全局引用(Global Reference)
  • 弱全局引用(Weak Global Reference)

下面我們就依次來看下:

1、局部引用(Local Reference)

局部引用,也成本地引用,通常是在函數中創建並使用。會阻止GC回收所有引用對象。

最常見的引用類型,基本上通過JNI返回來的引用都是局部引用,例如使用NewObject,就會返回創建出來的實例的局部引用,局部引用值在該native函數有效,所有在該函數中產生的局部引用,都會在函數返回的時候自動釋放(freed),也可以使用DeleteLocalRef函數手動釋放該應用。之所以使用DeleteLocalRef函數:實際上局部引用存在,就會防止其指向對象被垃圾回收期回收,尤其是當一個局部變量引用指向一個很龐大的對象,或是在一個循環中生成一個局部引用,最好的做法就是在使用完該對象後,或在該循環尾部把這個引用是釋放掉,以確保在垃圾回收器被觸發的時候被回收。在局部引用的有效期中,可以傳遞到別的本地函數中,要強調的是它的有效期仍然只是在第一次的Java本地函數調用中,所以千萬不能用C++全部變量保存它或是把它定義爲C++靜態局部變量。

2、全局引用(Global Reference)

全局引用可以跨方法、跨線程使用,直到被開發者顯式釋放。類似局部引用,一個全局引用在被釋放前保證引用對象不被GC回收。和局部應用不同的是,沒有俺麼多函數能夠創建全局引用。能創建全部引用的函數只有NewGlobalRef,而釋放它需要使用ReleaseGlobalRef函數

3、弱全局引用(Weak Global Reference)

是JDK 1.2 新增加的功能,與全局引用類似,創建跟刪除都需要由編程人員來進行,這種引用與全局引用一樣可以在多個本地帶阿媽有效,不一樣的是,弱引用將不會阻止垃圾回收期回收這個引用所指向的對象,所以在使用時需要多加小心,它所引用的對象可能是不存在的或者已經被回收。

通過使用NewWeakGlobalRef、ReleaseWeakGlobalRef來產生和解除引用。

4、引用比較

在給定兩個引用,不管是什麼引用,我們只需要調用IsSameObject函數來判斷他們是否是指向相同的對象。代碼如下:

(*env)->IsSameObject(env, obj1, obj2)

如果obj1和obj2指向相同的對象,則返回JNI_TRUE(或者1),否則返回JNI_FALSE(或者0),

PS:有一個特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null對象,如果obj是一個全局或者局部引用,使用(*env)->IsSameObject(env, obj, NULL)或者obj == NULL用來判斷obj是否指向一個null對象即可。但是需要注意的是,IsSameObject用於弱全局引用與NULL比較時,返回值的意義是不同於局部引用和全局引用的。代碼如下:

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 業務邏輯處理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);

自此,關於NDK與JNI基礎已經講解完畢,下一篇文章,讓我們來了解一下Android JNI學習(二)——實戰JNI之“hello world”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章