實現一個在JNI中調用Java對象的工具類,從此一行代碼就搞定!

2021-12-26 BennuCTech
前言

我們知道在jni中執行一個java函數需要調用幾行代碼才行,如

jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
jobject result = (*env).CallObjectMethod(obj, methodID, ...);

這樣使用起來很不方便,尤其當需要大量的調用java函數就會產生大量的上述代碼,由此我產生了一個開發封裝這些操作的工具類,以便大量簡化我們的開發。

簡單封裝

其實可以看到整個過程基本是固定不變的:先獲取Class,然後獲取method,然後在執行call。所以可以簡單的先封裝成一系列工具函數,如:

jobject callObjMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jobject result = (*env).CallObjectMethodV(obj, methodID, args);
va_end(args);
return result;
}

jint callIntMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jint result = (*env).CallIntMethodV(obj, methodID, args);
va_end(args);
return result;
}

jboolean callBooleanMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jboolean result = (*env).CallBooleanMethodV(obj, methodID, args);
va_end(args);
return result;
}

這樣當我們要通過jni執行某個java函數的時候,就一行代碼就可以搞定了,比如String.length():

jint len = callIntMethod(env, str, "length", "()I")

這樣就可以大大減少了代碼量,而且代碼也更易讀了。

優化

通過上面可以看到這些函數大部分代碼都非常類似,只有一行代碼和返回值有區別,所以我考慮使用函數模版來進行優化,如下:

template <typename T>
T callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
T result;
if(typeid(T) == typeid(jobject)){
result = (*env).CallObjectMethodV(obj, methodID, args);
}
if(typeid(T) == typeid(jdouble)){
result = (*env).CallDoubleMethodV(obj, methodID, args);
}
...
va_end(args);
return *result;
}

這樣只要調用callMethod<return type>即可,願望很美好,但是上面代碼實際上是無法通過編譯。

因為模版函數實際上是在編譯時,根據調用的類型,拷貝生成多個具體類型的函數以便使用。

所以如果有這樣的調用callMethod<jobject>(...),在編譯時就會拷貝成一個如下的函數:

jobject callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
jobject result;
if(typeid(jobject) == typeid(jobject)){
result = (*env).CallObjectMethodV(obj, methodID, args);
}
if(typeid(jobject) == typeid(jdouble)){
result = (*env).CallDoubleMethodV(obj, methodID, args);
}
...
va_end(args);
return *result;
}

注意這行代碼:

if(typeid(jobject) == typeid(jdouble)){
result = (*env).CallDoubleMethodV(obj, methodID, args);
}

雖然實際上是無法執行的代碼,但是編譯時還是會進行檢查,由於將jdouble類型的賦值給jobject類型的result,所以編譯不通過,類型無法轉換。而且這裡用強轉static_cast等方法都不行。

我考慮兩種方法來解決這個問題,一種是保證編譯不報錯,因為運行時不會執行的代碼,只要通過編譯就可以。另外一種是不同的類型編譯不同的代碼。

void指針

在c++中void指針可以被賦值任何類型指針,且void指針強轉為任何類型指針在編譯時不會報錯。代碼如下:

template <typename T>
T callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
T* result = new T();
if(typeid(T) == typeid(jobject)){
jobject objec = (*env).CallObjectMethodV(obj, methodID, args);
void *p = &objec;
result = (T*)p;
}
if(typeid(T) == typeid(jdouble)){
jdouble doub = (*env).CallDoubleMethodV(obj, methodID, args);
void *p = &doub;
result = (T*)p;
}
va_end(args);
return *result;
}

當然利用void指針很不安全,雖然可以通過編譯,但是執行時如果類型不同會直接造成crash。所以並不建議這種方式。

模版函數特例化

將差異代碼部分封裝到另一個模版函數中,並且對每種類型進行特例化,這樣還可以去掉if-else判斷,代碼如下:

template <typename K>
K call2Result(JNIEnv *env, jobject obj, jmethodID methodID, va_list args){
return *(new K());
}

template <>
jobject call2Result(JNIEnv *env, jobject obj, jmethodID methodID, va_list args){
return (*env).CallObjectMethodV(obj, methodID, args);
}

template <>
jdouble call2Result(JNIEnv *env, jobject obj, jmethodID methodID, va_list args){
return (*env).CallDoubleMethodV(obj, methodID, args);
}
...

template <typename T>
T callMethod(JNIEnv *env, jobject obj, const char *methodName, const char *methodSig, ...){
va_list args;
jclass objClass = (*env).GetObjectClass(obj);
jmethodID methodID = (*env).GetMethodID(objClass, methodName, methodSig);
va_start(args,methodSig);
T result = call2Result<T>(env, obj, methodID, args);
va_end(args);
return result;
}

