C、C++、Java?Java Native Interface(JNI)特輯——C反射java函數

C、C++、Java?Java Native Interface(JNI)特輯——C反射java函數

排版不佳建議點擊查看原文

java反射機制回顧


在上篇特輯中我們回顧了C語言的基本內容,這次我們正式聊聊JNI。在此之前我覺得有必要回顧一下java的反射機制。

關于java反射機制的基本概念及API我就不重復了,百度講的比我好。簡單的來說,反射機制指的是程序在運行時能夠獲取自身的信息。在java中,只要給定類的名字,?那么就可以通過反射機制來獲得類的所有信息。

為什么要用反射機制?直接創建對象不就可以了嗎,這就涉及到了動態與靜態的概念:

靜態編譯:在編譯時確定類型,綁定對象,即通過。

動態編譯:運行時確定類型,綁定對象。動態編譯最大限度發揮了java的靈活性,體現了多態的應用,有以降低類之間的藕合性。一句話,反射機制的優點就是可以實現動態創建對象和編譯,體現出很大的靈活性,特別是在J2EE的開發中它的靈活性就表現的十分明顯。比如,一個大型的軟件,不可能一次就把把它設計的很完美,當這個程序編譯后,發布了,當發現需要更新某些功能時,我們不可能要用戶把以前的卸載,再重新安裝新的版本,假如

這樣的話,這個軟件肯定是沒有多少人用的。采用靜態的話,需要把整個程序重新編譯一次才可以實現功能的更新,而采用反射機制的話,它就可以不用卸載,只需要在運行時才動態的創建和編譯,就可以實現該功能。

它的缺點是對性能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什么并且它滿足我們的要求。這類操作總是慢于只直接執行相同的操作。

JNI開發流程


我們開發的宗旨是不依賴任何開發工具,所以我們eclipse創建安卓工程,使用命令行編譯C代碼,雖然不太方便但這是一種通用的方式,不依賴開發工具,不管是androidStudio、還是eclipse都可以使用。

我們在工程目錄下創建了jni文件夾,并創建了fork.c文件、Application.mk文件、Android.mk文件,內容暫時不實現。

新建MyJni類,我們聲明了兩個本地方法getJninumber、Calljni。注意本地方法使用native關鍵字,內容在C代碼的對應函數中現。System.loadLibrary("fork");作用是加載本地.so鏈接庫(我們在jni中的C代碼編譯后會生成.so本地代碼,這是交叉編譯的概念:在一個平臺上去編譯另一個平臺上可以執行的本地代碼。我們的工程最終調用的并非是C文件,而是.so本地代碼)。

在MainActivity中我們在Button的點擊事件中調用了我們上面聲明的native方法并接收了相應的返回值在ToString方法中將int[]轉化為String,由于過程簡單這里不再上圖。

Android.mk解析


接下來我們單獨聊聊jni目錄下的Android.mk文件。Android.mk如果是底層開發的工程師一定再熟悉不過了,基本概念依然是留你自己百度去吧。通俗簡單的說就是告訴編譯器.c的源文件在什么地方,要生成的編譯對象的名字是什么。

LOCAL_PATH := $(call my-dir)

每個Android.mk文件必須以定義LOCAL_PATH為開始。它用于在開發tree中查找源文件。

宏my-dir?則由Build System提供。返回包含Android.mk的目錄路徑。

include $(CLEAR_VARS)

CLEAR_VARS 變量由Build System提供。并指向一個指定的GNU Makefile,由它負責清理很多LOCAL_xxx.

例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等。但不清理LOCAL_PATH.

這個清理動作是必須的,因為所有的編譯控制文件由同一個GNU Make解析和執行,其變量是全局的。所以清理后才能避免相互影響。

LOCAL_MODULE ? ?:= fork

LOCAL_MODULE模塊必須定義,以表示Android.mk中的每一個模塊。名字必須唯一且不包含空格。

Build System會自動添加適當的前綴和后綴。例如,fork,要產生動態庫,則生成libfork.so. 但請注意:如果模塊名被定為:libfork.則生成libfork.so. 不再加前綴。簡單來說就是指定了生成的動態鏈接庫的名字。

LOCAL_SRC_FILES := fork.c

