=========================項目心得和遇上的問題總結=========================
要實現這些功能,有很多種:多線程可以放在Jni層,這樣接收和消息的整理邏輯都在Jni層,這樣程序就會變得複雜一些,因爲你不僅要Java調用C,還要C調用Java。我們也可以把這些邏輯層放在App層處理,Jni層只負責打開串口文件,並fd組織成FildDescriptor返回給App。其實如果按照程序的封裝設計,Jni不應該有過多的邏輯處理,邏輯處理都應該交給App,用Java來寫,這樣的優點在於,相對於C來說,Java比較好寫,不用考慮指針和垃圾回收,內存溢出,線程安全等問題也會少很多。最重要的是,把邏輯處理放在App容易提高整個程序的維護性,和Jni層的複用性,只要把Jni打包成庫給別人使用即可。但是我們考慮到,C的效率更高,最主要C對字符處理和位的處理更容易。然後就很任性地選擇了用C來實現一些雜亂的處理,當然,也是想挑戰一下Jni下的編程。這裏面也確實遇上了很多問題,這裏就做一些簡單總結吧。
1
2
3
4
5
6
7
|
#ifdef
__cplusplus extern "C" { #endif //sourc... #ifdef
__cplusplus } #endif |
在這個功能App的設計中,對串口信息的接收發往上層通知的邏輯是連接整個框架的邏輯,所以,在哪和怎麼樣接收接收和通知會成爲這整個App的關鍵。然而串口收到的命令時間是不確定性,和收到的多少也是無法確定的,再加上串口接收命令的簡單,這給這些線程帶來了不少問題。
一開始,我們的線程設計爲如下
這樣的設計有一個好處就是,可以保證發送和接收的同步進行和想匹配,意思就是說,我發送出去的消息會等待接收到的消息,這樣就能保證我發什麼就會接收到相應的回覆。這時就可以根據回覆做出相對應的動作和錯誤處理。但是這樣就有一個問題就是這兩個線程的設計,有太多的假設,我們假設SendMsg後會先跑到Wait Ack等接收線程,但有可能時在從SendMsg跑到Wait Ack的過程中,就把時間片讓給了子線程,有可能這時候子線程已經收到消息並Ack了,這時主線程就會錯失這個消息。還有可能主線程的Lock跑到Handle中,子線程已經從新開始,執行clear Buffer了。
我們可以通過加多幾個信號量來解決這些同步問題。但是這邏輯之間的相互作用就會變得非常多,也會非常雜亂,所以我們就沒有往下走,開始想新的方案
在新方案中,我們把線程之間的功能都獨立開來,儘量讓各個線程之間的關聯和Wait更少一點,這樣,邏輯就很清晰,各種邏輯問題就會少很多,每個線程我只管把數據丟出去,而不管丟出去後後會如何,在這裏我們設計了一個函數包含一個interface讓線程調用,interface該做什麼動作就讓看具體實現了,或許interfack還會把消息丟到別一個線程呢。在這裏這樣的設計越是到後面,要處理的回覆越多,信息回覆越得雜的時候越能體現優越性。還有個優點是上層不用阻塞,而是很舒服地被調用,這裏就省下很多什麼監聽啊,阻塞等帶來的煩惱。而這種方法有一個很大的缺點就是,我們發送了消息後我們無法立即根據我們發送的消息做發判斷處理, 意思是我無法if( SendMsg() < 0 ){ ..... }。源碼如下,線程收到消息並整理好後就會調用notifyAck了
1
2
3
4
5
6
7
8
9
10
|
private void notifyACK( int ack,
String arg){ Log.i(TAG, "notifyACK
" +
arg); if (mAckCallBack
!= null ){ mAckCallBack.onACK(ack,
arg); } } public interface AckCallBack
{ public void onACK( int ack,
String arg); } |
這種方案中有一個問題要非常注意,在上層App中,UI線程和處理線程是分開的,即UI的更新最好不要在處理線程中,邏輯最好不要在UI線程中,如果不這樣,有可能會出現一些很奇怪的問題,而不報錯。我們就遇上了一個問題:在Jni中 callvoidmethod 不執行,callvoidmethod調用了notifyAck,但上面的Log怎麼調都不打印,而編譯器就不報錯。出現這個問題的原因是callvoidmethod 在jni的線程中調用,而notifyAck裏面又實現了UI的更新。所以導致了這個問題發生,而沒有報錯。這樣的問題很頭疼。
項目上還有一個問題是比較糾結是,開出來的多線程一定要回收。如果你不回收當你的程序退出後馬上再開,接收到的消息會在任意一時候亂入,還有可能打不開,或者直接造成死機。
下面的源碼是如果通知一個阻塞的線程退出,原理是用一個管道,然後在Poll數據的地方,同時poll這個管道,如果要退出則在這個管道時寫入數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
//init
pipe pipe(gThread_para.pipeFd) //通知線程退出 void kill_recv_thread( void ) { //notify
son thread to end if (
write(tp->pipeFd[1], "0" ,
1) != 1) { LOGE( "notify
son thread to end failed \n" ); } } void *recv_thread( void *args){ struct pollfd
pfd[2]; int32
timeout = -1; while (
1 ) { pfd[0].fd
= gThread_para.pipeFd[0]; pfd[0].events
= POLLIN; //Poll數據到來的同時Poll退出通知 int res
= poll(pfd, 2, timeout); if (POLLIN
== pfd[0].revents){ //end
thread return -2; } } } |
=========================技術實現難點總結=========================
好了,不多說了。接下來結合源碼,看一下一些技術上的問題:
一. Java調用C
這個是通用的寫法,網上有很多資料,也可以參考一下之前的文章《Android Jni 基礎筆記》。
二. C調用Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
jint
initCallBack(JNIEnv *env, jobject thiz){ pthread_t
mReceivePt; int res; //通過thiz這個對象找到這個類。 gNotifyCallBack.serialPollClass
= env->GetObjectClass(thiz); //再找到要調用的函數 gNotifyCallBack.ackCBMethod
= env->GetMethodID(gNotifyCallBack.serialPollClass, "notifyACK" , "(ILjava/lang/String;)V" ); if ( /*gNotifyCallBack.callComingCBMethod
== NULL || */ gNotifyCallBack.ackCBMethod
== NULL){ LOGE( "no
have method" ); return -GET_CB_METHOD_ERR; } /*重點:我們要調用notifyAck這個Java函數就必須要通過實例對象來調用,在這個函數裏可以通過thiz來調用, *但怎麼在任意地方調用這個實例對象的Java函數呢?有同學會想把thiz保存爲一個全局變量即可。 *但是這個thiz只會做爲一個臨時變量,這個函數過後就會被回收。所以我們這個方法是不可行的 *可行的方法就是用evn提供的接口NewGlobalRef來保存一個全局變量*/ gBTHandlerObject
= env->NewGlobalRef(thiz); //調用Java函數,後面兩個是傳參 env->CallVoidMethod(gBTHandlerObject,
gNotifyCallBack.ackCBMethod, ack, strArg); } |
三. Jni多線程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
//創建線程 int creatThread(JNIEnv
*env){ /*保存當前的虛擬機。很多時候你無法隨意得到當前的env *我們可以通過保存當前的虛擬機,來隨時獲得當前的evn *C調用Java還有一種方法,就是讓Java在獲得出來的虛擬機上跑,這裏就不介紹了 */ env->GetJavaVM(&gJvm); assert (gJvm
!= NULL); pthread_create(&mReceivePt,
NULL, recv_thread, ( void *)tp); } //Thread
func void *recv_thread( void *args){ JNIEnv
*env; struct thread_para
*pThread_para = ( struct thread_para
*)args; //把當前線程依附在當前的env中,並獲得當前env if (gJvm->AttachCurrentThread(&env,
NULL) != JNI_OK){ LOGE( "%s:
AttachCurrentThread() failed" ,
__FUNCTION__); return NULL; } //get
son thread's id pThread_para->pid
= pthread_self(); //push
up the thread clean function pthread_cleanup_push(thread_clean,
args); while (1) { //main
function in son thread } //通知退出 pthread_cleanup_pop(1); //取消依附 if (gJvm->DetachCurrentThread()
!= JNI_OK){ LOGE( "%s:
DetachCurrentThread() failed" ,
__FUNCTION__); } LOGI( "Pthread
exit!!!" ); pthread_exit(0); } void thread_clean( void *args) { struct thread_para
*pThread_para = ( struct thread_para
*)args; LOGI( "thread_clean\n" ); } |
四. Jni多個目錄的Android.mk 編譯
有兩種情況會把源碼分爲多個目錄,一個是多個源碼在不同的目錄,但是生成的是同一個模塊。二是不同的目錄下的源碼生成一個模塊
第一種情況:
這種情況下是要把所有的源文件都加入到Android.mk LOCAL_SRC_FILES這個宏裏。
把源碼文件加入這個宏可以用幾個腳本函數:
以下腳本在alps\build\core\definitions.mk中定義
#找出子目錄的所有Java文件
LOCAL_SRC_FILES := $(call all-subdir-java-files)
#找出指定目錄的所有Java文件
LOCAL_SRC_FILES := $(call all-java-files-under,src tests)
#同樣還有C的腳本函數,可以到definitions.mk查找相應的函數
all-c-files-under
但是definitions.mk並沒有cpp的腳本函數那該怎麼寫呢?
假如我有源碼在bt文件夾和當前文件下,寫法如下:
1
2
3
4
5
|
bt_sources
:= $(wildcard $(LOCAL_PATH) /bt/ *.cpp) bt_sources
:= $(bt_sources:$(BT_DIR)/%=%) LOCAL_SRC_FILES
:= $(bt_sources:%=$(BT_DIR_NAME)/%) \ current.cpp |
第一句話的意思查找出這個路徑下所有的cpp文件,得出的結果是$(bt_sources) 的值爲:絕對路徑+目錄下所有的cpp文件
jni/bt/xxxa.cpp jni/bt/xxxb.cpp jni/bt/xxxc.cpp #注意:我們是在apk的源目錄下用ndk-build的,jni的源碼在jni目錄下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
LOCAL_PATH
:= $(call my- dir ) include
$(LOCAL_PATH) /SerialPoll/Android .mk #generate
libserialctrl.so include
$(CLEAR_VARS) LOCAL_PRELINK_MODULE
:= false BT_DIR_NAME
:= Bt BT_DIR
:= $(LOCAL_PATH)/$(BT_DIR_NAME) bt_sources
:= $(wildcard $(BT_DIR)/*.cpp) bt_sources
:= $(bt_sources:$(BT_DIR)/%=%) main_source
:= $(wildcard $(LOCAL_PATH)/*.cpp) main_source
:= $(main_source:$(LOCAL_PATH)/%=%) #$(warning
$(bt_sources)) #$(warning
$(main_source)) LOCAL_SRC_FILES
:= $(bt_sources:%=$(BT_DIR_NAME)/%) \ $(main_source) LOCAL_LDLIBS
:= -llog LOCAL_SHARED_LIBRARIES
:= \ libandroid_runtime\ liblog
\ libcutils
\ libnativehelper
\ libcore /include LOCAL_PRELINK_MODULE
:= false LOCAL_MODULE
:= libserialctrl include
$(BUILD_SHARED_LIBRARY) |
第二種情況:
只要在總的Android.mk裏include目標目錄的Android.mk即可。include $(LOCAL_PATH)/xxxx/Android.mk
目標目錄的Android.mk如基礎寫法。但源碼的路徑已經變了,要使用如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#generate
libserialpoll.so include
$(CLEAR_VARS) LOCAL_PRELINK_MODULE
:= false src_file
:= $(sildcard $(LOCAL_PATH) /xxxxxA/ *.cpp) LOCAL_SRC_FILES
:= $(src_file:%=xxxxxA/%) LOCAL_LDLIBS
:= -llog LOCAL_SHARED_LIBRARIES
:= \ libandroid_runtime\ liblog
\ LOCAL_PRELINK_MODULE
:= false LOCAL_MODULE
:= libserialpoll include
$(BUILD_SHARED_LIBRARY) |