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

weixin_34321977發表於2017-07-22

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

排版不佳建議點選檢視原文

java反射機制回顧


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

關於java反射機制的基本概念及API我就不重複了,百度講的比我好。簡單的來說,反射機制指的是程式在執行時能夠獲取自身的資訊。在java中,只要給定類的名字, 那麼就可以通過反射機制來獲得類的所有資訊。

為什麼要用反射機制?直接建立物件不就可以了嗎,這就涉及到了動態與靜態的概念:

靜態編譯:在編譯時確定型別,繫結物件,即通過。

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

這樣的話,這個軟體肯定是沒有多少人用的。採用靜態的話,需要把整個程式重新編譯一次才可以實現功能的更新,而採用反射機制的話,它就可以不用解除安裝,只需要在執行時才動態的建立和編譯,就可以實現該功能。

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

JNI開發流程


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

7014896-759df376077d60d6

我們在工程目錄下建立了jni資料夾,並建立了fork.c檔案、Application.mk檔案、Android.mk檔案,內容暫時不實現。

7014896-16b567e1b7f2aec5

新建MyJni類,我們宣告瞭兩個本地方法getJninumber、Calljni。注意本地方法使用native關鍵字,內容在C程式碼的對應函式中現。System.loadLibrary("fork");作用是載入本地.so連結庫(我們在jni中的C程式碼編譯後會生成.so原生程式碼,這是交叉編譯的概念:在一個平臺上去編譯另一個平臺上可以執行的原生程式碼。我們的工程最終呼叫的並非是C檔案,而是.so原生程式碼)。

7014896-1034abf4f762d3a1

在MainActivity中我們在Button的點選事件中呼叫了我們上面宣告的native方法並接收了相應的返回值在ToString方法中將int[]轉化為String,由於過程簡單這裡不再上圖。

Android.mk解析


7014896-c15bf577cc039782

接下來我們單獨聊聊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解析


7014896-90debd511dcb53c9

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標頭檔案來完成函式的宣告。

7014896-9b41c9586ca51976

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

7014896-57ac0fc8b0436e59

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

7014896-661baff494f8c219

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

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

7014896-55a40fab69a1c80e

我們開啟jni.h原始碼找到JNIEnv所定義的位置,#if defined(_cplusplus)意思是如果是C++檔案則JNIEnv是_JNIEnv的自定義型別,#else否則JNIEnv 是JniNativeInterface這個結構體的一級指標!由於我們是C程式碼檔案所以是後者。

回到我們的C程式碼中JNIenv* env,實際上就是JniNativeInterface這個結構體的二級指標。我們通過(*env)就可以方便的呼叫JniNativeInterface結構體中定義的函式指標。

7014896-1878fdc3635bbe1f

接下來我們追蹤第二個引數jclass,發現在原始碼中jclass其實是jobject的自定義型別,jobject又是void*的自定義型別。呼叫本地方法的java物件就是這個jobject,在這裡我們的本地方法的java物件是MyJni,所以jclass就是MyJni類的例項物件。

7014896-29b80922558e6836

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

7014896-b75f3da786f14e9a

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

我們通過(*env)可以直接呼叫結構體中的GetArrayLength函式,函式接收JNIEnv*和jintArray型別的引數返回陣列的長度,jsize實際上是int的自定義型別。

通過GetIntArrayElements函式獲取array陣列的指標,最後一個引數傳佈爾值標誌陣列是否被複制。這裡我們並不關心,所以傳NULL。

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

執行ndk-build即可開始編譯C程式,編譯完成後可在libs資料夾下看到編譯完成的.so動態連結庫。(前提是你已經配置了NDK環境變數)

7014896-2aa9349c32f14225
7014896-eb6aef9e288a079f

在點選事件中我們呼叫了getJninumber傳入int[]{1,2,3,4,5}陣列,並接收了返回值然後吐司。完成了一次java傳入資料給C處理後返回java的操作。

C函式反射呼叫java方法


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

7014896-ac2cf324a198f759

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

7014896-e8a273a2858c193b

這是我們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會崩潰


7014896-7874749b2d8c7a81

細心的小夥伴發現我為啥把Toast註釋了改用控制檯輸出?

7014896-bad463bf05b2821e

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

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

方法簽名


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

7014896-e246ac84c1134a1e

在命令列進入專案的bin\classes目錄執行javap,會看到有幫助提示,我們輸入javap -s 方法所在的全類名 即可看到方法簽名。複製到程式碼中即可。

7014896-ec88b6717de654ae

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

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

7014896-bc8e4c1639ed0349

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

謝謝閱讀!

相關文章