1. 程式人生 > >Monkey原始碼分析之事件源

Monkey原始碼分析之事件源


上一篇文章《Monkey原始碼分析之執行流程》給出了monkey執行的整個流程,讓我們有一個概貌,那麼往後的文章我們會嘗試進一步的闡述相關的一些知識點。

這裡先把整個monkey類的結構圖給出來供大家參考,該圖源自網上(我自己的backbook pro上沒有安裝OmniGraffle工具,55美金,不捨得,所以直接貼網上的)


圖中有幾點需要注意下的:

  • MonkeyEventScript應該是MonkeySourceScript
  • MonkeyEventRandom應該是MonkeySourceRandom
  • 這裡沒有列出其他源,比如我們今天描述的重點MonkeySourceNetwork,因為它不是由MonkeyEventQueque這個類維護的,但其維護的事件佇列和MonkeyEventQueque一樣都是繼承於LinkedList的,所以大同小異

本文我們重點是以處理來來自網路sokcet也就是monkeyrunner的命令為例子來闡述事件源是怎麼處理的,其他的源大同小異。

1. 事件佇列維護者CommandQueque

在開始之前我們需要先去了解幾個基礎類,這樣子我們才方便分析。

我們在獲取了事件源之後,會把這些事件排隊放入一個佇列,然後其他地方就可以去把佇列裡面的事件取出來進一步進行處理了。那麼這裡我們先看下維護這個事件佇列的相應程式碼:

    public static interface CommandQueue {
        /**
         * Enqueue an event to be returned later.  This allows a
         * command to return multiple events.  Commands using the
         * command queue still have to return a valid event from their
         * translateCommand method.  The returned command will be
         * executed before anything put into the queue.
         *
         * @param e the event to be enqueued.
         */
        public void enqueueEvent(MonkeyEvent e);
    };

    // Queue of Events to be processed.  This allows commands to push
    // multiple events into the queue to be processed.
    private static class CommandQueueImpl implements CommandQueue{
        private final Queue<MonkeyEvent> queuedEvents = new LinkedList<MonkeyEvent>();

        public void enqueueEvent(MonkeyEvent e) {
            queuedEvents.offer(e);
        }

        /**
         * Get the next queued event to excecute.
         *
         * @return the next event, or null if there aren't any more.
         */
        public MonkeyEvent getNextQueuedEvent() {
            return queuedEvents.poll();
        }
    };

介面CommandQueue只定義個了一個方法enqueueEvent,由實現類CommandQueueImpl來實現,而實現類維護了一個MonkeyEvent型別的由LinkedList實現的佇列quequeEvents,然後實現了兩個方法來分別往這個佇列裡面放和取事件。挺簡單的實現,這裡主要是要提醒大家queueEvents這個佇列的重要性。這裡要注意的是MonkeyEventScript和monkeyEventRandom這兩個事件源維護佇列的類稍微有些不一樣,用的是MonkeyEventQueue這個類,但是其實這個類也是繼承自上面描述的LinkedList的,所以原理是一樣的。

最後建立和維護一個CommandQueueImple這個實現類的一個例項commandQueque來轉被對裡面的quequeEvents進行管理。

    private final CommandQueueImpl commandQueue = new CommandQueueImpl();

2. 事件翻譯員MonkeyCommand

下一個我們需要了解的基礎內部類就是MonkeCommand。從資料來源過來的命令都是一串字串,我們需要把它轉換成對應的monkey事件並存入到我們上面提到的由CommandQueque維護的事件佇列quequeEvents裡面。首先我們看下MonkeyCommand這個介面:

    /**
     * Interface that MonkeyCommands must implement.
     */
    public interface MonkeyCommand {
        /**
         * Translate the command line into a sequence of MonkeyEvents.
         *
         * @param command the command line.
         * @param queue the command queue.
         * @return MonkeyCommandReturn indicating what happened.
         */
        MonkeyCommandReturn translateCommand(List<String> command, CommandQueue queue);
    }

