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

Monkey原始碼分析之事件注入

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

在看這篇文章之前,希望大家最好先去看下另外幾篇博文,這樣理解起來就會更容易更清晰了:

1. 事件架構

這裡我們先從上一篇文章《Monkey原始碼分析之事件源》中來自網上的monkey架構圖中擷取MonkeyEvent相關的部分來看下MonkeyEvent的架構是怎麼樣的。

從上圖可以看到,MonkeyEvent定義了三個public方法,然後繼承下來的有5個不同的類,每個類對應一種事件型別:
  • MonkeyActivityEvent: 代表Activity相關的事件
  • MonkeyMotionEvent:代表Motion相關的事件
  • MonkeyKeyEvent: 代表Key相關的事件
  • MonkeyFlibEvent: 代表Flib相關的事件
  • MonkeyThrottleEvent:代表睡眠事件
圖中還描述了這是一個command設計模式,其實僅僅在這個圖裡面是沒有看出來就是command設計模式的,往下我們會描述它究竟是怎麼實現了command設計模式的。 這裡我們先拿一個例項來看下一個具體的event是怎麼構成的,這裡為了連貫性,我們就拿一個上一篇文章描述的通過網路事件源過來的一個事件做描述吧,這裡我挑了MonkeyKeyEvent。

2. 構建MonkeyKeyEvent

這裡它的父類MonkeyEvent我們就不深入描述了,因為它只是聲明瞭幾個方法而已,只要腦袋裡知道其聲明瞭一個很重要的injectKeyEvent的方法,每個子類都需要通過實現它來注入事件就可以了。 現在我們先來看下MonkeyKeyEvent的建構函式:
public class MonkeyKeyEvent extends MonkeyEvent {
    private long mDownTime = -1;
    private int mMetaState = -1;
    private int mAction = -1;
    private int mKeyCode = -1;
    private int mScancode = -1;
    private int mRepeatCount = -1;
    private int mDeviceId = -1;
    private long mEventTime = -1;

    private KeyEvent keyEvent = null;

    public MonkeyKeyEvent(int action, int keycode) {
        super(EVENT_TYPE_KEY);
        mAction = action;
        mKeyCode = keycode;
    }

    public MonkeyKeyEvent(KeyEvent e) {
        super(EVENT_TYPE_KEY);
        keyEvent = e;
    }