這樣在編譯時,如果返回值是jobject類型的,當編譯到call2Result時,就會直接調用jobject call2Result(...)這個函數,就不再涉及類型轉換的問題。

這樣去掉了if判斷,但是由於沒有通用的函數,所以所有使用的類型都需要特例化,如果某個類型未特例化,代碼執行可能就會有問題。而在jni中,與java對應的類型其實就那麼十幾種,所以我們只要全部實現一遍call2Result即可。

undefined reference to

使用模版函數出現這個問題,是因為沒有將模版函數的實現寫在頭文件中,只將模版函數的聲明在頭文件中,而在源文件中實現的。

所以我們應該將模版函數的實現也寫進頭文件中,而模版函數特例化則可以在源文件中實現,但是注意要include頭文件。

返回值是void類型

因為void的特殊性,所以如果當成泛型來處理會有很多問題,這裡把返回值是void類型的單獨實現一個函數即可。

總結

上面我們僅僅是實現了調用普通函數的工具,根據這個思路我們還可以實現調用靜態函數、獲取成員變量、賦值成員變量等,這樣當我們在進行jni開發的時候,如果需要對java對象或類進行操作,只需要一行代碼就可以了。

源碼

關注本公眾號(BennuCTech),發送「JNIObjectTools」獲取源碼。

實現一個通用的中英文排序工具

Android中使控制項保持固定寬高比的幾種方式

RecyclerView局部刷新機制——payload

