主頁 > 移動端開發 > Android-JNI開發概論

Android-JNI開發概論

2023-06-18 08:09:29 移動端開發

什么是JNI開發

JNI的全稱是Java Native Interface,顧名思義,這是一種解決Java和C/C++相互呼叫的編程方式,它其實只解決兩個方面的問題,怎么找到和怎么訪問, 弄清楚這兩個話題,我們就學會了JNI開發,需要注意的是,JNI開發只涉及到一小部分C/C++開發知識,遇到問題的時候我們首先要判斷是C/C++的問題還是JNI的問題,這可以節省很多搜索和定位的時間,

用JVM的眼光看函式呼叫

我們知道Java程式是不能單獨運行的,它需要運行在JVM上的,而JVM卻又需要跑在物理機上,所以它的任務很重,既要處理Java代碼,又要處理各種作業系統,硬體等問題,可以說了解了JVM,就了解了Java的全部,當然包括JNI,所以我們先以JVM的身份來看看Java代碼是怎樣跑起來的吧(只是粗略的內容,省去了很多步驟,為了突出我們在意的部分),

運行Java代碼前,會先啟動一個JVM,在JVM啟動后,會加載一些必要的類,這些類中包含一個叫主類的類,也就是含有一個靜態成員函式,函式簽名為public static void main(String[] args)的方法,資源加載完成后,JVM就會呼叫主類的main方法,開始執行Java代碼,隨著代碼的執行,一個類依賴另一個類,層層依賴,共同完成了程式功能,這就是JVM的大概作業流程,可以說JVM就好比一座大橋,連接著Java大山和native大山,

現在問題來了,在Java程式中,某個類需要通過JNI技術訪問JVM以外的東西,那么它需要怎樣告訴我(我現在是JVM)呢?需要一種方法 把普通的Java方法標記成特殊,這個標記就是native關鍵字(使用Kotlin時雖然也可以使用這個關鍵字,但是Kotlin有自己的關鍵字external),當我執行到這個方法時,看到它不一樣的標記,我就會從其他地方而不是Class里面尋找執行體,這就是一次JNI呼叫,也就是說對于Java程式來說,只需要將一個方法標記為native,在需要的地方呼叫這個方法,就可以完成JNI呼叫了,但是對于我,該怎樣處理這一次JNI呼叫呢?其實上面的尋找執行體的程序是一個跳轉問題,在C/C++的世界,跳轉問題就是指標問題,那么這個指標它應該指向哪里呢?

C/C++代碼是一個個函式(下文會將Java方法直接用方法簡稱,而C/C++函式直接用函式簡稱)組合起來的,每一個函式都是一個指標,這個特性恰好滿足我的需要,但是對于我,外面世界那么大,我并不知道從哪里,找什么東西,給我的資訊還是不夠,為了限定范圍,我規定,只有通過System.loadLibrary(“xxx”)加載的函式,我才會查找,其余的我直接罷工(拋錯),這一下子減輕了我的作業量,至少我知道從哪里找了,

確定了范圍,下一步就是在這個范圍里確定真正的目標了,Java世界里怎樣唯一標識一個類呢,有的人會脫口而出——類名,其實不全對,因為類名可能會重名,我們需要全限定的類名,也就是包名加類名,如String的全限定類名就是java.lang.String,但是這和我們查找native的方法有什么聯系呢,當然有聯系,既然一個全限定的類名是唯一的,那么它的方法也是唯一的,那么假如我規定以這個類的全限定類名加上方法名作為native函式的函式名,這樣我是不是就可以通過函式名的方式找到native的函式看呢,答案是肯定的,但是有瑕疵,因為Java系統支持方法多載,也就是一個類里面,同名的方法可能有多個,那么構成多載的條件是什么呢,是引數串列不同,所以,結果就很顯然了,我在前面的基礎上再加上引數串列,組合成查找條件,我是不是就可以唯一確定某一個native函式了呢,這就是JNI的靜態注冊,

不過,既然我只需要確定指標的指向,那么我能不能直接給指標賦值,而不是每次都去查找呢,雖然我不知道累,但是還是很耗費時間的,對于這種需求,我當然也是滿足的啦,你直接告訴我,我就不找了,我還樂意呢,而且,既然你都給我找到了,我就不需要下那么多規定了,都放開,你說是我就相信你它是,這就是JNI的動態注冊,