    public MonkeyKeyEvent(long downTime, long eventTime, int action,
            int code, int repeat, int metaState,
            int device, int scancode) {
        super(EVENT_TYPE_KEY);

        mAction = action;
        mKeyCode = code;
        mMetaState = metaState;
        mScancode = scancode;
        mRepeatCount = repeat;
        mDeviceId = device;
        mDownTime = downTime;
        mEventTime = eventTime;
    }
MonkeyKeyEvent有多個建構函式,引數都不一樣,但是目的都只有一個,通過傳進來的引數獲得足夠的資訊儲存成成員變數,以便今後建立一個android.view.KeyEvent,皆因該系統事件就是可以根據不同的引數進行初始化的。比如下面的getEvent方法就是根據不同的引數建立對應的KeyEvent的。注意這系統KeyEvent是非常重要的,因為我們今後通過WindowManager注入事件就要把它的物件傳進去去驅動相應的按鍵相關的事件。
     * @return the key event
     */
    private KeyEvent getEvent() {
        if (keyEvent == null) {
            if (mDeviceId < 0) {
                keyEvent = new KeyEvent(mAction, mKeyCode);
            } else {
                // for scripts
                keyEvent = new KeyEvent(mDownTime, mEventTime, mAction,
                                        mKeyCode, mRepeatCount, mMetaState, mDeviceId, mScancode);
            }
        }
        return keyEvent;
    }
支援的成員變數比較多,名字都挺淺顯易懂,我這裡就簡單描述兩個我們最常用的:
  • mAction:代表了這個keyevent的動作,就是系統KeyEvent裡面定義的ACTION_DOWN,ACTION_UP或者ACTION_MULTIPLE.
  • mKeyCode: 代表了你按下的究竟是哪個按鍵,同樣是在系統的KeyEvent定義的,比如82就代表了我們的系統選單這個鍵值。
    public static final int KEYCODE_MENU            = 82;

3. 獲取視窗事件注入者WindowManager

既然要往系統注入事件,那麼首先要做的事情當然是先去獲得注入事件的管理類,然後例項化它來給我們呼叫了,我們注入事件用的就是WindowManager這個類,而它的例項化是在monkey啟動的時候通過main函式呼叫的run那裡開始初始化的:
    private int run(String[] args) {
        ...
        if (!getSystemInterfaces()) {
            return -3;
        }
        ....
}
那麼我們進入該方法看下我們需要的WindowManager是怎麼初始化的。
    private boolean getSystemInterfaces() {
        mAm = ActivityManagerNative.getDefault();
        if (mAm == null) {
            System.err.println("** Error: Unable to connect to activity manager; is the system "
                    + "running?");
            return false;
        }

        mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
        if (mWm == null) {
            System.err.println("** Error: Unable to connect to window manager; is the system "
                    + "running?");
            return false;
        }

        mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
        if (mPm == null) {
            System.err.println("** Error: Unable to connect to package manager; is the system "
                    + "running?");
            return false;
        }

        try {
            mAm.setActivityController(new ActivityController());
            mNetworkMonitor.register(mAm);
        } catch (RemoteException e) {
            System.err.println("** Failed talking with activity manager!");
            return false;
        }

        return true;
    }
這裡我們主要是要理解裡面用到的一些管理類。這裡其實我們真正值得關注的就是WindowManager這個類,因為我們注入真實時間的時候其實就是呼叫了它的方法。其他的類其實在我們這篇文章中並沒有用到的,但是既然看到了就順便了解下吧。 我們先看下程式碼中提到的ActivityManagerNative這個類相關的資訊,具體請檢視轉發的博文《ActivityManager框架解析》,個人認為寫的挺不錯的。以下我按照自己的理解簡單描述了下
  • ActivityManager: 管理著系統的所有正在執行的activities,通過它可以獲得系統正在執行的tasks,services,記憶體資訊等。正常來說我們的應用可以同通過(ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)例項化。而它提供的方法的操作都是依賴於ActivityManagerNativeProxy這個代理類來實現的
  • ActivityManagerNative:ActivityManagerProxy實現了介面IActivitManager,但並不真正實現這些方法,它只是一個代理類,真正動作的執行為Stub類ActivityManagerService,ActivityManagerService物件只有一個並存在於system_process程序中,ActivityManagerService繼承於ActivityManagerNative存根類。
  • ActivityManagerProxy:程式碼中的第一行mAm = ActivityManagerNative.getDefault();獲得的其實就是ActivityManagerProxy的物件,而不是ActivityManagerNative
  •  IWindowManager:WindowManager主要用來管理視窗的一些狀態、屬性、view增加、刪除、更新、視窗順序、訊息收集和處理等,但在android1.6以後隱藏掉了。這裡之所以還能呼叫是因為monkey是在有android原始碼的情況下編譯出來的,如果沒有原始碼的話,那麼就需要用到反射機制利用Class.forName來呼叫獲取了。

然後是PackageManager:
  • PackageManager:本類API是對所有基於載入資訊的資料結構的封裝,包括以下功能:
  • 安裝,解除安裝應用查詢permission相關資訊
  • 查詢Application相關資訊(application,activity,receiver,service,provider及相應屬性等)
  • 查詢已安裝應用
  • 增加,刪除permission
  • 清除使用者資料、快取,程式碼段等
  • ServiceManager:ServiceMananger是android中比較重要的一個程序,它是在init程序啟動之後啟動,從名字上就可以看出來它是用來管理系統中的service。比如:InputMethodService、ActivityManagerService等。在ServiceManager中有兩個比較重要的方法:add_service、check_service。系統的service需要通過add_service把自己的資訊註冊到ServiceManager中,當需要使用時,通過check_service檢查該service是否存在

4.WindowManager往系統視窗注入事件

那麼到了現在我們已經獲得了要WindowManager物件了,下一步就要看MonkeyKeyEvent是怎麼使用這個物件來向系統視窗傳送按鍵key事件的了。我們定位到injectEvent這個方法。
    @Override
    public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {
        if (verbose > 1) {
            String note;
            if (mAction == KeyEvent.ACTION_UP) {
                note = "ACTION_UP";
            } else {
                note = "ACTION_DOWN";
            }

            try {
                System.out.println(":Sending Key (" + note + "): "
                        + mKeyCode + "    // "
                        + MonkeySourceRandom.getKeyName(mKeyCode));
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println(":Sending Key (" + note + "): "
                        + mKeyCode + "    // Unknown key event");
            }
        }

        // inject key event
        try {
            if (!iwm.injectKeyEvent(getEvent(), false)) {
                return MonkeyEvent.INJECT_FAIL;
            }
        } catch (RemoteException ex) {
            return MonkeyEvent.INJECT_ERROR_REMOTE_EXCEPTION;
        }

        return MonkeyEvent.INJECT_SUCCESS;
    }
注意傳入引數
  • iwm:這個就是我們前面獲取到的WindowManager的例項物件
  • iam:ActivityManager的例項物件,其實在這裡我們並不需要用到,但是為了相容其他MonkeyXXXEvent對這個介面方法的實現,這裡還是要傳進來,但不作處理
整個方法程式碼不多,最終就通過呼叫iwm.injectKeyEvent方法,傳入上面MonkeyKeyEvent初始化的時候建立的是系統KeyEvent物件,來實現按鍵事件的注入,這樣就能模擬使用者按下系統選單等按鍵的功能了。

5.monkey注入事件處理方式分類

剛才以MonkeyKeyEvent作為例項來描述了該型別的事件是怎麼構造以及如何在重寫MonkeyEvent抽象父類的injectEvent時呼叫iWindowManager這個隱藏類的injectKeyEvent方法來注入按鍵事件的。其實其他的事件型別重寫MonkeyEvent的injectEvent方法的時候並不一定會真正的往系統視窗注入事件的,比如MonkeyThrottleEvent實現的injectEvent其實就僅僅是睡眠一下而已:
    @Override
    public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {

        if (verbose > 1) {
            System.out.println("Sleeping for " + mThrottle + " milliseconds");
        }
        try {
            Thread.sleep(mThrottle);
        } catch (InterruptedException e1) {
            System.out.println("** Monkey interrupted in sleep.");
            return MonkeyEvent.INJECT_FAIL;
        }
        
        return MonkeyEvent.INJECT_SUCCESS;
    }
所以雖然不同的MonkeyEvent實現類都實現了父類的injectEvent方法,但是並不是所有的的MonkeyEvent都需要注入事件的。所有這個介面方法的名字我覺得Google 工程師起得不好,比如叫做handleEvent就不會造成混亂了(個人見解) 以下列表列出了monkey支援的關鍵事件的不同處理方法:

事件處理方式

MonkeyEvent實現類

關鍵程式碼

註釋

通過WindowManager注入事件

MonkeyKeyEvent

injectKeyiwm.injectKeyEvent(getEvent(),false)Event

MonkeyTouchEvent

iwm.injectPointerEvent(me,false)

MonkeyTrackballEvent

iwm.injectTrackballEvent(me,false)

通過往事件裝置/dev/input/event0傳送命令注入事件

MonkeyFlipEvent

FileOutputStream("/dev/input/event0")

通過ActvityManagerstartInstrumentation方法啟動一個應用

MonkeyInstrumentationEvent

iam.startInstrumentation(cn,null, 0,args,null)

睡眠

MonkeyThrottleEvent

Thread.sleep(mThrottle)

MonkeyWaitEvent

Thread.sleep(mWaitTime)


6. MonkeyEvent之Command模式

都說MonkeyEvent使用Command模式來設計得,那麼究竟command設計模式是怎麼樣得呢?我們先看下下圖。
那麼我們對號入座,看下MonkeyEvent得設計是否滿足該command模式的要求:
  • Command:MonkeyEvent,聲明瞭injectEvent這個execute介面方法
  • ConcreteCommand:  各個MonkeyEvent實現類:MonkeyKeyEvent,MonkeyTouchEvent,MonkeyWaitEvent...
  • Client:  Monkey,記得它在runMonkeyCyles方法中呼叫了mEventSource.getNextEvent()方法來從事件源獲取事件,並根據各個事件源的translateCommand方法來建立對應事件(ConcretCommand)吧?不記得的話請先看《Monkey原始碼分析之執行流程》和《Monkey原始碼分析之事件源
  • Receiver:  WindowManager等的例項物件,因為是它們最終實施和執行了injectXXXEvent這些請求。
  • Invoker:  Monkey,因為直接呼叫MonkeyKevent(command)的injectEvent(execute)這個方法的地方依然是在Monkey的runMonkeyCeles這個方法中:ev.injectEvent(mWm,mAm,mVerbose)。所以Monkey在這裡既扮演餓Command角色,又扮演了Invoker這個角色。
從中可以看到 MonkeyEvent的設計確實是滿足了Command模式的,那麼這樣設計有什麼好處呢?大家不知道的最好自己去google,這裡我自己不精通設計模式,所以我只能實際情況實際分析,看下網上描述的這個設計模式的優點在我們的monkey中是否有獲得:
  •   (1)命令模式使新的命令很容易地被加入到系統裡:誠然!如果增加個實現處理吹下螢幕的事件(Command)的話我們只需要增加個類MonkeyBlowEvent,並實現injectEvent介面,然後在裡面呼叫相應的Receiver來注入Blow這個事件就行了
  •   (2)允許接收請求的一方決定是否要否決請求:這點本人沒有領悟好處是什麼,誰清楚的請comment
  •   (3)能較容易地設計一個命令佇列:確實!monkey中就是把所有的事件抽象成MonkeyEvent然後放到我們的EventQueque裡面的
  •   (4)可以容易地實現對請求的撤銷和恢復:這裡沒有用到,因為一個event消費掉後是不能撤銷的。你總不能說你現在點選了個按鈕後悔了,程式會點選後先不執行等待你傳送個undo命令吧。不過如果用在文件編輯的undo功能中應該是挺不錯的
  •   (5)在需要的情況下,可以較容易地將命令記入日誌:也是,每個ConcreteCommand類都是獨立的,所以想把命令記錄下來是很簡單的事情該是我MonkeyKeyEvent的命令總不會變成是你MonkeyTouchEvent的命令嘛

相關推薦

Monkey原始碼分析事件注入

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

Monkey原始碼分析事件

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

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