Android Jni 多線程 藍牙串口收發 實例 一

                在工作有一個這樣的需求:在一個Android App上,通過串口對一個藍牙進行操作,其中包括髮送消息,接收消息,並進行處理。    

=========================項目心得和遇上的問題總結=========================

    要實現這些功能,有很多種:多線程可以放在Jni層,這樣接收和消息的整理邏輯都在Jni層,這樣程序就會變得複雜一些,因爲你不僅要Java調用C,還要C調用Java。我們也可以把這些邏輯層放在App層處理,Jni層只負責打開串口文件,並fd組織成FildDescriptor返回給App。其實如果按照程序的封裝設計,Jni不應該有過多的邏輯處理,邏輯處理都應該交給App,用Java來寫,這樣的優點在於,相對於C來說,Java比較好寫,不用考慮指針和垃圾回收,內存溢出,線程安全等問題也會少很多。最重要的是,把邏輯處理放在App容易提高整個程序的維護性,和Jni層的複用性,只要把Jni打包成庫給別人使用即可。但是我們考慮到,C的效率更高,最主要C對字符處理和位的處理更容易。然後就很任性地選擇了用C來實現一些雜亂的處理,當然,也是想挑戰一下Jni下的編程。這裏面也確實遇上了很多問題,這裏就做一些簡單總結吧。

        首先,我們Jni還是用C++寫比較好。因爲如果你比較懶,在Jni 編程裏面提供的接口,一個同名的函數C++比C會少一些傳參,具體比較一下jni.h就可發現。然後還有一個問題,在Android系統的Frameworks裏會使用Jni會做一些工具,如果我們把我們寫好的Jni放進系統裏編譯就可以很方便地利用這些工具,如用AndroidRuntime 可以很方便地獲得Jnv(運行時環境)和JavaVm(當前App的虛擬機)。用C++還有一個優點是,如果你是C++編程高手,你可以很容易用C++寫一個架構很好的Jni,如果不是,你也可以當C來用。
        但是,用C++來寫會帶來一個很致命的問題。就是編譯好後在Java調用會Jni會提示,無法找到庫,和無法找到對應的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)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章