做程序员苦逼的地方就在于,当公司决定做什么的时候,是不会跟你商量的,只会跟你说,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();
mapRecv = new ConcurrentHashMap();
controlQueue = new ConcurrentLinkedQueue();
finshAndWait = new HashMap();
errorSendTaskID = new HashSet();
errorRecvTaskID = new HashSet();
// 对象锁
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 mapSend;
// 接收task的map列表
private ConcurrentHashMap 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.aidl
interface 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.aidl
interface 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 !