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