=========================项目心得和遇上的问题总结=========================
要实现这些功能,有很多种:多线程可以放在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) |