它只定義了一個實現類需要實現的方法translateCommand,從它的描述和接受的的引數可以知道,這個方法要做的事情就是把從事件源接受到的字串命令轉換成上面說的CommandQueue型別維護的那個eventQueues。以monkeyrunner發過來的press這個命令為例子,傳過來給monkey的字串是"press KEY_COKDE"(請檢視《MonkeyRunner原始碼分析之與Android裝置通訊方式》)

針對每一個命令都會有一個對應的MonkeyCommand的實現類來做真正的字串到事件的翻譯工作,以剛才提到的press這個命令為例子,我們看下它的實現程式碼:

    /**
     * Command to "press" a buttons (Sends an up and down key event.)
     */
    private static class PressCommand implements MonkeyCommand {
        // press keycode
        public MonkeyCommandReturn translateCommand(List<String> command,
                                                    CommandQueue queue) {
            if (command.size() == 2) {
                int keyCode = getKeyCode(command.get(1));
                if (keyCode < 0) {
                    // Ok, you gave us something bad.
                    Log.e(TAG, "Can't find keyname: " + command.get(1));
                    return EARG;
                }

                queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, keyCode));
                queue.enqueueEvent(new MonkeyKeyEvent(KeyEvent.ACTION_UP, keyCode));
                return OK;

            }
            return EARG;
        }
    }
以monkeyrunner過來的'press KEY_CODE'為例分析這段程式碼:
  • 從字串中得到第1個引數,也就是key_code
  • 判斷key_code是否有效
  • 建立按下按鍵的MonkeyKeyEvent事件並存入到CommandQueque維護的quequeEvents
  • 建立彈起按鍵的MonkeyKeyEvent事件並存入到CommandQueque維護的quequeEvents(press這個動作會出發按下和彈起按鍵兩個動作)
命令字串和對應的MonkeyCommand實現類的對應關係會由MonkeySourceNetwork類的COMMAND_MAP這個私有靜態成員來維護,這裡只是分析了"press"這個命令,其他的大家有興趣就自行分析,原理是一致的。
private static final Map<String, MonkeyCommand> COMMAND_MAP = new HashMap<String, MonkeyCommand>();

    static {
        // Add in all the commands we support
        COMMAND_MAP.put("flip", new FlipCommand());
        COMMAND_MAP.put("touch", new TouchCommand());
        COMMAND_MAP.put("trackball", new TrackballCommand());
        COMMAND_MAP.put("key", new KeyCommand());
        COMMAND_MAP.put("sleep", new SleepCommand());
        COMMAND_MAP.put("wake", new WakeCommand());
        COMMAND_MAP.put("tap", new TapCommand());
        COMMAND_MAP.put("press", new PressCommand());
        COMMAND_MAP.put("type", new TypeCommand());
        COMMAND_MAP.put("listvar", new MonkeySourceNetworkVars.ListVarCommand());
        COMMAND_MAP.put("getvar", new MonkeySourceNetworkVars.GetVarCommand());
        COMMAND_MAP.put("listviews", new MonkeySourceNetworkViews.ListViewsCommand());
        COMMAND_MAP.put("queryview", new MonkeySourceNetworkViews.QueryViewCommand());
        COMMAND_MAP.put("getrootview", new MonkeySourceNetworkViews.GetRootViewCommand());
        COMMAND_MAP.put("getviewswithtext",
                        new MonkeySourceNetworkViews.GetViewsWithTextCommand());
        COMMAND_MAP.put("deferreturn", new DeferReturnCommand());
    }

3. 事件源獲取者之getNextEvent

終於到了如何獲取事件的分析了,我們繼續以MonkeySourceNetwork這個處理monkeyrunner過來的網路命令為例子,看下它是如何處理monkeyrunner過來的命令的。我們先看下它實現的介面類MonkeyEventSource

