C谜慌、C++然想、Java?Java Native Interface(JNI)特輯——C反射java函數(shù)

C欣范、C++变泄、Java?Java Native Interface(JNI)特輯——C反射java函數(shù)

排版不佳建議點(diǎn)擊查看原文

java反射機(jī)制回顧


在上篇特輯中我們回顧了C語言的基本內(nèi)容恼琼,這次我們正式聊聊JNI妨蛹。在此之前我覺得有必要回顧一下java的反射機(jī)制。

關(guān)于java反射機(jī)制的基本概念及API我就不重復(fù)了驳癌,百度講的比我好滑燃。簡單的來說,反射機(jī)制指的是程序在運(yùn)行時(shí)能夠獲取自身的信息颓鲜。在java中表窘,只要給定類的名字典予,?那么就可以通過反射機(jī)制來獲得類的所有信息。

為什么要用反射機(jī)制乐严?直接創(chuàng)建對象不就可以了嗎瘤袖,這就涉及到了動(dòng)態(tài)與靜態(tài)的概念:

靜態(tài)編譯:在編譯時(shí)確定類型,綁定對象,即通過昂验。

動(dòng)態(tài)編譯:運(yùn)行時(shí)確定類型捂敌,綁定對象。動(dòng)態(tài)編譯最大限度發(fā)揮了java的靈活性既琴,體現(xiàn)了多態(tài)的應(yīng)用占婉,有以降低類之間的藕合性。一句話甫恩,反射機(jī)制的優(yōu)點(diǎn)就是可以實(shí)現(xiàn)動(dòng)態(tài)創(chuàng)建對象和編譯逆济,體現(xiàn)出很大的靈活性,特別是在J2EE的開發(fā)中它的靈活性就表現(xiàn)的十分明顯磺箕。比如奖慌,一個(gè)大型的軟件,不可能一次就把把它設(shè)計(jì)的很完美松靡,當(dāng)這個(gè)程序編譯后简僧,發(fā)布了,當(dāng)發(fā)現(xiàn)需要更新某些功能時(shí)雕欺,我們不可能要用戶把以前的卸載岛马,再重新安裝新的版本,假如

這樣的話阅茶,這個(gè)軟件肯定是沒有多少人用的蛛枚。采用靜態(tài)的話,需要把整個(gè)程序重新編譯一次才可以實(shí)現(xiàn)功能的更新脸哀,而采用反射機(jī)制的話蹦浦,它就可以不用卸載,只需要在運(yùn)行時(shí)才動(dòng)態(tài)的創(chuàng)建和編譯撞蜂,就可以實(shí)現(xiàn)該功能盲镶。

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

JNI開發(fā)流程


我們開發(fā)的宗旨是不依賴任何開發(fā)工具宇色,所以我們eclipse創(chuàng)建安卓工程,使用命令行編譯C代碼,雖然不太方便但這是一種通用的方式宣蠕,不依賴開發(fā)工具例隆,不管是androidStudio、還是eclipse都可以使用抢蚀。

我們在工程目錄下創(chuàng)建了jni文件夾镀层,并創(chuàng)建了fork.c文件、Application.mk文件皿曲、Android.mk文件唱逢,內(nèi)容暫時(shí)不實(shí)現(xiàn)。

新建MyJni類屋休,我們聲明了兩個(gè)本地方法getJninumber坞古、Calljni。注意本地方法使用native關(guān)鍵字博投,內(nèi)容在C代碼的對應(yīng)函數(shù)中現(xiàn)绸贡。System.loadLibrary("fork");作用是加載本地.so鏈接庫(我們在jni中的C代碼編譯后會生成.so本地代碼盯蝴,這是交叉編譯的概念:在一個(gè)平臺上去編譯另一個(gè)平臺上可以執(zhí)行的本地代碼毅哗。我們的工程最終調(diào)用的并非是C文件,而是.so本地代碼)捧挺。

在MainActivity中我們在Button的點(diǎn)擊事件中調(diào)用了我們上面聲明的native方法并接收了相應(yīng)的返回值在ToString方法中將int[]轉(zhuǎn)化為String虑绵,由于過程簡單這里不再上圖。

Android.mk解析


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

LOCAL_PATH := $(call my-dir)

每個(gè)Android.mk文件必須以定義LOCAL_PATH為開始捕发。它用于在開發(fā)tree中查找源文件。

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

include $(CLEAR_VARS)