相關焦點

  • JNI-Thread中start方法調用與run方法回調分析
    前言在java編程中,線程Thread是我們經常使用的類。那麼創建一個Thread的本質究竟是什麼,本文就此問題作一個探索。1.JNI機制的基本使用當我們new出一個Thread的時候,僅僅是創建了一個java層面的線程對象,而只有當Thread的start方法被調用的時候,一個線程才真正開始執行了。
  • Java築基 - JNI到底是個啥
    首先回顧一下jni的主要功能,從jdk1.1開始jni標準就成為了java平臺的一部分,它提供的一系列的API允許java和其他語言進行交互,實現了在java代碼中調用其他語言的函數。1、準備java代碼首先定義一個包含了native方法的類如下,之後我們要使用這個類中的native方法通過jni調用c++編寫成的動態連結庫中的方法:public class JniTest {    static{        System.loadLibrary
  • Java通過-jni調用c語言
    在Ubuntu14.04中通過Java調用c語言(1)首先編寫一個簡單的Java程序。該文件中定義了c的函數原型。在實現c函數的時候需要。(5)編寫c語言去實現這些方法,一個簡單的代碼如下:        #include <stdio.h>         #include "TestJNI.h"         int i=0;         JNIEXPORT void JNICALL Java_TestJNI_set (JNIEnv * env, jobject obj, jint j)
  • NDK/JNI 開發之 Java 類和 C 結構體互轉示例
    一、簡介JNI 開發中,常常會存在對應的 Java 類和 C 結構體需要互相轉換。通過本實例學習和了解這個過程。NativeLibrary首先新建一個類,來負責調用 native 方法。下面代碼中,比較重要的地方,一個是在 JNI_OnLoad 方法中,我們調用了 register_classes 方法去註冊類,這是因為我們要在 JNI 中使用 Java 的類、成員、方法,必須將他們先關聯起來。其次就是轉換的兩個方法的實現,也都放在了 Register.cpp 裡了。
  • 一行JAVA代碼如何運行起來?
    JVM運行Java程序有兩種方式,分別是jar包和Class類文件,jar包是偏上層的方式,把所有程序都打包成一個jar包,便於交付測試人員測試、運維人員發布,它的運行邏輯是通過java.exe找到java自帶的GetMainClassName函數,該函數獲取JNIENV實例,並調用JarFileJNIENV實例中的GetMainfest()函數獲取MainClass函數,Main函數再調用Java.c
  • java基礎之理解JNI原理
    JNI是JAVA標準平臺中的一個重要功能,它彌補了JAVA的與平臺無關這一重大優點的不足,在JAVA實現跨平臺的同時,也能與其它語言(如C、C++)的動態庫進行交互,給其它語言發揮優勢的機會。有了JAVA標準平臺的支持,使JNI模式更加易於實現和使用。
  • 使用Frida列印Java類函數調用關係
    通過對Android源碼進行閱讀,可以很快發現ART下類函數的四種調用:java調用java,java調用jni,jni調用java,jni調用jni;而java函數又有兩種運行模式:interpreter模式和quick模式。ART對不用的調用過程有著不同的處理邏輯。
  • JVM 解剖公園:JNI 臨界區與 GC Locker
    在臨界區內,原生代碼不允許調用其他 JNI 函數;也不允許調用任何其他阻塞當前線程的系統調用,等待其他 Java 線程完成(例如,另一個正在執行寫操作,當前線程對寫入的 stream 執行讀操作)。即使 VM 本身不支持 pinning,這些限制也能讓原生代碼更有機會得到數組指針而非數組拷貝。
  • Java代碼執行漏洞中類動態加載的應用
    Java中類的加載方式分為顯式和隱式,隱式加載是通過new等途徑生成的對象時Jvm把相應的類加載到內存中,顯示加載是通過 Class.forName(..) 等方式由程式設計師自己控制加載,而顯式類加載方式也可以理解為類動態加載,我們也可以自定義類加載器去加載任意的類。
  • 音視頻開發之旅(17) JNI與NDK的學習和使用
    JNI:Java Native Interface(java本地接口),使得Java與本地語言(C、CPP)相互調用NDK:Native Development Kit,是Android的一個工具開發包,幫助開發者快速開發C、CPP動態庫,自動將動態庫打包進入APK。通過JNI實現Java和Native的交互,在Android上通過NDK實現JNI的功能。
  • Android JNI中的異常處理 與Log日誌使用2步驟
    而在jni 中使用日誌就只需要2步驟。步驟一:引入頭文件 ,include log.h步驟二 定義宏。結果:JNI異常處理異常處理是java 程序設計中的重要功能,java 中 拋出一個異常,虛擬機停止執行代碼並且調用棧反向檢查能處理特定的異常類型處理程序代碼塊,叫做捕獲異常。
  • Java多線程並發工具類-信號量Semaphore對象講解
    Java多線程並發工具類-Semaphore對象講解通過前面的學習,我們已經知道了Java多線程並發場景中使用比較多的兩個工具類:做加法的CycliBarrier對象以及做減法的CountDownLatch對象並對這兩個對象進行了比較。我們發現這兩個對象要麼是做加法,要麼是做減法的。那麼有沒有既做加法也做減法的呢?
  • JNI解析以及在Android中的實際應用
    JNI是Java Native Interface的縮寫,它提供了若干的API實現了Java和其他語言的通信(在Android裡面主要是C&C++)。從Java1.1開始,JNI標準成為java平臺的一部分,它允許Java代碼和其他語言寫的代碼進行動態交互,JNI標準保證本地代碼能工作在任何Java 虛擬機環境,目前的很多熱修復補的開源項目。
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口 加載階段完成後,這些二進位字節流按照虛擬機所需的格式存儲在方法區之中。
  • Swing程序中如何調用JavaFX代碼
    (中國軟體網訊)當我們完成了MyScene類後,可以開始寫Java的主程序了,這是個標準的Swing程序中調用JavaFX代碼如下:package swingtest;/*** JavaFXToSwingTest.java http://www.javafxblogs.com* @
  • 淺談Java中字符串的初始化及字符串操作類
    "hello world"執行完第一行代碼後, 內存是這樣子的: 第二行代碼 s1.intern();String類的源碼中有對 intern因為我第一次見這幾行代碼時也卡殼了。首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。
  • JNI開發中,你需要知道的一些建議
    它們本質上都是指向函數表指針的指針(在C++版本中,它們被定義為類,該類包含一個指向函數表的指針,以及一系列可以通過這個函數表間接地訪問對應的JNI函數的成員函數)。JavaVM提供「調用接口(invocation interface)」函數, 允許使用者創建和銷毀一個JavaVM。理論上可以在一個進程中擁有多個JavaVM對象,但Android只允許存在一個。
  • 牛逼的程式設計師都這麼寫Java方法的...
    JNI一開始是為了本地已編譯語言,尤其是C和C++而設計 的,但是它並不妨礙你使用其他語言,只要調用約定受支持就可以了。使用java與本地已編譯的代碼交互,通常會喪失平臺可移植性。但是,有些情況下這樣做是可以接受的,甚至是必須的,比如,使用一些舊的庫,與硬體、作業系統進行交互,或者為了提高程序的性能。JNI標準至少保證本地代碼能工作在任何Java 虛擬機實現下。
  • Go 調用 Java 方案和性能優化分享
    = JNI_OK) { printf("AttachCurrentThread error\n"); exit(1); } printf("done\n"); return 0;}依賴的頭文件和動態連結庫可以在JDK目錄找到,比如在我的Mac上是/Library/Java/JavaVirtualMachines/jdk1.8.0_171
  • java開發工程師 javascript面向對象的初識
    冰凍三尺非一日之寒,希望大家在學習java的日子裡一定一定要堅持不懈,嚴格要求。多練,多問,多百度。祝大家早日成為一名優秀的軟體工程師! 那麼如果要向一個html頁面中寫入js代碼,就必須定義一個<script></script>標籤,所有的js代碼都在這個<script>標籤中。