/**
 * event source interface
 */
public interface MonkeyEventSource {
    /**
     * @return the next monkey event from the source
     */
    public MonkeyEvent getNextEvent();

    /**
     * set verbose to allow different level of log
     *
     * @param verbose output mode? 1= verbose, 2=very verbose
     */
    public void setVerbose(int verbose);

    /**
     * check whether precondition is satisfied
     *
     * @return false if something fails, e.g. factor failure in random source or
     *         file can not open from script source etc
     */
    public boolean validate();
}

這裡我最關心的就是getNextEvent這個介面,因為就是它來從socket獲得我們monkeyrunner過來的命令,然後通過上面描述的MonkeyCommand的實現類來把命令翻譯成最上面的CommandQueque維護的quequeEvents佇列的。往下我們會看它是怎麼做到的,這裡我們先看下介面實現類MonkeySourceNetwork的建構函式:
    public MonkeySourceNetwork(int port) throws IOException {
        // Only bind this to local host.  This means that you can only
        // talk to the monkey locally, or though adb port forwarding.
        serverSocket = new ServerSocket(port,
                                        0, // default backlog
                                        InetAddress.getLocalHost());
    }
所做的事情就是通過指定的埠例項化一個ServerSocket,這裡要注意它繫結的只是本地主機地址,意思是說只有本地的socket連線或者通過埠轉發連過來的adb埠(也就是我們這篇文章關注的monkeyrunner啟動的那個adb)才會被接受。

這裡只是例項化了一個socket,現在為止還沒有真正啟動起來的,也就是說還沒有開始真正的啟動對指定埠的監聽的。真正開始監聽是startServer這個方法觸發的:

    /**
     * Start a network server listening on the specified port.  The
     * network protocol is a line oriented protocol, where each line
     * is a different command that can be run.
     *
     * @param port the port to listen on
     */
    private void startServer() throws IOException {
        clientSocket = serverSocket.accept();
        // At this point, we have a client connected.
        // Attach the accessibility listeners so that we can start receiving
        // view events. Do this before wake so we can catch the wake event
        // if possible.
        MonkeySourceNetworkViews.setup();
        // Wake the device up in preparation for doing some commands.
        wake();

        input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        // auto-flush
        output = new PrintWriter(clientSocket.getOutputStream(), true);
    }
這裡除了開始監聽埠之外,還如monkeyrunner對埠讀寫的情況一樣,維護和例項化了input和output這兩個成員變數來專門對埠資料進行操作。
那麼這個startServer開始監聽資料的方法又是由誰呼叫的呢?這裡終於就來到了我們這一章節,也是本文的核心getNextEvent了
    public MonkeyEvent getNextEvent() {
        if (!started) {
            try {
                startServer();
            } catch (IOException e) {
                Log.e(TAG, "Got IOException from server", e);
                return null;
            }
            started = true;
        }

        // Now, get the next command.  This call may block, but that's OK
        try {
            while (true) {
                // Check to see if we have any events queued up.  If
                // we do, use those until we have no more.  Then get
                // more input from the user.
                MonkeyEvent queuedEvent = commandQueue.getNextQueuedEvent();
                if (queuedEvent != null) {
                    // dispatch the event
                    return queuedEvent;
                }

                // Check to see if we have any returns that have been deferred. If so, now that
                // we've run the queued commands, wait for the given event to happen (or the timeout
                // to be reached), and handle the deferred MonkeyCommandReturn.
                if (deferredReturn != null) {
                    Log.d(TAG, "Waiting for event");
                    MonkeyCommandReturn ret = deferredReturn.waitForEvent();
                    deferredReturn = null;
                    handleReturn(ret);
                }

                String command = input.readLine();
                if (command == null) {
                    Log.d(TAG, "Connection dropped.");
                    // Treat this exactly the same as if the user had
                    // ended the session cleanly with a done commant.
                    command = DONE;
                }

                if (DONE.equals(command)) {
                    // stop the server so it can accept new connections
                    try {
                        stopServer();
                    } catch (IOException e) {
                        Log.e(TAG, "Got IOException shutting down!", e);
                        return null;
                    }
                    // return a noop event so we keep executing the main
                    // loop
                    return new MonkeyNoopEvent();
                }

                // Do quit checking here
                if (QUIT.equals(command)) {
                    // then we're done
                    Log.d(TAG, "Quit requested");
                    // let the host know the command ran OK
                    returnOk();
                    return null;
                }

                // Do comment checking here.  Comments aren't a
                // command, so we don't echo anything back to the
                // user.
                if (command.startsWith("#")) {
                    // keep going
                    continue;
                }

                // Translate the command line.  This will handle returning error/ok to the user
                translateCommand(command);
            }
        } catch (IOException e) {
            Log.e(TAG, "Exception: ", e);
            return null;
        }
    }