JNI的函式注冊

上一節我們通過化身JVM的方式了解了JNI函式注冊的淵源,并且引出了兩種函式注冊方式,從例子上,我們也可以總結出兩種注冊方式的特點

注冊型別 優點 缺點
靜態注冊 JVM自動查找
實作簡單
函式名賊長,限制較多
查找耗時
動態注冊 運行快
對函式名無限制
實作復雜

那么具體怎么做呢?我們接著往下說,

靜態注冊

雖然靜態注冊限制比較多,但是都是一些淺顯的規則,更容易實施,所以先從靜態注冊開始講解,

靜態注冊有著明確的開發步驟

  1. 撰寫Java類,宣告native方法;
  2. 使用java xxx.java將Java源檔案編譯為class檔案
  3. 使用javah xxx生成對應的.h檔案
  4. 構建工具中引入.h檔案
  5. 實作.h檔案中的函式

上面的這個步驟是靜態開發的基本步驟,但是其實在如今強大的IDE面前,這些都不需要我們手動完成了,在Android Studio中,定義好native方法后,在方法上按alt + enter就可以生成正確的函式簽名,直接寫函式邏輯就可以了,但是學習一門學問,我們還是要抱著求真,求實的態度,所以我用一個例子來闡述一下這些規則,以加深讀者的理解,

Test.java

package me.hongui.demo

public class Test{
    native String jniString();
}

native-lib.cpp

#include <jni.h>

extern "C" jstring Java_me_hongui_demo_Test_jniString(JNIEnv *env, jobject thiz) {
    // TODO: implement jniString()
}

上面就是一個JNI函式在兩端宣告的例子,不難發現

  1. 函式簽名以Java_為前綴
  2. 前綴后面跟著類的全路徑,也就是包含包名和類名
  3. _作為路徑分隔符
  4. 函式的第一個引數永遠是JNIEnv *型別,第二個引數根據函式型別的不同而不同,static型別的方法,對應的是jclass型別,否則對應的是jobject型別,型別系統后面會詳細展開,

為什么Java方法對應到C/C++函式后,會多兩個引數呢,我們知道JVM是多執行緒的,而我們的JNI方法可以在任何執行緒呼叫,那么怎樣保證呼叫前后JVM能找到對應的執行緒呢,這就是函式第一個引數的作用,它是對執行緒環境的一種封裝,和執行緒一一對應,也就是說不能用一個執行緒的JNIEnv物件在另一個執行緒里使用,另外,它是一個C/C++訪問Java世界的視窗,JNI開發的絕大部分時間都是和JNIEnv打交道,

動態注冊

同樣按照開發程序,我們一步一步來完成,
我們把前面的Java_me_hongui_demo_Test_jniString函式名改成jniString(當然不改也可以,畢竟沒限制),引數串列保持不變,這時,我們就會發現Java檔案報錯了,說本地方法未實作,其實我們是實作了的,只是JVM找不到,為了讓JVM能找到,我們需要向JVM注冊,
那么怎么注冊,在哪注冊呢,似乎哪里都可以,又似乎都不可以,
前面說過,JVM只會查找通過System.loadLibrary(“xxx”); 加載的庫,所以要想使用native方法,首先要先加載包含該方法的庫檔案,之后,才可使用,加載了庫,說明Java程式要開始使用本地方法了,在加載庫之后,呼叫方法之前,理論上都是可以注冊方法的,但是時機怎么確定呢,JNI早就給我們安排好了,JVM在把庫加載進虛擬機后,會呼叫函式jint JNI_OnLoad(JavaVM *vm, void *reserved),以確認JNI的版本,版本資訊會以回傳值的形式傳遞給JVM,目前可選的值有JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6,假如庫沒有定義這個函式,那么默認回傳的是JNI_VERSION_1_1,庫將會加載失敗,所以,為了支持最新的特性我們通常回傳較高的版本,既然有了這么好的注冊時機,那么下一步就是實作注冊了,

但事情并沒有這么簡單,由JNI_OnLoad函式引數串列可知,目前,可供使用的只有JVM,但是查閱JVM的API,我們并沒有發現注冊的函式——注冊函式是寫在JNIEnv類里面的,恰巧的是,JVM提供了獲取JNIEnv物件的函式,