CLEAR_VARS 變量由Build System提供。并指向一個(gè)指定的GNU Makefile遏匆,由它負(fù)責(zé)清理很多LOCAL_xxx.

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

這個(gè)清理動(dòng)作是必須的,因?yàn)樗械木幾g控制文件由同一個(gè)GNU Make解析和執(zhí)行幅聘,其變量是全局的凡纳。所以清理后才能避免相互影響。

LOCAL_MODULE ? ?:= fork

LOCAL_MODULE模塊必須定義帝蒿,以表示Android.mk中的每一個(gè)模塊荐糜。名字必須唯一且不包含空格。

Build System會自動(dòng)添加適當(dāng)?shù)那熬Y和后綴。例如暴氏,fork丛版,要產(chǎn)生動(dòng)態(tài)庫,則生成libfork.so. 但請注意:如果模塊名被定為:libfork.則生成libfork.so. 不再加前綴偏序。簡單來說就是指定了生成的動(dòng)態(tài)鏈接庫的名字页畦。

LOCAL_SRC_FILES := fork.c

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

不必列出頭文件研儒,build System 會自動(dòng)幫我們找出依賴文件豫缨。

缺省的C++源碼的擴(kuò)展名為.cpp. 也可以修改,通過LOCAL_CPP_EXTENSION端朵。

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

include $(BUILD_SHARED_LIBRARY)

BUILD_SHARED_LIBRARY:是Build System提供的一個(gè)變量,指向一個(gè)GNU Makefile Script冲呢。它負(fù)責(zé)收集自從上次調(diào)用include $(CLEAR_VARS)后的所有LOCAL_XXX信息舍败。并決定編譯為什么。

BUILD_STATIC_LIBRARY:編譯為靜態(tài)庫敬拓。

BUILD_SHARED_LIBRARY:編譯為動(dòng)態(tài)庫邻薯。

BUILD_EXECUTABLE:編譯為Native C可執(zhí)行程序。

Application.mk解析


Application.mk是用來描述你的應(yīng)用程序需要哪些模塊乘凸,以及這些模塊所要具有的一些特性厕诡。

Application.mk文件一般是放在$PROJECT/jni/目錄下的($PROJECT代表你所寫程序的項(xiàng)目目錄),這樣ndk-build命令可以自動(dòng)搜索到它营勤。當(dāng)然灵嫌,Application.mk文件其實(shí)是可選的。默認(rèn)情況下葛作,如果ndk-build命令找不到Application.mk文件的話寿羞,就會使用如下規(guī)則進(jìn)行編譯:

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

2)對于所有模塊赂蠢,NDK編譯系統(tǒng)會根據(jù)“armeabi” ABI來生成機(jī)器代碼绪穆,即指令集是ARMv5TE。

具體來說客年,Application.mk文件中霞幅,可以包含對下面幾個(gè)變量的定義:

APP_PROJECT_PATH

APP_MODULES

APP_OPTIM

APP_CFLAGS

APP_CPPFLAGS

APP_CXXFLAGS

APP_BUILD_SCRIPT

APP_ABI

這里我們只用到了APP_ABI默認(rèn)情況下,NDK編譯系統(tǒng)會根據(jù)“armeabi” ABI來生成機(jī)器代碼量瓜,即一個(gè)使用ARMv5TE指令集并且支持軟件浮點(diǎn)操作的CPU司恳。

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

編寫C代碼


我們在MyJni類中聲明了兩個(gè)本地方法耍共,所以我們的C代碼需要新建兩個(gè)函數(shù)對應(yīng)java的本地方法。本地函數(shù)命名規(guī)則: 返回值 Java_包名_類名_本地方法名猎塞。按照此命名規(guī)則我們當(dāng)然是可以創(chuàng)建對應(yīng)的函數(shù)的试读,可是如果java本地方法數(shù)量過多,這時(shí)候就需要生成.h頭文件來完成函數(shù)的聲明荠耽。

首先我們進(jìn)入項(xiàng)目工程的src目錄下钩骇,輸入javah命令系統(tǒng)有相應(yīng)的提示,我們選擇-jni生成JNI樣式的標(biāo)頭文件铝量,最后接上java本地方法所在類的全類名即可在項(xiàng)目src目錄下生成頭文件xxx.h债蜜。

在頭文件中我們發(fā)現(xiàn)籽前,java本地方法對應(yīng)的函數(shù)名已經(jīng)幫我們生成好了,我們只需要拷貝到C文件中作為我們的函數(shù)即可森逮。