有了以上介紹的那些背景知識,這段程式碼的理解就不會太費力了,我這裡大概描述下:
  • 啟動socket埠監聽monkeyrunner過來的連線和資料
  • 進入無限迴圈
    • 呼叫最上面描述的commandQueque這個事件佇列維護者例項來嘗試來從佇列獲得一個事件
    • 如果佇列由事件的話就立刻返回給上一篇文章《MonkeyRunner原始碼分析之啟動》描述的runMonkeyCles那個方法取呼叫執行
    • 如果佇列沒有事件的話,呼叫上面描述的socket讀寫變數input來獲得socket中monkeyrunner發過來的一行資料(也就是一個命令字串)
    • 呼叫translateCommand這個私有方法來針對不同的命令呼叫不同的MonkeyCommand實現類介面的translateCommand把字串命令翻譯成對應的事件並放到命令佇列裡面(這個命令上面還沒有描述,往下我會分析下)
    • 如果確實沒有命令了或者收到訊號要退出了等情況下就跳出迴圈,否則回到迴圈開始繼續以上步驟
好,我們還是看看剛才那個translateCommand的私有方法究竟是怎麼呼叫到不同命令的translateCommand介面的:
    /**
     * Translate the given command line into a MonkeyEvent.
     *
     * @param commandLine the full command line given.
     */
    private void translateCommand(String commandLine) {
        Log.d(TAG, "translateCommand: " + commandLine);
        List<String> parts = commandLineSplit(commandLine);
        if (parts.size() > 0) {
            MonkeyCommand command = COMMAND_MAP.get(parts.get(0));
            if (command != null) {
                MonkeyCommandReturn ret = command.translateCommand(parts, commandQueue);
                handleReturn(ret);
            }
        }
    }
