Android双屏驱动Service架构实现
来源:互联网 发布:caxa机械画图软件 编辑:程序博客网 时间:2024/05/23 16:59
做程序员苦逼的地方就在于,当公司决定做什么的时候,是不会跟你商量的,只会跟你说,xxx,这个可能需要你来实现一下。fuck,想好实现思路了吗?(这是我司的程序员提出,我们来做整理完善的)
Android双屏显示,可能会和别的双屏机制不同,大多数情况下是一个android系统,分主副屏而已。我司的硬件是两个android系统,两个屏幕,内部通过一根usb直连(这根usb连接线很稳定,代工厂和我们讲的,坑~)。双屏运行两个独立的android系统,相互通过一条底层的通道传输数据,主屏通常可以运行业务软件。副屏可以显示宣传图片,视频,购物清单等信息,但不限于这些,实际上副屏也是个可以触摸交互的系统。
在这样的硬件前提下,我们开发需要实现这两个屏幕的通信,涉及到usb的驱动开发(由代工厂搞定),我们只需要调用jni的一些方法即可。上层应用之间的通信,类似于广播,主副屏可以相互发送接收,提供公共的api,可供其他的app调用,使之能实现自己的业务逻辑。
本片文章主要讲底层的Service的实现(也就是驱动的上一层)
思路
双屏通信,主屏会要求副屏显示一些文字(命令),图片(发送文件+命令),图文混合,甚至会发送一些音频,视频等大文件,几百M到几G不等。因为是双向通信,主屏发送指令过后,需要等待副屏的回调。通常如果是命令或者是小文件,毫秒级别内就能被处理掉。但如果是几个G的大文件呢?时间就被延时了,如果这个时候再有命令发送过来了,就会等待(需要维护一个任务列表),这样肯定是不好的。所以我们切割文件,将文件分包,一个一个的发送,最后拼装还原,这样即使中途了命令或者小文件,也能立马被处理掉。
我们有一个任务队列,service不断的去任务队列去轮询,取到任务,根据Task信息,区分是任务类别,做相应的处理。如果是文件的话,我们进行分包处理,这里,我暂定义的最大单个文件包为512kb,然后发送。副屏接收,拼装,还原(每个包有相应的头信息),在给主屏反馈结果,主屏做相应处理。
大致的一个流程图:
协议与机制
其实在整个流程中,我们主要要区分任务的来源,以及之后要反馈的源头,分包与还原包不能错乱,不然会产生脏数据。
那我们定义一下任务的类型:FileTask(文件任务),MemoryTask(内存任务,字符串之类的),ControlTask(控制任务)。对不同的任务类型处理是不同的,有的直接是内存传输,有的是写本地文件。
之前在usb通道还没有连通的时候,我们是用UDP协议来写demo实现的,在usb通来以后,直接改用就可以来。所以说,即使没有这个usb通道,你也可以用udp连接来测试两个手机直接的传输,只是这个速度就依赖于网络了。
具体实现
我们有一个底层service,CoreService,所有的发送、接收,回调都是靠它来实现的。那我们就具体围绕它来展开。
@Override public void onCreate() { mSerialPort = new SerialPort(); connectRunable = new Connect(); new Thread(connectRunable).start(); }
SerialPort类里面是一些native的方法,通过jni来调用底层的usb驱动的,这个就略过来,各家的都是不一样的。我们只要知道它有读写的方法即可。
public native int read(int fd, byte[] data, int offset, int len); public native int write(int fd, byte[] data, int offset, int len);
Connect类是连接操作,副屏向主屏发起连接的请求。
class Connect implements Runnable { @Override public void run() { id = 0; // 在建立连接之前,断开之前所有的任务 if (mapSend != null) { for (Integer i : mapSend.keySet()) { for (SendTask sendTask : mapSend.get(i).tasksFile) { try { sendTask.setTaskState(TaskState.error_sendData); sendTask.getCallback().sendError(sendTask.getID(), sendTask.getTaskState().get_code(), sendTask.getTaskState().get_message()); } catch (Exception e) { } } for (SendTask sendTask : mapSend.get(i).tasksMemory) { try { sendTask.setTaskState(TaskState.error_sendData); sendTask.getCallback().sendError(sendTask.getID(), sendTask.getTaskState().get_code(), sendTask.getTaskState().get_message()); } catch (Exception e) { } } } } // 重新初始化参数 mapSend = new ConcurrentHashMap<Integer, SendProcess>(); mapRecv = new ConcurrentHashMap<Integer, RecvTask>(); controlQueue = new ConcurrentLinkedQueue<ControlTask>(); finshAndWait = new HashMap<Integer, Task>(); errorSendTaskID = new HashSet<Integer>(); errorRecvTaskID = new HashSet<Integer>(); // 对象锁 lock = new Object(); // 线程控制锁 controlThreadLock = new Object(); controlProcess = new SendProcess(errorSendTaskID, controlThread, finshAndWait); mapSend.put(-1, controlProcess); controlThread = new ControlThread(controlQueue, controlProcess, lock, controlThreadLock, finshAndWait, errorSendTaskID); controlProcess.setControlThread(controlThread); controlThread.start(); while (true) { // usb端口打开 if (mSerialPort.tryOpen(isMain)) { // isMain true:表示主屏 false:表示副屏 if (isMain) { byte[] temp = new byte[1]; boolean flag = false; do { // 读取建立请求连接的数据 -1 if (mSerialPort.read(mSerialPort.mFd, temp, 0, 1) <= 0) { flag = true; break; } else if (temp[0] != -1 && temp[0] != -2) { flag = true; break; } else if (temp[0] == -2) { flag = false; break; } // 向副屏发送建立请求连接的数据 -1 if (mSerialPort.write(mSerialPort.mFd, temp, 0, 1) <= 0) { flag = true; break; } } while (temp[0] == -1); if (flag) { continue; } } else { // 向主屏发送建立请求连接的数据 -1 sendsyn = new sendSYN(); sendsynThread = new Thread(sendsyn); sendsynThread.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } byte[] temp = new byte[1]; mSerialPort.clear(mSerialPort.mFd); if (mSerialPort.read(mSerialPort.mFd, temp, 0, 1) <= 0) { sendsyn.gh = false; continue; } else if (temp[0] != -1) { sendsyn.gh = false; continue; } sendsyn.gh = false; try { sendsynThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } temp[0] = -2; // 读取建立请求连接的数据 -1 if (mSerialPort.write(mSerialPort.mFd, temp, 0, 1) <= 0) { sendsyn.gh = false; continue; } } // 发送处理 transferSend = new TransferSend(lock, mapSend, mSerialPort, handler); // 接收处理 transferRecv = new TransferRecv(mapRecv, mSerialPort, controlThread, errorRecvTaskID, handler); transferRecv.start(); transferSend.start(); // 发送handler表示连接成功 handler.sendMessage(handler.obtainMessage(-2)); break; } else { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } isConnecting = false; } }
代码有点多,来分析一下初始化的整个连接过程。
// 发送task的map列表 private ConcurrentHashMap<Integer, SendProcess> mapSend; // 接收task的map列表 private ConcurrentHashMap<Integer, RecvTask> mapRecv;
1.首先,在建立连接之前,断开之前所有的任务,Task的TaskState.error_sendData就是表示usb数据通道断开,取消所有发送接收的任务。
2.初始化参数。包括发送,接收的列表,锁对象,任务id等,ControlThread类是一个线程控制类,来处理task的。SendProcess类是发送task的包装类,把它丢到ControlThread类去处理,然后开启Thread,让它不停的去轮询任务队列。
3.开始建立发送与接收的连接。isMain这个字段,true表示主屏,false表示副屏。先来看副屏的代码,初始化一个sendSYN类。它向主屏发送一个为-1字节的数据,然后如果主屏收到一个为-1的数据,就表示这是副屏发起的连接请求(正常的数据请求是不可能为-1 的)。主屏收到以后,也向副屏发送一个-1字节的数据,如果副屏也收到来这个数据,表示双屏建立连接成功。
连接通信成功,就可以相互发送数据了。来看下定义的Task这个类。
这里面是一些数据信息,hasSendedLength,hasRecvLength,fileLength用来分包,还原包的,sender表示发送者,成功或失败要反馈发送者…
TaskType是一个枚举
分为文件任务,内存任务,控制任务三种。
那我们是如何向副屏发送任务的呢?是通过服务的aidl来发送的和接收回调信息的。
// SendService.aidlinterface SendService { int sendFileToFile(in String recvPackageName,in String path,boolean isReport, long userFlag,in SendServiceCallback callback); int sendByteToMemory(in String recvPackageName,in byte [] data,in SendServiceCallback callback);}
在CoreService的onBind()方法中,返回了此对象。现在来看下SendService的具体实现。
@Override public IBinder onBind(Intent intent) { if (callback == null) { callback = new CallBack(); } return callback; } private class CallBack extends SendService.Stub { @Override public int sendFileToFile(String recvPackageName, String path, boolean isReport, long userFlag, SendServiceCallback callback) throws RemoteException { SendProcess process = mapSend.get(Binder.getCallingUid()); if (process == null) { process = new SendProcess(errorSendTaskID, controlThread, finshAndWait); mapSend.put(Binder.getCallingUid(), process); } int tem_id = getID(); synchronized (lock) { process.addTask(new FileTask(tem_id, getApplicationContext().getPackageManager().getNameForUid(Binder.getCallingUid()), recvPackageName, path, isReport, userFlag, callback)); lock.notify(); } return tem_id; } @Override public int sendByteToMemory(String recvPackageName, byte[] data, SendServiceCallback callback) throws RemoteException { SendProcess process = mapSend.get(Binder.getCallingUid()); if (process == null) { process = new SendProcess(errorSendTaskID, controlThread, finshAndWait); mapSend.put(Binder.getCallingUid(), process); } int tem_id = getID(); synchronized (lock) { process.addTask(new MemoryTask(tem_id, getApplicationContext().getPackageManager().getNameForUid(Binder.getCallingUid()), recvPackageName, data, callback)); lock.notify(); } return tem_id; } }
主要是从map队列中取出客户端任务,放进任务列表,唤醒处理线程,执行任务。主要发送任务是SendProcess类,其中我们定义了sendM()和sendF()方法,来发送字符串命令和文件。
最终都通过task的send()方法来发送,其实也就是前面所说的SerialPort中的write()这个jni方法。
其中fillData()方法,也就是前面所有的给Task填充数据,包括请求头,大小,描述等。
下面我们再来看一下接收的方法,也是前面所说的SerialPort的read()来读取发送过来的数据,通过aidl回调主屏。其中的回调callback在任务创建的时候传入的,在控制线程和发送线程中对其作出相应的回调处理。
// SendServiceCallback.aidlinterface SendServiceCallback { oneway void sendSuccess(int id); oneway void sendError(int id,int errorId, String errorInfo); oneway void sendProcess(int id, long totle, long sended);}
获取到数据,进行拼装还原,取出任务的携带信息,进行分类处理。
byte flag = data[4]; int taskId = ByteUtil.bytes2Int(data, 5); fileTaskRecv = (RecvTask) mapRecv.get(taskId); int sendPackNameLength = ByteUtil.bytes2Int(data, 9); int recvPackNameLength = ByteUtil.bytes2Int(data, 13 + sendPackNameLength); if (fileTaskRecv == null) { if (errorTaskID.contains(taskId)) { return; } String sendPackName = new String(data, 13, sendPackNameLength, "utf-8"); String recvPackName = new String(data, 17 + sendPackNameLength, recvPackNameLength, "utf-8"); int timeOut = ByteUtil.bytes2Int(data, 17 + recvPackNameLength + sendPackNameLength); long fileLength = ByteUtil.bytes2long(data, 21 + recvPackNameLength + sendPackNameLength); long userFlag = ByteUtil.bytes2long(data, 29 + recvPackNameLength + sendPackNameLength);
最后通过Service中的Handler来回调处理,其实是通过广播发送出去的,所以需要接收双屏通信信息的app都要注册该广播。
存在问题与改进空间
这样,双屏通信的大致流程就已经说完了,其中有一些需要补充和完善的地方,因为项目紧急,所以第一版上线的也比较粗糙,后续会陆续改进的。但我想,不管是怎样改,分包,发送,接收,回调,原包这样逻辑应该是通用的。
问题:1.接收到的文件没有处理,因为sdcard的内存是有限的。
2.如果连接断开(像强行关机之类),失败的任务是不能复原的。在初始化的时候,我们把所有以前的任务都当作失败任务放弃掉了。这里其实可以做一些缓存处理,在拿出未完成的任务,在重连的时候继续处理。
还有一些东西可能还没有考虑到,在后面无尽的需求中,也许会增加上去。
Beating Heart !
- Android双屏驱动Service架构实现
- Android实现双屏异显
- Android实现双屏异显
- Android实现双屏异显
- Android实现双屏异显
- Android实现双屏异显
- Android 利用presentation实现双屏异显
- android presentation实现双屏异显
- Android双屏异显的实现
- Android2.3异步双屏修改记录之android CS架构
- Android2.3异步双屏修改记录之android CS架构
- Android2.3异步双屏修改记录之android CS架构
- android实现双屏异显,双触摸输入
- Android 系统中 Location Service 的实现与架构
- Android 系统中 Location Service 的实现与架构
- Android 系统中 Location Service 的实现与架构
- Android 系统中 Location Service 的实现与架构
- Android 系统中 Location Service 的实现与架构
- linux 小知识
- hadoop的PID路径问题
- 如何理解JS中的this指向问题
- Comparator接口和Comparable接口的区别
- 分数加减法
- Android双屏驱动Service架构实现
- 细说java对象的浅复制和深复制
- 如何写toolbar
- 键盘Key Code对照表和 ASCII 字符集
- Java基本六:代理
- caffe学习资料
- 第八周—C语言 穷举法(换分币问题)
- JEECG弹出表单调用列表刷新
- Java 访问修饰符