LOCAL_SRC_FILES變量必須包含將要打包如模塊的C/C++ 源碼。

不必列出頭文件,build System 會自動幫我們找出依賴文件。

缺省的C++源碼的擴展名為.cpp. 也可以修改,通過LOCAL_CPP_EXTENSION。

簡單來說就是指定了C的源文件叫什么名字。

include $(BUILD_SHARED_LIBRARY)

BUILD_SHARED_LIBRARY:是Build System提供的一個變量,指向一個GNU Makefile Script。它負責收集自從上次調用include $(CLEAR_VARS)后的所有LOCAL_XXX信息。并決定編譯為什么。

BUILD_STATIC_LIBRARY:編譯為靜態庫。

BUILD_SHARED_LIBRARY:編譯為動態庫。

BUILD_EXECUTABLE:編譯為Native C可執行程序。

Application.mk解析


Application.mk是用來描述你的應用程序需要哪些模塊,以及這些模塊所要具有的一些特性。

Application.mk文件一般是放在$PROJECT/jni/目錄下的($PROJECT代表你所寫程序的項目目錄),這樣ndk-build命令可以自動搜索到它。當然,Application.mk文件其實是可選的。默認情況下,如果ndk-build命令找不到Application.mk文件的話,就會使用如下規則進行編譯:

1)會編譯全部在Android.mk文件中列出的模塊;

2)對于所有模塊,NDK編譯系統會根據“armeabi” ABI來生成機器代碼,即指令集是ARMv5TE。

具體來說,Application.mk文件中,可以包含對下面幾個變量的定義:

APP_PROJECT_PATH

APP_MODULES

APP_OPTIM

APP_CFLAGS

APP_CPPFLAGS

APP_CXXFLAGS

APP_BUILD_SCRIPT

APP_ABI

這里我們只用到了APP_ABI默認情況下,NDK編譯系統會根據“armeabi” ABI來生成機器代碼,即一個使用ARMv5TE指令集并且支持軟件浮點操作的CPU。

你可以通過定義APP_ABI變量來選擇一個不同的ABI。這里我們使用了all,表示我們選擇編譯全平臺的機器代碼,也可以有針對的寫x86則會只編譯x86平臺處理器的代碼。

編寫C代碼


我們在MyJni類中聲明了兩個本地方法,所以我們的C代碼需要新建兩個函數對應java的本地方法。本地函數命名規則: 返回值 Java_包名_類名_本地方法名。按照此命名規則我們當然是可以創建對應的函數的,可是如果java本地方法數量過多,這時候就需要生成.h頭文件來完成函數的聲明。

首先我們進入項目工程的src目錄下,輸入javah命令系統有相應的提示,我們選擇-jni生成JNI樣式的標頭文件,最后接上java本地方法所在類的全類名即可在項目src目錄下生成頭文件xxx.h。

在頭文件中我們發現,java本地方法對應的函數名已經幫我們生成好了,我們只需要拷貝到C文件中作為我們的函數即可。

我們來到C代碼,這是java本地方法getJninumber所對應的函數。我們發現有三個值:JNIEnv*、jclass、jintArray,這都是啥呢?別急我們一個一個聊。

首先注意到我們引入了這個函數包,jni.h其實是我們android開發中NDK開發包提供的(詳細目錄android-ndk-r9d\platforms\android-19\arch-arm\usr\include\jni.h)。

我們打開jni.h源碼找到JNIEnv所定義的位置,#if defined(_cplusplus)意思是如果是C++文件則JNIEnv是_JNIEnv的自定義類型,#else否則JNIEnv?是JniNativeInterface這個結構體的一級指針!由于我們是C代碼文件所以是后者。

回到我們的C代碼中JNIenv* env,實際上就是JniNativeInterface這個結構體的二級指針。我們通過(*env)就可以方便的調用JniNativeInterface結構體中定義的函數指針。

接下來我們追蹤第二個參數jclass,發現在源碼中jclass其實是jobject的自定義類型,jobject又是void*的自定義類型。調用本地方法的java對象就是這個jobject,在這里我們的本地方法的java對象是MyJni,所以jclass就是MyJni類的實例對象。

最后一個參數jintArray實際上和上一個類似,它是jarray的自定義類型,最終也是void*。在這里jintArray對應我們java本地方法getJninumber傳入的int[]數組。

