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)


猜你喜欢

转载自blog.csdn.net/shell812/article/details/49763249