1. 程式人生 > >Android雙屏驅動Service架構實現

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 !