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)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

竖子敢尔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值