Android雙屏驅動Service架構實現
做程式設計師苦逼的地方就在於,當公司決定做什麼的時候,是不會跟你商量的,只會跟你說,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.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 !