我們來到C代碼,這是java本地方法getJninumber所對應(yīng)的函數(shù)恼蓬。我們發(fā)現(xiàn)有三個(gè)值:JNIEnv*诅需、jclass渔呵、jintArray,這都是啥呢责掏?別急我們一個(gè)一個(gè)聊。

首先注意到我們引入了這個(gè)函數(shù)包轩拨,jni.h其實(shí)是我們android開發(fā)中NDK開發(fā)包提供的(詳細(xì)目錄android-ndk-r9d\platforms\android-19\arch-arm\usr\include\jni.h)践瓷。

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

回到我們的C代碼中JNIenv* env寸宵,實(shí)際上就是JniNativeInterface這個(gè)結(jié)構(gòu)體的二級指針。我們通過(*env)就可以方便的調(diào)用JniNativeInterface結(jié)構(gòu)體中定義的函數(shù)指針元咙。

接下來我們追蹤第二個(gè)參數(shù)jclass,發(fā)現(xiàn)在源碼中jclass其實(shí)是jobject的自定義類型梯影,jobject又是void*的自定義類型。調(diào)用本地方法的java對象就是這個(gè)jobject庶香,在這里我們的本地方法的java對象是MyJni甲棍,所以jclass就是MyJni類的實(shí)例對象。

最后一個(gè)參數(shù)jintArray實(shí)際上和上一個(gè)類似赶掖,它是jarray的自定義類型感猛,最終也是void*。在這里jintArray對應(yīng)我們java本地方法getJninumber傳入的int[]數(shù)組奢赂。

明白了函數(shù)的三個(gè)參數(shù)后我們要開始實(shí)現(xiàn)我們的函數(shù)邏輯陪白,我們需要將java中傳入的int[]數(shù)組處理更改后給它作為jintArray返回。

我們通過(*env)可以直接調(diào)用結(jié)構(gòu)體中的GetArrayLength函數(shù)膳灶,函數(shù)接收J(rèn)NIEnv*和jintArray類型的參數(shù)返回?cái)?shù)組的長度,jsize實(shí)際上是int的自定義類型咱士。

通過GetIntArrayElements函數(shù)獲取array數(shù)組的指針立由,最后一個(gè)參數(shù)傳布爾值標(biāo)志數(shù)組是否被復(fù)制。這里我們并不關(guān)心序厉,所以傳NULL锐膜。

數(shù)組長度有了,指針有了弛房,我們遍歷數(shù)組道盏,通過指針位運(yùn)算更改了數(shù)組中每一個(gè)元素的值(+10),最終return回去文捶。

運(yùn)行ndk-build即可開始編譯C程序捞奕,編譯完成后可在libs文件夾下看到編譯完成的.so動(dòng)態(tài)鏈接庫。(前提是你已經(jīng)配置了NDK環(huán)境變量)

在點(diǎn)擊事件中我們調(diào)用了getJninumber傳入int[]{1,2,3,4,5}數(shù)組拄轻,并接收了返回值然后吐司颅围。完成了一次java傳入數(shù)據(jù)給C處理后返回java的操作。

C函數(shù)反射調(diào)用java方法


接下來我們聊聊下一個(gè)函數(shù)Calljni的實(shí)現(xiàn),看看他是如何實(shí)現(xiàn)回掉java方法的恨搓。

JniCallMe便是C函數(shù)Calljni需要回掉的java方法院促,它在MainActivity中定義。

這是我們Calljni函數(shù)的實(shí)現(xiàn)斧抱,一起來看看:

找到字節(jié)碼對象常拓,在java中萬物皆對象Class也是對象,我們需要反射的方法在MainActivity中辉浦,所以我們需要獲取MainActivit的Class對象弄抬。當(dāng)然JniNativeInterface這個(gè)結(jié)構(gòu)體幫我們定義好了對應(yīng)的函數(shù),我們只需要調(diào)用FindClass函數(shù)宪郊,最后一個(gè)參數(shù)是我們的Class的全路徑用斜杠隔開即可掂恕。

找到方法所在類的對象,我們的方法定義在MainActiviy中弛槐,所以我們需要獲取MainActiviy對象懊亡,注意本函數(shù)的第二個(gè)參數(shù)jclass obj并不能直接使用,因?yàn)樗莕ative方法所在類的對象即是MyJni類的對象乎串,并不是我們要的店枣!通過AllocObject函數(shù),最后一個(gè)參數(shù)把第一步獲取MainActivit的Class對象傳入即可叹誉。