很簡單,就是獲取monkeyunner進來的命令字串列表的的第一個值,然後通過上面的COMMAND_MAP把字串轉換成對應的MonkeyCommand實現類,然後呼叫其tranlsateCommand把該字串命令翻譯成對應的MonkeyEvent並存儲到事件佇列。 比如monkeyrunner過來的字串轉換成佇列是[‘press','KEY_CODE'],獲得第一個列表成員是press,那麼COMMAND_MAP對應於"press"字串這個key的MonkeyCommand就是:
COMMAND_MAP.put("press", new PressCommand());
所以呼叫的就是PressCommand這個MonkeyCommand介面實現類的translateCommand方法來把press這個命令轉換成對應的MonkeyKeyEvent了。

4.總結

最後我們結合上一章《Monkey原始碼分析之執行流程》把整個獲取事件源然後執行該事件的過程整理下:
  • Monkey啟動開始呼叫run方法
  • ran方法根據輸入的引數例項化指定的事件源,比如我們這裡的MonkeySourceNetwork
  • Monkey類中的runMonkeyCyles這個方法開始迴圈取事件執行
    • 呼叫Monkey類維護的mEventSource的getNextEvent方法來獲取一條事件,在本文例項中就是上面表述的MonkeySourceNetwork例項的getNextEvent方法
      • getNextEvent方法從CommandQueueImpl例項commandQueque所維護的quequeEvents裡面讀取一條事件
      • 如果事件存在則返回
      • getNextEvent方法啟動事件源讀取監聽,本文例項中就是上面的startServer方法來監聽monkeyrunner過來的socket連線和命令資料
      • getNextEvent方法從事件源讀取一個命令
      • getNextEvent方法通過呼叫對應的的MonkeyCommand介面實現類的translateCommand方法把字串命令翻譯成對應的monkey事件然後儲存到commandQueque維護的quequeEvents佇列
    • 執行返回event的injectEvent方法
好,事件源的分析就到此為止了,下一篇文章準備描述Monkey的Event,看它是如何執行這些事件的。


相關推薦

Monkey原始碼分析事件

上一篇文章《Monkey原始碼分析之執行流程》給出了monkey執行的整個流程,讓我們有一個概貌,那麼往後的文章我們會嘗試進一步的闡述相關的一些知識點。 這裡先把整個monkey類的結構圖給出來供大家參考,該圖源自網上(我自己的backbook pro上沒有安裝OmniG

Monkey原始碼分析事件注入

本系列的上一篇文章《Monkey原始碼分析之事件源》中我們描述了monkey是怎麼從事件源取得命令,然後將命令轉換成事件放到事件佇列裡面的,但是到現在位置我們還沒有了解monkey裡面的事件是怎麼一回事,本篇文章就以這個問題作為切入點,嘗試去搞清楚monkey的event架

Qt原始碼分析事件分發器QEventDispatcherWin32

分析Qt原始碼一則想自己在開發學習中有積累,同時自己也一直有一種理念,使用她那麼就更深入的認識她。 如果有分析不正確的,還煩請各位看官指正。 事件分發器建立 在QCoreApplication建構函式中 if (!QCoreApplicationPrivate

React原始碼分析事件系統

React原始碼分析之事件系統(轉載自阿里雲) react自己實現了一套高效的事件系統,包括了事件的註冊、儲存、分發、和重用,在DOM事件體系基礎上做了很大改進,減少了記憶體消耗,簡化了事件邏輯,並最大化的解決了IE等瀏覽器的事件不相容問題。與傳統的DOM體系相比,它有如下特點:

Monkey原始碼分析執行流程

在《MonkeyRunner原始碼分析之與Android裝置通訊方式》中,我們談及到MonkeyRunner控制目標android裝置有多種方法,其中之一就是在目標機器啟動一個monkey服務來監聽指定的一個埠,然後monkeyrunner再連線上這個埠來發送命令,驅動mo

Android事件分發機制原始碼分析Activity篇

在之前的事件分發分析中,曾提及到View的事件是由ViewGroup分發的,然而ViewGroup的事件我們只是稍微帶過是由Activity分發的。而我們知道,事件產生於使用者按下螢幕的一瞬間,事件生成後,經過一系列的過程來到我們的Activity層,那麼事件是怎樣從Activity傳遞

android原始碼分析View的事件分發(上)

1、View的繼承關係圖 View的繼承關係圖如下: 其中最重要的子類為ViewGroup,View是所有UI元件的基類,而ViewGroup是容納這些元件的容器,同時它也是繼承於View類。而UI元件的繼承關係如上圖,比較常用的元件類用紅色字型標出

Live555 原始碼分析延遲事件處理

live555的延遲事件        主要存放在BasicTaskScheduler0的成員變數        DelayQueue fDelayQueue;中        其中        D

Netty原始碼分析ChannelPipeline—入站事件的傳播

之前的文章中我們說過ChannelPipeline作為Netty中的資料管道,負責傳遞Channel中訊息的事件傳播,事件的傳播分為入站和出站兩個方向,分別通知ChannelInboundHandler與ChannelOutboundHandler來觸發對應事件。這篇文章我們先對Netty中入站事件的傳播,也

Netty原始碼分析ChannelPipeline—出站事件的傳播

上篇文章中我們梳理了ChannelPipeline中入站事件的傳播,這篇文章中我們看下出站事件的傳播,也就是ChannelOutboundHandler介面的實現。 1、出站事件的傳播示例 我們對上篇文章中的示例程式碼進行改造,在ChannelPipeline中加入ChannelOutboundHandler

Spark原始碼分析Spark Shell(上)

https://www.cnblogs.com/xing901022/p/6412619.html 文中分析的spark版本為apache的spark-2.1.0-bin-hadoop2.7。 bin目錄結構: -rwxr-xr-x. 1 bigdata bigdata 1089 Dec

Netty 原始碼分析拆包器的奧祕

為什麼要粘包拆包 為什麼要粘包 首先你得了解一下TCP/IP協議,在使用者資料量非常小的情況下,極端情況下,一個位元組,該TCP資料包的有效載荷非常低,傳遞100位元組的資料,需要100次TCP傳送,100次ACK,在應用及時性要求不高的情況下,將這100個有效資料拼接成一個數據包,那會縮短到一個TCP資

【Android】原始碼分析 - View事件分發機制

事件分發物件 (1)所有 Touch 事件都被封裝成了 MotionEvent 物件,包括 Touch 的位置、時間、歷史記錄以及第幾個手指(多指觸控)等。 (2)事件型別分為 ACTION_DOWN, ACTION_UP,ACTION_MOVE,ACTION_POINTER_D

Android原始碼分析為什麼在onCreate() 和 onResume() 獲取不到 View 的寬高

轉載自:https://www.jianshu.com/p/d7ab114ac1f7 先來看一段很熟悉的程式碼,可能在最開始接觸安卓的時候,大部分人都寫過的一段程式碼;即嘗試在 onCreate() 和 onResume() 方法中去獲取某個 View 的寬高資訊: 但是列印輸出後,我們會發

netty原始碼分析服務端啟動

ServerBootstrap與Bootstrap分別是netty中服務端與客戶端的引導類,主要負責服務端與客戶端初始化、配置及啟動引導等工作,接下來我們就通過netty原始碼中的示例對ServerBootstrap與Bootstrap的原始碼進行一個簡單的分析。首先我們知道這兩個類都繼承自AbstractB

SNMP原始碼分析(一)配置檔案部分

snmpd.conf想必不陌生。在程序啟動過程中會去讀取配置檔案中各個配置。其中幾個引數需要先知道是幹什麼的:   token:配置檔案的每行的開頭,例如 group MyROGroup v1 readSec 這行token的引數是group。  

【kubernetes/k8s原始碼分析】kubelet原始碼分析cdvisor原始碼分析

  資料流 UnsecuredDependencies -> run   1. cadvisor.New初始化 if kubeDeps.CAdvisorInterface == nil { imageFsInfoProvider := cadv

【kubernetes/k8s原始碼分析】kubelet原始碼分析容器網路初始化原始碼分析

一. 網路基礎   1.1 網路名稱空間的操作 建立網路名稱空間: ip netns add 名稱空間內執行命令: ip netns exec 進入名稱空間: ip netns exec bash   1.2 bridge-nf-c

【kubernetes/k8s原始碼分析】kubelet原始碼分析資源上報

0. 資料流   路徑: pkg/kubelet/kubelet.go   Run函式() ->   syncNodeStatus ()  ->   registerWithAPIServer() ->

【kubernetes/k8s原始碼分析】kubelet原始碼分析啟動容器

主要是呼叫runtime,這裡預設為docker 0. 資料流 NewMainKubelet(cmd/kubelet/app/server.go) -> NewKubeGenericRuntimeManager(pkg/kubelet/kuberuntime/kuberuntime