JVM有多個和JNIEnv相關的函式,在Android開發中,我們需要使用AttachCurrentThread來獲取JNIEnv物件,這個函式會回傳執行狀態,當回傳值等于JNI_OK的時候,說明獲取成功,有了JNIEnv物件,我們就可以注冊函式了,

先來看看注冊函式的宣告——jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods,回傳值不用多說,和AttachCurrentThread一樣,指示執行狀態,難點在引數上,第一個引數是jclass型別,第二個是JNINativeMethod指標,都是沒見過的主,

為什么需要這么多引數呢,JVM不只需要一個函式指標嗎,還是唯一性的問題,記得前面的靜態注冊嗎,靜態注冊用全限定型別和方法,引數串列,回傳值的組合確定了函式的唯一性,但是對于動態注冊,這些都是未知的,但是又是必須的,為了確定這些值,只能通過其他的方式,jclass就是限定方法的存在范圍,獲取jclass物件的方式也很簡單,使用JNIEnvjclass FindClass(const char* name)函式,引數需要串全限定符的類名,并且把.換成/,也就是類似me/hongui/demo/Test的形式,為啥這樣寫,后面會單獨拿一節出來細說,

第二個和第三個引陣列合起來就是常見的陣列引數形式,先來看看JNINativeMethod的定義,

typedef struct { 
    char *name; 
    char *signature; 
    void *fnPtr; 

} JNINativeMethod; 

有個撰寫訣竅,按定義順序,相關性是從Java端轉到C/C++端,怎么理解呢?name是只的Java端對應的native函式的名字,這是純Java那邊的事,Java那邊取啥名,這里就是啥名,第二個signature代表函式簽名,簽名資訊由引數串列和回傳值組成,形如(I)Ljava/lang/String;,這個簽名就是和兩邊都有關系了,首先Java那邊的native方法定義了引數串列和回傳值的型別,也就是限定了簽名的形式,其次Java的資料型別對應C/C++的轉換需要在這里完成,也就是引數串列和回傳值要寫成C/C++端的形式,這就是和C/C++相關了,最后一個fnPtr由名字也可得知它是一個函式指標,這個函式指標就是純C/C++的內容了,代表著Java端的native方法在C/C++對應的實作,也就是前文所說的跳轉指標的,知道了這些,其實我們還是寫不出代碼,因為,我們還有JNI的核心沒有說到,那就是型別系統,

JNI的型別系統

由于涉及到Java和C/C++兩個語言體系,JNI的型別系統很亂,但并非無跡可尋,首先需要明確的是,兩端都有自己的型別系統,Java里的booleanintString,C/C++的bool,int,string等等,遺憾的是,它們并不一一對應,也就是說C/C++不能識別Java的型別,既然型別不兼容,談何呼叫呢,這也就是JNI欲處理的問題,

JNI型別映射

為了解決型別不兼容的問題,JNI引入了自己的型別系統,型別系統里定義了和C/C++兼容的型別,并且還對Java到C/C++的型別轉換關系做了規定,怎么轉換的呢,這里有個表

Java型別 C/C++型別 描述
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

乍一看,沒什么特別的,不過就是加了j前綴(除了void),但是,這只是基本型別,我們應該沒忘記Java是純面向物件的語言吧,各種復雜物件才是Java的主戰場啊,而對于復雜物件,情況就復雜起來了,我們知道在Java中,任何物件都是Object類的子類,那么我們是否可以把除上面的基本型別以外的所有復雜型別都當作Object類的物件來處理呢,可是可以,但是不方便,像陣列,字串,例外等常用類,假如不做轉換使用起來比較繁瑣,為了方便我們開發,JNI又將復雜型別分為下面這幾種情況

jobject                     (所有的Java物件)
    |
    |--jclass               (java.lang.Class)
    |--jstring              (java.lang.String)
    |--jarray               (陣列)
    |     |
    |     |-- jobjectArray  (Object陣列)
    |     |-- jbooleanArray (boolean陣列)
    |     |-- jbyteArray    (byte陣列)
    |     |-- jcharArray    (char陣列)
    |     |-- jshortArray   (short陣列)
    |     |-- jintArray     (int陣列)
    |     |-- jlongArray    (long陣列)
    |     |-- jfloatArray   (float陣列)
    |     |-- jdoubleArray  (double陣列)
    |--jthrowable           (java.lang.Throwable例外)