獲取方法對象鸯两,通過GetMethodID函數(shù),最后兩個(gè)參數(shù)傳入方法的名稱长豁、方法簽名(由于java的方法允許重載钧唐,GetMethodID函數(shù)需要通過方法簽名才能區(qū)分,怎么查看方法簽名蕉斜?我們晚點(diǎn)聊)逾柿。

最后一步缀棍,反射java方法,CallVoidMethod函數(shù)可以幫我們做到机错,它是針對無返回值的java方法反射爬范,傳入env、MainActivity對象弱匪、方法對象青瀑、還有java方法的形參...這里我們傳入int值6。

至此萧诫,函數(shù)就編寫完成斥难,我們在Button點(diǎn)擊事件中調(diào)用Calljni本地方法,C函數(shù)便會反射JniCallMe方法并傳入形參6完成控制臺打印帘饶。

為什么Toast會崩潰


細(xì)心的小伙伴發(fā)現(xiàn)我為啥把Toast注釋了改用控制臺輸出哑诊?

因?yàn)闀?bào)錯(cuò)!<翱獭镀裤!通過log我們發(fā)現(xiàn)是空指針異常,Context對象為Null缴饭∈钊埃可是我們明明通過MainActivity.this傳入Cantext。

原因是這樣颗搂,由于我們在C函數(shù)中第2步通過AllocObject函數(shù)獲取的MainActiviy對象其實(shí)是new出來的担猛。Android程序與Java程序不一樣,并不是隨隨便便寫一個(gè)類丢氢,在main()方法里面就能運(yùn)行傅联。Android是基于組件化設(shè)計(jì)的,組件的運(yùn)行需要一套完整的Android的環(huán)境的卖丸,在這個(gè)環(huán)境下Activity,Service才能運(yùn)行纺且,而這些組件不能以new的方式創(chuàng)建實(shí)例,它需要相應(yīng)的上下文環(huán)境稍浆,也就是我們Context〔轮觯可以說Context是這些Android組件運(yùn)行的一個(gè)核心類衅枫。所以我們并不能獲取到Context對象,從而導(dǎo)致了空指針異常朗伶。

方法簽名


在上面的GetMethodID函數(shù)中的最后一個(gè)參數(shù)需要傳入方法簽名弦撩,那么方法簽名應(yīng)該如何獲取论皆?

在命令行進(jìn)入項(xiàng)目的bin\classes目錄運(yùn)行javap,會看到有幫助提示益楼,我們輸入javap -s 方法所在的全類名 即可看到方法簽名猾漫。復(fù)制到代碼中即可。

至此本篇所聊的內(nèi)容都結(jié)束了感凤,下篇我們來聊聊關(guān)于使用JNI調(diào)用cfork子進(jìn)程的話題悯周。

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

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

謝謝閱讀!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末族跛,一起剝皮案震驚了整個(gè)濱河市闰挡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌礁哄,老刑警劉巖长酗,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桐绒,居然都是意外死亡夺脾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門掏膏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劳翰,“玉大人,你說我怎么就攤上這事馒疹〖阳ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵颖变,是天一觀的道長生均。 經(jīng)常有香客問我,道長腥刹,這世上最難降的妖魔是什么马胧? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮衔峰,結(jié)果婚禮上佩脊,老公的妹妹穿的比我還像新娘。我一直安慰自己垫卤,他們只是感情好威彰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著穴肘,像睡著了一般歇盼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上评抚,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天豹缀,我揣著相機(jī)與錄音伯复,去河邊找鬼。 笑死邢笙,一個(gè)胖子當(dāng)著我的面吹牛啸如,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸣剪,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼组底,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了筐骇?” 一聲冷哼從身側(cè)響起债鸡,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铛纬,沒想到半個(gè)月后厌均,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡告唆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年棺弊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片擒悬。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡模她,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出懂牧,到底是詐尸還是另有隱情侈净,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布僧凤,位于F島的核電站畜侦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏躯保。R本人自食惡果不足惜旋膳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望途事。 院中可真熱鬧验懊,春花似錦、人聲如沸尸变。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽振惰。三九已至,卻和暖如春垄懂,著一層夾襖步出監(jiān)牢的瞬間骑晶,已是汗流浹背痛垛。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桶蛔,地道東北人匙头。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像仔雷,于是被迫代替她去往敵國和親蹂析。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容