明白了函數的三個參數后我們要開始實現我們的函數邏輯,我們需要將java中傳入的int[]數組處理更改后給它作為jintArray返回。

我們通過(*env)可以直接調用結構體中的GetArrayLength函數,函數接收JNIEnv*和jintArray類型的參數返回數組的長度,jsize實際上是int的自定義類型。

通過GetIntArrayElements函數獲取array數組的指針,最后一個參數傳布爾值標志數組是否被復制。這里我們并不關心,所以傳NULL。

數組長度有了,指針有了,我們遍歷數組,通過指針位運算更改了數組中每一個元素的值(+10),最終return回去。

運行ndk-build即可開始編譯C程序,編譯完成后可在libs文件夾下看到編譯完成的.so動態鏈接庫。(前提是你已經配置了NDK環境變量)

在點擊事件中我們調用了getJninumber傳入int[]{1,2,3,4,5}數組,并接收了返回值然后吐司。完成了一次java傳入數據給C處理后返回java的操作。

C函數反射調用java方法


接下來我們聊聊下一個函數Calljni的實現,看看他是如何實現回掉java方法的。

JniCallMe便是C函數Calljni需要回掉的java方法,它在MainActivity中定義。

這是我們Calljni函數的實現,一起來看看:

找到字節碼對象,在java中萬物皆對象Class也是對象,我們需要反射的方法在MainActivity中,所以我們需要獲取MainActivit的Class對象。當然JniNativeInterface這個結構體幫我們定義好了對應的函數,我們只需要調用FindClass函數,最后一個參數是我們的Class的全路徑用斜杠隔開即可。

找到方法所在類的對象,我們的方法定義在MainActiviy中,所以我們需要獲取MainActiviy對象,注意本函數的第二個參數jclass obj并不能直接使用,因為它是native方法所在類的對象即是MyJni類的對象,并不是我們要的!通過AllocObject函數,最后一個參數把第一步獲取MainActivit的Class對象傳入即可。

獲取方法對象,通過GetMethodID函數,最后兩個參數傳入方法的名稱、方法簽名(由于java的方法允許重載,GetMethodID函數需要通過方法簽名才能區分,怎么查看方法簽名?我們晚點聊)。

最后一步,反射java方法,CallVoidMethod函數可以幫我們做到,它是針對無返回值的java方法反射,傳入env、MainActivity對象、方法對象、還有java方法的形參...這里我們傳入int值6。

至此,函數就編寫完成,我們在Button點擊事件中調用Calljni本地方法,C函數便會反射JniCallMe方法并傳入形參6完成控制臺打印。

為什么Toast會崩潰


細心的小伙伴發現我為啥把Toast注釋了改用控制臺輸出?

因為會報錯!!!通過log我們發現是空指針異常,Context對象為Null。可是我們明明通過MainActivity.this傳入Cantext。

原因是這樣,由于我們在C函數中第2步通過AllocObject函數獲取的MainActiviy對象其實是new出來的。Android程序與Java程序不一樣,并不是隨隨便便寫一個類,在main()方法里面就能運行。Android是基于組件化設計的,組件的運行需要一套完整的Android的環境的,在這個環境下Activity,Service才能運行,而這些組件不能以new的方式創建實例,它需要相應的上下文環境,也就是我們Context。可以說Context是這些Android組件運行的一個核心類。所以我們并不能獲取到Context對象,從而導致了空指針異常。

方法簽名


在上面的GetMethodID函數中的最后一個參數需要傳入方法簽名,那么方法簽名應該如何獲取?

在命令行進入項目的bin\classes目錄運行javap,會看到有幫助提示,我們輸入javap -s 方法所在的全類名 即可看到方法簽名。復制到代碼中即可。

至此本篇所聊的內容都結束了,下篇我們來聊聊關于使用JNI調用cfork子進程的話題。

歡迎長按下圖-識別圖中二維碼或者掃一掃,搜索微信公眾號:黃君華。關注我的公眾號:

如果你有不同意見或建議或者有好的技術文章想和大家分享歡迎投稿,可以把你的文章使用附件的形式發送到我的郵箱2908116133@qq.com

謝謝閱讀!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容