兩個表合起來就是Java端到C/C++的型別轉換關系了,也就是說,當我們在Java里宣告native代碼時,native函式引數和回傳值的對應關系,也是C/C++呼叫Java代碼引數傳遞的對應關系,但是畢竟兩套系統還是割裂的,型別系統只定義了兼容方式,并沒有定義轉換方式,雙方的引數還是不能相互識別,所以,JNI又搞了個型別簽名,欲處理型別的自動轉換問題,

JNI的型別簽名

型別簽名和型別別映射類似,也有對應關系,我們先來看個對應關系表

型別簽名 Java型別
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ; fully-qualified-class
[type type[]
(arg-types)ret-type method type

對于基本型別,也很簡單,就是取了首字母,除了boolean(首字母被byte占用了),long(字母被用作了符合物件的前綴識別符號),
著重需要注意的是復合型別,也就是某個類的情況,它的簽名包含三部分,前綴L,中間是型別的全限定名稱,跟上后綴;,三者缺一不可,并且限定符的分隔符要用/替換, , 注意,型別簽名和型別系統不是一個概念,型別通常是純字串的,用在函式注冊等地方,被JVM使用的,型別系統是和普通型別一樣的,可以定義變數,作為引數串列,被用戶使用的, 另外,陣列物件也有自己的型別簽名,也是有著型別前綴[,后面跟著型別的簽名,最后的方法型別,也就是接下來我們著重要講的地方,它也是由三部分組成()和包含在()里面的引數串列,()后面的回傳值,這里用到的所有型別,都是指型別簽名,

我們來看個例子

long f (int n, String s, boolean[] arr); 

它的型別簽名怎么寫呢?我們來一步一步分析

  1. 確定它在Java里面的型別,在表中找出對應關系,確定簽名形式,
  2. 用步驟1的方法確定它的組成部分的型別,
  3. 將確定好的簽名組合在一起

此例是方法型別,對應表中最后一項,所以簽名形式為(引數)回傳值,該方法有三個引數,我們按照步驟1的方式逐一確定,

  1. int n對應int型別,簽名是I;
  2. String s對應String型別,是復合型別,對應表中倒數第三項,所以它的基本簽名形式是L全限定名;,而String的全限定名java.lang.String,用/替換,后變成java/lang/String,按步驟3,將它們組合在一起就是Ljava/lang/String;;
  3. boolean[] arr對應陣列型別,簽名形式是[型別boolean的簽名是Z,組合在一起就是[Z;
  4. 最后來看回傳值,回傳值是long型別,簽名形式是J

按照簽名形式將這些資訊組合起來就是(ILjava/lang/String;[Z)J注意型別簽名和簽名之間沒有任何分割符,也不需要,型別簽名是緊密排列的

再看動態注冊

有了JNI的型別系統的支持,回過頭來接著看動態注冊的例子,讓我們接著完善它,

  1. 用JVM物件獲取JNIEnv物件,即auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
  2. 用步驟1獲取的JNIEnv物件獲取jclass物件,即auto cls=jniEnv->FindClass("me/hongui/demo/Test");
  3. 定義JNINativeMethod陣列,即JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};,這里的方法簽名可以參看上一節,
  4. 呼叫JNIEnvRegisterNatives函式,即status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
  5. 當然,別忘了實作對應的native函式,即這里的jniString——JNINativeMethod的第三個引數,

這五步就是動態注冊中JNI_OnLoad函式的實作模板了,主要的變動還是來自jclass的獲取引數和JNINativeMethod的簽名等,必須做到嚴格的一一對應,如下面的例子

extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved){
    JNIEnv* jniEnv= nullptr;
    auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
    if(JNI_OK==status){
        JNINativeMethod methods[]={{"jniString", "()Ljava/lang/String;",reinterpret_cast<void *>(jniString)}};
        auto cls=jniEnv->FindClass("me/hongui/demo/Test");
        status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));
        if(JNI_OK==status) {
            return JNI_VERSION_1_6;
        }
    }
    return JNI_VERSION_1_1;
}

在JNI中使用資料

前面磨磨唧唧說了這么一大片,其實才講了一個問題——怎么找到,雖然繁雜,但好在有跡可循,大不了運行奔潰,下面要講的這個問題就棘手多了,需要一點點耐性和細心,這一部分也可以劃分成兩個小問題——訪問已知物件的資料,創建新物件,有一點還是要提一下,這里的訪問還創建都是針對Java程式而言的,也就是說,物件是存在JVM虛擬機的堆上的,我們的操作都是基于堆物件的操作, 而在C/C++的代碼里,操作堆物件的唯一途徑就是通過JNIenv提供的方法,所以,這部分其實就是對JNIenv方法的應用講解,

Java物件的訪問

在面向物件的世界中,我們說訪問物件,通常指兩個方面的內容,訪問物件的屬性、呼叫物件的方法,這些操作在Java世界中,很好實作,但是在C/C++世界卻并非如此,在JNI的型別系統那一節,我們也了解到,Java中的復雜物件在C/C++中都對應著jobject這個類,顯然,無論Java世界中,那個物件如何牛逼,在C/C++中都是一視同仁的,為了實作C/C++訪問Java的復雜物件,結合訪問物件的方式,JNIEnv提供了兩大類方法,一類是對應屬性的,一類是對應方法的,借助JNIEnv,C/C++就能實作訪問物件的目標了,而且它們還有一個較為統一的使用步驟:

  1. 根據要訪問的內容準備好對應id(fieldid或者methodid),
  2. 確定訪問的物件和呼叫資料
  3. 通過JNIEnv的方法呼叫完成物件訪問

可以看出來,這使用步驟和普通面向物件的方式多了一些準備階段(步驟1,2),之前提到過,這部分的內容需要的更多的是耐心和細心,不需要多少酷炫的操作,畢竟發揮空間也有限,這具體也體現在上面的步驟1,2,正是這個準備階段讓整個C/C++的代碼變得丑陋和脆弱,但是——又不是不能用,是吧,

看一個例子,Java里定義了一個Person類,類定義如下

public class Person(){
    private int age;
    private String name;

    public void setName(String name){
        return this.name=name;
    }
}

現在,我們在C/C++代碼里該怎么訪問這個類的物件呢,假定需要讀取這個物件的age值,設定這個物件的name值,根據上面的步驟,我們有以下步驟

  1. 準備好agefieldid,setNamemethodid,根據JNIEnv的方法,我們可以看到四個相關的,fieldid,methodid各兩個,分普通的和靜態的,我們這里都是普通的,所以確定的方法是GetFieldIDGetMethodID,第一個引數就是jclass物件,獲取方法前面已經說過,即通過JNIEnvFindClass方法,引數是全限定類名,以/替換.,后面兩個引數對應Java端的名稱和型別簽名,age屬于field,int的型別簽名是IsetName屬于method,簽名形式是(引數)回傳值,這里引數的簽名是Ljava/lang/String;,回傳值的簽名是V,組合起來就是"(Ljava/lang/String;)V"
  2. 假定我們已經有了Person物件obj,通過Java傳過來的,
  3. 分別需要呼叫兩個方法,age是整形屬性,要獲取它的值,對應就需要使用GetIntField方法,setName是回傳值為void的方法,所以應該使用CallVoidMethod

通過上面的分析,得出下面的示例代碼,

auto cls=jniEnv->FindClass("me/hongui/demo/Person");
auto ageId=jniEnv->GetFieldID(cls,"age","I");
auto nameId=jniEnv->GetMethodID(cls,"setName","(Ljava/lang/String;)V");
jint age=jniEnv->GetIntField(obj,ageId);
auto name=jniEnv->NewStringUTF("張三");
jniEnv->CallVoidMethod(obj,nameId,name);

從上面的分析和示例來看,耐心和細心主要體現在

  1. 對要訪問的屬性或者方法要耐心確定型別和名稱,并且要保持三個步驟中的型別要一一對應,即呼叫GetFieldID的型別要以GetXXXField的型別保持一致,方法也是一樣,
  2. 對屬性或方法的靜態非靜態修飾也要留心,通常靜態的都需要使用帶有static關鍵字的方法,普通的則不需要,如GetStaticIntField就是對應獲取靜態整型屬性的值,而GetIntField則是獲取普通物件的整型屬性值,
  3. 屬性相關的設定方法都是類似于SetXField的形式,里面的X代表著具體型別,和前面的型別系統中的型別一一對應,假如是復雜物件,則用Object表示,如SetObjectField,而訪問屬性只需要將前綴Set換成Get即可,對于靜態屬性,則是在SetX之間加上固定的Static,即SetStaticIntField這種形式,
  4. 方法呼叫則是以Call為前綴,后面跟著回傳值的型別,形如CallXMethod的形式,這里X代表回傳值,如CallVoidMethod就表示呼叫物件的某個回傳值為void型別的方法,同樣對應的靜態方法則是在CallX之間加上固定的Static,如CallStaticVoidMethod

向Java世界傳遞資料

向Java世界傳遞資料更需要耐心,因為我們需要不斷地構造物件,組合物件,設定屬性,而每一種都是上面Java物件的訪問的一種形式,

構造Java物件

C/C++構造Java物件和呼叫方法類似,但是,還是有很多值得關注的細節,根據前面的方法,我們構造物件,首先要知道構造方法的id,而得到id,我們需要得到jclass,構造方法的名字和簽名,我們知道在Java世界里,構造方法是和類同名的,但是在C/C++里并不是這樣,它有著特殊的名字——<init>,注意,這里的<>不能少,也就是說無論這個類叫什么,它的建構式的名字都是<init> 而函式簽名的關鍵點在于回傳值,構造方法的回傳值都是void也就是對應簽名型別V

接前面那個Person類的例子,要怎樣構造一個Person物件呢,

  1. 通過JNIEnvFindClass得到就jclass物件,記得將'替換成/
  2. 根據需要得到合適的構造方法的id,我沒有定義構造方法,那么編譯器會為它提供一個無參的構造方法,也就是函式簽名為()V,呼叫JNIEnvGetMethodID得到id,
  3. 呼叫JNIEnvNewObject創建物件,記得傳遞構造引數,我這里不需要傳遞,

綜上分析,這個創建程序類似于如下示例

auto cls=env->FindClass("me/hongui/demo/Person");
auto construct=env->GetMethodID(cls,"<init>","()V");
auto age=env->GetFieldID(cls,"age","I");
auto name=env->GetFieldID(cls,"name","Ljava/lang/String;");
auto p=env->NewObject(cls,construct);
auto nameValue=https://www.cnblogs.com/honguilee/archive/2023/06/17/env->NewStringUTF("張三");
env->SetIntField(p,age,18);
env->SetObjectField(p,name,nameValue);
return p

上面的示例有個有意思的點,其實示例中創建了兩個Java物件,一個是Person物件,另一個是String物件,因為在編程中,String出境的概率太大了,所以JNI提供了這個簡便方法,同樣特殊的還有陣列物件的創建,并且因為陣列型別不確定,還有多個版本的創建方法,如創建整型陣列的方法是NewIntArray,方法簽名也很有規律,都是NewXArray的形式,其中X代表陣列的型別,這些方法都需要一個引數,即陣列大小,既然提到了陣列,那么陣列的設定方法就不得不提,設定陣列元素的值也有對應的方法,形如SetXArrayRegion,如SetIntArrayRegion就是設定整型陣列元素的值,和Java世界不同的是,這些方法都是支持同時設定多個值的,整形陣列的簽名是這樣——void SetIntArrayRegion(jintArray array,jsize start, jsize len,const jint* buf)第二個引數代表設定值的開始索引,第三個引數是數目,第四個引數是指向真正值的指標,其余型別都是類似的,

讓資料訪問更進一步

有些時候,我們不是在呼叫native方法時訪問物件,而是在將來的某個時間,這在Java世界很好實作,總能找到合適的類存放這個呼叫時傳遞進來的物件參考,在后面使用時直接用就可以了,native世界也是這樣嗎?從使用流程上是一樣的,但是從實作方式上卻是很大不同,

Java世界是帶有GC的,也就是說,將某個臨時物件X傳遞給某個物件Y之后,X的生命周期被轉移到了Y上了,X不會在呼叫結束后被銷毀,而是在Y被回收的時候才會一同回收,這種方式在純Java的世界里沒有問題,但是當我們把這個臨時物件X傳遞給native世界,試圖讓它以Java世界那樣作業時,應用卻崩潰了,報錯JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0xxxxx,為什么同樣的操作在Java里面可以,在native卻不行呢,問題的根源就是Java的GC,GC可以通過各種垃圾檢測演算法判斷某個物件是否需要標記為垃圾,而在native世界,不存在GC,為了不造成記憶體泄漏,只能采取最嚴格的策略,默認呼叫native方法的地方就是使用Java物件的地方,所以在native方法呼叫的作用域結束后,臨時物件就被GC標記為垃圾,后面想再使用,可能已經被回收了,還好,強大的JNIEnv類同樣提供了方法讓我們改變這種默認策略——NewGlobalRef,物件只需要通過這種方式告訴JVM,它想活得更久一點,JVM在執行垃圾檢測的時候就不會把它標記為垃圾,這個物件就會一直存,在,直到呼叫DeleteGlobalRef這里NewGlobalRefDeleteGlobalRef是一一對應的,而且最好是再不需要物件的時候就呼叫DeleteGlobalRef釋放記憶體,避免記憶體泄漏,

總結

JNI開發會涉及到Java和C/C++開發的知識,在用C/C++實作JNI時,基本思想就是用C/C++語法寫出Java的邏輯,也就是一切為Java服務,JNI開發程序中,主要要處理兩個問題,函式注冊和資料訪問,

函式注冊推薦使用動態注冊,在JNI_OnLoad函式中使用JNIEnvRegisterNatives注冊函式,注意保持Java的native方法和型別簽名的一致性,復合型別不要忘記前綴L、后綴;,并將.替換為/

資料訪問首先需要確定訪問周期,需要在多個地方或者不同時間段訪問的物件,記得使用NewGlobalRef阻止物件被回收,當然還要記得DeleteGlobalRef,訪問物件需要先拿到相應的id,然后根據訪問型別確定訪問方法,設定屬性通常是SetXField的形式,獲取屬性值通常是GetXField的形式,呼叫方法,需要根據回傳值的型別確定呼叫方法,通常是CallXMethod的形式,當然,這些都是針對普通物件的,假如需要訪問靜態屬性或者方法,則是在普通版本的X前面加上Static,這里的所有X都是指代型別,除了基本型別外,其他物件都用Object替換,

在注冊函式和訪問資料的時候需要時刻關注的就是資料型別,C/C++資料型別除了基本型別外都不能直接傳遞到Java里,需要通過創建物件的方式傳遞,一般的創建物件方式NewObject可以創建任何物件,而對于使用頻繁的字串和陣列有對應的快速方法NewStringUTFNewXArray,向Java傳遞字串和陣列,這兩個方法少不了,

青山不改,綠水長流,咱們下期見!

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/555485.html

標籤:其他

上一篇:Android-NDK開發——基本概念

下一篇:返回列表

標籤雲
其他(161221) Python(38240) JavaScript(25505) Java(18245) C(15237) 區塊鏈(8271) C#(7972) AI(7469) 爪哇(7425) MySQL(7256) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5875) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4603) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2436) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1984) HtmlCss(1968) 功能(1967) Web開發(1951) C++(1941) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1881) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • Android-JNI開發概論

    ### 什么是JNI開發 JNI的全稱是Java Native Interface,顧名思義,這是一種解決Java和C/C++相互呼叫的編程方式。**它其實只解決兩個方面的問題,怎么找到和怎么訪問。** 弄清楚這兩個話題,我們就學會了JNI開發。**需要注意的是,JNI開發只涉及到一小部分C/C++ ......

    uj5u.com 2023-06-18 08:09:29 more
  • Android-NDK開發——基本概念

    在Android開發中,有時候出于安全,性能,代碼共用的考慮,需要使用C/C++撰寫的庫。雖然在現代化工具鏈的支持下,這個作業的難度已經大大降低,但是畢竟萬事開頭難,初學者往往還是會遇到很多不可預測的問題。本篇就是基于此背景下寫的一份簡陋指南,希望能對剛開始撰寫C/C++庫的讀者有所幫助。同時為了盡 ......

    uj5u.com 2023-06-18 08:09:06 more
  • Android-JNI開發概論

    ### 什么是JNI開發 JNI的全稱是Java Native Interface,顧名思義,這是一種解決Java和C/C++相互呼叫的編程方式。**它其實只解決兩個方面的問題,怎么找到和怎么訪問。** 弄清楚這兩個話題,我們就學會了JNI開發。**需要注意的是,JNI開發只涉及到一小部分C/C++ ......

    uj5u.com 2023-06-18 08:09:00 more
  • Kotlin協程-從理論到實戰

    > 上一篇文章從理論上對Kotlin協程進行了部分說明,本文將在上一篇的基礎上,從實戰出發,繼續協程之旅。 ### 從源頭說起 在Kotlin中,要想使用協程,首先需要使用協程創建器創建,但還有個前提——協程作用域(`CoroutineScope`)。在早期的Kotlin實作中,協程創建器是一等函式 ......

    uj5u.com 2023-06-17 08:16:30 more
  • Kotlin協程-從一到多

    > 上一篇文章,我介紹了Kotlin協程的創建,使用,協作等內容。本篇將引入更多的使用場景,繼續帶你走進協程世界。 ### 使用協程處理異步資料流 常用編程語言都會內置對同一型別不同物件的資料集表示,我們通常稱之為容器類。不同的容器類適用于不同的使用場景。Kotlin的`Flow`就是在異步計算的需 ......

    uj5u.com 2023-06-17 08:16:25 more
  • 【有獎調研】HarmonyOS新物種,鴻蒙流量新陣地——元服務邀你來答

    “聊技術無話不談,一起來吹吹元服務!暢聊你對元服務的想法,說不定,你就能撬動元服務的爆發增長!” 元服務(即原子化服務)是華為“輕量化”服務的新物種,可提供全新的服務和互動方式,讓應用化繁為簡,讓服務觸手可及!基于鴻蒙萬能卡片,元服務可實作應用功能在桌面“永遠打開”,實作智能推薦、服務直達! 而在元 ......

    uj5u.com 2023-06-17 08:16:19 more
  • Kotlin協程-從理論到實戰

    > 上一篇文章從理論上對Kotlin協程進行了部分說明,本文將在上一篇的基礎上,從實戰出發,繼續協程之旅。 ### 從源頭說起 在Kotlin中,要想使用協程,首先需要使用協程創建器創建,但還有個前提——協程作用域(`CoroutineScope`)。在早期的Kotlin實作中,協程創建器是一等函式 ......

    uj5u.com 2023-06-17 08:15:56 more
  • Kotlin協程-從一到多

    > 上一篇文章,我介紹了Kotlin協程的創建,使用,協作等內容。本篇將引入更多的使用場景,繼續帶你走進協程世界。 ### 使用協程處理異步資料流 常用編程語言都會內置對同一型別不同物件的資料集表示,我們通常稱之為容器類。不同的容器類適用于不同的使用場景。Kotlin的`Flow`就是在異步計算的需 ......

    uj5u.com 2023-06-17 08:15:51 more
  • 【有獎調研】HarmonyOS新物種,鴻蒙流量新陣地——元服務邀你來答

    “聊技術無話不談,一起來吹吹元服務!暢聊你對元服務的想法,說不定,你就能撬動元服務的爆發增長!” 元服務(即原子化服務)是華為“輕量化”服務的新物種,可提供全新的服務和互動方式,讓應用化繁為簡,讓服務觸手可及!基于鴻蒙萬能卡片,元服務可實作應用功能在桌面“永遠打開”,實作智能推薦、服務直達! 而在元 ......

    uj5u.com 2023-06-16 08:48:47 more
  • 100個物聯網專案(基于ESP32)1ESP32的基礎

    ## 1-NodeMCU、ESP32的基礎 ### 簡介 NodeMCU是一個開源的IoT(物聯網)平臺,包括在樂鑫的ESP8266 Wi-Fi SoC上運行的韌體和基于ESP-12模塊的硬體。它是由一樂鑫在2014年創建的,他們希望為物聯網專案提供低成本和靈活的平臺。ESP32是低成本的微芯片,具 ......

    uj5u.com 2023-06-16 08:30:36 more