Android自动化测试框架新书:<<MonnkeyRunner实现原理剖析>>交流

最后更新于:2022-04-01 19:57:08

大家觉得编写一本描述MonkeyRunner原理分析的书籍如何?估计大概10万字左右。内容大概分布如下: 1. **Monkey实现原理**: 去描述运行在目标安卓机器的monkey是如何运行并处理MonkeyRunner发送过来的事件请求并把事件注入到系统的 1. **Monkey命令处理源码情景分析**:去分析关键命令事件如touch,tap等的实现原理 1. **AndroidDebugMonitor(adb)运行原理**: 分析MonkeyRunner是如何使用ddmlib库的AndroidDebugMonitor来跟目标安卓设备进行adb协议通信的 1. **DeviceMonitor运行原理**:分析DeviceMonitor线程是如何监控设备的接入和移除,并如何维护每个安卓设备的基本信息的 1. **MonkeyRunner架构:**分析MonkeyRunner的整体架构,包括如何使用不同的库,如ddmlib,chimpchat,如何和adb服务器通信,如何和monkey通信,如何和viewserver通信 1. **HierarchyViewer和viewserver原理**:分析MonkeyRunner是如何提供面向控件进行脚本代码编写的,以及HierarchyViewer和viewserver的实现原理剖析 1. **EasyMonnkeyDevice实现原理** 1. etc... 真心希望诸公能给出意见,坦诚布公的评论下,内容不限,可以指教是否有市场,也可以提出想要的内容等等...这里先叩谢诸公了!
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyRunner源码分析之工作原理图

最后更新于:2022-04-01 19:57:06

花了点时间整理了下MonkeyRunner的工作原理图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755cf0f02.jpg)
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

monkey源码分析之事件注入方法变化

最后更新于:2022-04-01 19:57:04

在上一篇文章《[Monkey源码分析之事件注入](http://blog.csdn.net/zhubaitian/article/details/40450009)》中,我们看到了monkey在注入事件的时候用到了《[Monkey源码分析番外篇之Android注入事件的三种方法比较](http://blog.csdn.net/zhubaitian/article/details/40430053)》中的第一种方法,通过Internal API的WindowManager的injectKeyEvent之类的方法注入事件。这种方法在android api level 16也就是android4.1.2之后已经发生了变化: - 在此之后注入事件的方式变成了使用InputManager的injectInputEvent方法了 - 而InputManager的getInstance和injectInputEvent等方法后来又变成了隐藏方法,具体哪个版本我没有去查,但起码我现在在看的Android 4.4.2是这样的 - 同样,uiautomator使用的注入事件方法用的也是InputManager的injectInputEvent的方法,这我想就是为什么UIAutomator只支持api level 16以后的android版本了 这里我们看下monkey在最新的版本API Level 19(android 4.4.2)的注入事件代码。 ~~~ /* */ public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) /* */ { /* 101 */ if (verbose > 1) { String note; /* */ String note; /* 103 */ if (this.mAction == 1) { /* 104 */ note = "ACTION_UP"; /* */ } else { /* 106 */ note = "ACTION_DOWN"; /* */ } /* */ try /* */ { /* 110 */ System.out.println(":Sending Key (" + note + "): " + this.mKeyCode + " // " + MonkeySourceRandom.getKeyName(this.mKeyCode)); /* */ } /* */ catch (ArrayIndexOutOfBoundsException e) /* */ { /* 114 */ System.out.println(":Sending Key (" + note + "): " + this.mKeyCode + " // Unknown key event"); /* */ } /* */ } /* */ /* */ /* 119 */ KeyEvent keyEvent = this.mKeyEvent; /* 120 */ if (keyEvent == null) { /* 121 */ long eventTime = this.mEventTime; /* 122 */ if (eventTime <= 0L) { /* 123 */ eventTime = SystemClock.uptimeMillis(); /* */ } /* 125 */ long downTime = this.mDownTime; /* 126 */ if (downTime <= 0L) { /* 127 */ downTime = eventTime; /* */ } /* 129 */ keyEvent = new KeyEvent(downTime, eventTime, this.mAction, this.mKeyCode, this.mRepeatCount, this.mMetaState, this.mDeviceId, this.mScanCode, 8, 257); /* */ } /* */ /* */ /* 133 */ if (!InputManager.getInstance().injectInputEvent(keyEvent, 1)) /* */ { /* 135 */ return 0; /* */ } /* 137 */ return 1; /* */ } /* */ } ~~~ 可以看到最后的注入事件方法从原来的iwm.injectKeyEvent变成了现在的Inputmanager.getInstance().injectInputEvent方法了。
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

Monkey源码分析之事件注入

最后更新于:2022-04-01 19:57:02

本系列的上一篇文章《[Monkey源码分析之事件源](http://blog.csdn.net/zhubaitian/article/details/40422149)》中我们描述了monkey是怎么从事件源取得命令,然后将命令转换成事件放到事件队列里面的,但是到现在位置我们还没有了解monkey里面的事件是怎么一回事,本篇文章就以这个问题作为切入点,尝试去搞清楚monkey的event架构是怎么样的,然后为什么是这样架构的,以及它又是怎么注入事件来触发点击等动作的。 在看这篇文章之前,希望大家最好先去看下另外几篇博文,这样理解起来就会更容易更清晰了: - 《[Monkey源码分析番外篇之Android注入事件的三种方法比较](http://blog.csdn.net/zhubaitian/article/details/40430053)》 - 《[Monkey源码分析番外篇之WindowManager注入事件如何跳出进程间安全限制](http://blog.csdn.net/zhubaitian/article/details/40428097)》 - 《[Android下WindowManager的作用](http://blog.csdn.net/zhubaitian/article/details/39554819)》 ## 1. 事件架构 这里我们先从上一篇文章《[Monkey源码分析之事件源](http://blog.csdn.net/zhubaitian/article/details/40422149)》中来自网上的monkey架构图中截取MonkeyEvent相关的部分来看下MonkeyEvent的架构是怎么样的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755c80a79.jpg) 从上图可以看到,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框架解析](http://blog.csdn.net/zhubaitian/article/details/40424757)》,个人认为写的挺不错的。以下我按照自己的理解简单描述了下 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755c996dc.jpg) - **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类,不清楚的请看本人较早转发的博客<[Android 之 Window、WindowManager 与窗口管理](http://blog.csdn.net/zhubaitian/article/details/39554777)> -  I**WindowManager**:WindowManager主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等,但在android1.6以后隐藏掉了。这里之所以还能调用是因为monkey是在有android源码的情况下编译出来的,如果没有源码的话,那么就需要用到反射机制利用Class.forName来调用获取了。 然后是PackageManager: - **PackageManager**:本类API是对所有基于加载信息的数据结构的封装,包括以下功能: - 安装,卸载应用查询permission相关信息 - 查询Application相关信息(application,activity,receiver,service,provider及相应属性等) - 查询已安装应用 - 增加,删除permission - 清除用户数据、缓存,代码段等 最后是SeriviceManager,具体描述请看《[Android 之 ServiceManager与服务管理](http://blog.csdn.net/zhubaitian/article/details/40425635)》 - **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设计模式是怎么样得呢?我们先看下下图。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755cb790c.jpg) 那么我们对号入座,看下MonkeyEvent得设计是否满足该command模式的要求: - **Command**:MonkeyEvent,声明了injectEvent这个execute接口方法 - **ConcreteCommand**:  各个MonkeyEvent实现类:MonkeyKeyEvent,MonkeyTouchEvent,MonkeyWaitEvent... - **Client**:  Monkey,记得它在runMonkeyCyles方法中调用了mEventSource.getNextEvent()方法来从事件源获取事件,并根据各个事件源的translateCommand方法来创建对应事件(ConcretCommand)吧?不记得的话请先看《[Monkey源码分析之运行流程](http://blog.csdn.net/zhubaitian/article/details/40395327)》和《[Monkey源码分析之事件源](http://blog.csdn.net/zhubaitian/article/details/40422149)》 - **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的命令嘛
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

Monkey源码分析番外篇之Android注入事件的三种方法比较

最后更新于:2022-04-01 19:56:59

原文:http://www.pocketmagic.net/2012/04/injecting-events-programatically-on-android/#.VEoIoIuUcaV 往下分析monkey事件注入源码之前先了解下在android系统下事件注入的方式,翻译一篇国外文章如下。 ## Method 1: Using internal APIs 方法1:使用内部APIs This approach has its risks, like it is always with internal, unpublished APIs. 该方法和其他所有内部没有向外正式公布的APIs一样存在它自己的风险。 The idea is to get an instance of WindowManager in order to access the injectKeyEvent / injectPointerEvent methods. 原理是通过获得WindowManager的一个实例来访问injectKeyEvent/injectPointerEvent这两个事件注入方法。 ~~~ IBinder wmbinder = ServiceManager.getService( "window" ); IWindowManager m_WndManager = IWindowManager.Stub.asInterface( wmbinder ); ~~~ The ServiceManager and WindowsManager are defined as Stubs. We can then bind to these services and call the methods we need.  ServiceManager和Windowsmanager被定义为存根Stubs类。我们根据我们的需要绑定上这些服务并访问里面的方法。 To send a key do the following: 通过以下方式发送一个事件: ~~~ // key down m_WndManager.injectKeyEvent( new KeyEvent( KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A ),true ); // key up m_WndManager.injectKeyEvent( new KeyEvent( KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A ),true ); ~~~ To send touch/mouse events use: 发送touch/mouse事件: ~~~ //pozx goes from 0 to SCREEN WIDTH , pozy goes from 0 to SCREEN HEIGHT m_WndManager.injectPointerEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),MotionEvent.ACTION_DOWN,pozx, pozy, 0), true); m_WndManager.injectPointerEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),MotionEvent.ACTION_UP,pozx, pozy, 0), true); ~~~ This works fine, but only inside your application 这种方法能在你的应用中很好的工作,但,也仅仅只能在你的应用中而已 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755c14f98.jpg) The moment you're trying to inject keys/touch events to any other window, you'll get a force close because of the following exception: 一旦你想要往其他窗口注入keys/touch事件,你将会得到一个强制关闭的消息: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755c305a2.jpg) ~~~ E/AndroidRuntime(4908): java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission ~~~ Not much joy, as INJECT_EVENTS is a system permission. A possible solution is discussed [here](http://stackoverflow.com/questions/3598662/how-to-compile-android-application-with-system-permissions) and [here](http://stackoverflow.com/questions/5383401/android-inject-events-permission). 苦逼了吧,毕竟INJECT_EVENTS是需要系统权限的,一些可能解决的方案在[这里](http://stackoverflow.com/questions/3598662/how-to-compile-android-application-with-system-permissions)和[这里](http://stackoverflow.com/questions/5383401/android-inject-events-permission)有讨论到。 (译者注:请查看本人上一篇翻译的《[Monkey源码分析番外篇之WindowManager注入事件如何跳出进程间安全限制](http://blog.csdn.net/zhubaitian/article/details/40428097)》里面有更详细针对这个问题的描述) ## Method 2: Using an instrumentation object **方法2: 使用instrumentation对象** This is a clean solution based on public API, but unfortunately it still requires that INJECT_EVENTS permission. 相对以上的隐藏接口和方法,这个是比较干净(上面的是隐藏的,故需要用到android不干净不推荐的方法去获取)的方式,但不幸的事它依然有上面的JINECT_EVENTS这个只有系统应用(基本上就是android自己提供的,如monkey)才被允许的权限问题。 ~~~ Instrumentation m_Instrumentation = new Instrumentation(); m_Instrumentation.sendKeyDownUpSync( KeyEvent.KEYCODE_B ); ~~~ For touch events you can use: 以下是触摸事件实例: ~~~ //pozx goes from 0 to SCREEN WIDTH , pozy goes from 0 to SCREEN HEIGHT m_Instrumentation.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),MotionEvent.ACTION_DOWN,pozx, pozy, 0); m_Instrumentation.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),MotionEvent.ACTION_UP,pozx, pozy, 0); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755c4767d.jpg) All good inside the test application, and will crash instantly when trying to inject keys to outside apps, not because the approach doesn't work, but because Android Developers have chosen so. Thanks guys, you rock! Not. 在应用内操作的话完全没有问题,但一旦跳出这个应用去触发按键事件的话就会崩溃。不是因为这个方法不工作,而是因为android开发人员做了限制。谢谢你们,android的开发者们,你牛逼!个屁。 By looking at sendPointerSync's code, you will quickly see it uses the same approach as presented in method 1). So this is the same thing, but packed nicely in a easy to use API: 通过分析sendPointerSync的对应代码,可以看到其实instrumentation使用到的注入事件方式其实和方法一提到的通过WindowManager.injectPointerEvents是一样的,所以穿的都是同一条内裤,只是Robotium出来走动的时候套上条时尚喇叭裤,而以上直接调用WindowManager的方式就犹如只穿一条内裤出街的区别而已。 ~~~ public void sendPointerSync(MotionEvent event) { validateNotAppThread(); try { (IWindowManager.Stub.asInterface(ServiceManager.getService("window"))) .injectPointerEvent(event, true); } catch (RemoteException e) { } } ~~~ ## Method 3: Direct event injection to /dev/input/eventX **方法3:直接注入事件到设备/dev/input/eventX** Linux exposes a uniform input event interface for each device as /dev/input/eventX where X is an integer. We can use it directly and skip the above Android Platform permission issues. linux以系统设备的方式向用户暴露了一套统一的事件注入接口/dev/input/eventX(其中X代表一个整数)。我们可以直接跳用而跳过以上的平台(android这个机遇linux的平台)限制问题。 For this to work, we will need root access, so this approach only works on a rooted device. 但是这需要工作的话,你需要rooted过的设备。 By default the eventX files have the permission set for 660 (read and write for Owner and Group only). To inject keys from our application, we need to make it writable. So do this first: 设备文件eventX默认是被设置为660这个权限的(Owner和同组成员有读写,而owner是root)。为了向这个设备注入事件,你必须让它能可写。所以请先做以下动作: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755c605bc.jpg) ~~~ adb shell su chmod 666 /dev/input/event3 ~~~ You will need root to run the chmod command. 你将需要root权限来运行chmod命令。
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

Monkey源码分析番外篇之WindowManager注入事件如何跳出进程间安全限制

最后更新于:2022-04-01 19:56:57

在分析monkey源码的时候有些背景知识没有搞清楚,比如在看到monkey是使用windowmanager的injectKeyEvent方法注入事件的时候,心里就打了个疙瘩,这种方式不是只能在当前应用中注入事件吗?Google了下发现了国外一个大牛有留下蛛丝马迹描述这个问题,特意摘录下来并做相应部分的翻译,其他部分大家喜欢就看下,我就不翻译了。 ## How it works Behind the scenes, Monkey uses several private interfaces to communicate with three essential system services: 1. Package Manager: Monkey uses the package manager to get a list of Activities for a given Intent. This enables Monkey to randomly switch between Activities while testing an application. 1. Activity Manager: Monkey calls the very powerful setActivityController function on the Activity Manager. This effectively gives Monkey complete control over the activity life-cycle for the duration of the test. 1. Window Manager: Monkey calls a series of functions on the Window Manager to inject events into the application. This enables Monkey to simulate touches and key-presses. Because Monkey communicates at this level, there is no obvious difference between events which have arrived from Monkey and events which have arrived from an actual user. In fact, the distinction is so seamless that it is sometimes necessary to manually check who is in control — hence the famous isUserAMonkey() method in the Android Window Manager: Monkey通过调用WindowManager的一系列方法来注入事件到应用中。这样monkey可以模拟触摸和按键等用户行为。正是因为monkey是在这个层面和应用交互的,所以你的应用接收到的事件哪个是来自真实用户,哪个是来自monkey模拟的已经没有很明显的界限了。事实上正是因为这种近似无缝的区别,我们有时不得不去判断究竟是谁在控制着我们的设备了--这就是为什么android系统提供的isUserAMonkey()方法变得这么流行的原因了。 Monkey sends random events to any application you choose. In order to ensure that this doesn’t cause a security hole, Android uses several techniques to ensure that only monkey can send events, and only when the phone’s user is asking it to. Monkey随机的往不同的的app发送随机事件。为了防止这种行为导致android自家的安全漏洞出来,android使用了几个技术来保证只有monkey可以,且在改手机设备用户允许的情况下才可以,往不同的app发送事件。 Firstly, Monkey itself can only be run by root, or by someone in the “shell” Unix group. Normally, only “adb shell” runs as the “shell group”. This means that the only way to run monkey is to do so through “adb shell”. 首先,monkey本身只能一是被root运行,二是被属于shell这个组的成员运行。而正常来说,只有”adb shell“是在shell这个组下运行的。这就意味着运行monkey的唯一方法就是通过‘adb shell’了。 Secondly, the Monkey application, which is mostly written in Java, asks for two special [manifest permissions](http://developer.android.com/reference/android/Manifest.permission.html). The first, SET_ACTIVITY_WATCHER, allows Monkey to take control of the activity life-cycle. The second, INJECT_EVENTS, allows Monkey to simulate touches and key presses. Importantly, no normal Android application can request these permissions — they are only granted to applications supplied with the Android system. So there is little danger of a rogue APK taking control of an Android device using Monkey. 其次,monkey这个android自身提供的应用,大部分是用android的native语言java来编写的,它会向系统请求两个特背的manifest权限。第一个就是SET_ACTIVITY_WATCHER这个权限,它允许monkey对activity的生命周期进行全权控制。第二个就是INJECT_EVENTS这个权限它允许monkey去模拟触摸和按键事件。重要的是,正常的安卓app是不能请求到这些权限的--只有android系统同意的应用才会得到允许获得这些权限(译者注:其实就是需要android系统的AOSP系统签名。monkey是android自己维护编写的工具,当然是允许了) 以下是本人摘录的INJECT_EVENTS这个manifest选项的官方解析: **INJECT_EVENTS**:Allows an application to inject user events (keys, touch, trackball) into the event stream and deliver them to ANY window. Monkey events What is an event? In Android, events are sent in  response to user input, or due to system events, such as power management. Monkey supports quite a few event types, but only three of them are of interest for automated testing: - KeyEvent: these events are sent by the window manager in response to hardware button presses, and also presses on the keyboard — whether hardware, or on-screen. - MotionEvent: sent by the window manager in response to presses on the touchscreen. - FlipEvent: sent when the user flips out the hardware keyboard on the HTC Dream. On that device, this would imply an orientation change. Unfortunately, Monkey does not simulate orientation changes on other devices.
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

Monkey源码分析之事件源

最后更新于:2022-04-01 19:56:55

上一篇文章《[Monkey源码分析之运行流程](http://blog.csdn.net/zhubaitian/article/details/40395327)》给出了monkey运行的整个流程,让我们有一个概貌,那么往后的文章我们会尝试进一步的阐述相关的一些知识点。 这里先把整个monkey类的结构图给出来供大家参考,该图源自网上(我自己的backbook pro上没有安装OmniGraffle工具,55美金,不舍得,所以直接贴网上的) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755bda8a1.jpg) 图中有几点需要注意下的: - 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 queuedEvents = new LinkedList(); 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 command, CommandQueue queue); } ~~~ 它只定义了一个实现类需要实现的方法translateCommand,从它的描述和接受的的参数可以知道,这个方法要做的事情就是把从事件源接受到的字符串命令转换成上面说的CommandQueue类型维护的那个eventQueues。以monkeyrunner发过来的press这个命令为例子,传过来给monkey的字串是"press KEY_COKDE"(请查看《[MonkeyRunner源码分析之与Android设备通讯方式](http://blog.csdn.net/zhubaitian/article/details/40295559)》) 针对每一个命令都会有一个对应的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 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 COMMAND_MAP = new HashMap(); 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源码分析之启动](http://blog.csdn.net/zhubaitian/article/details/40343759)》描述的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 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源码分析之运行流程](http://blog.csdn.net/zhubaitian/article/details/40395327)》把整个获取事件源然后执行该事件的过程整理下: - 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,看它是如何执行这些事件的。
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

Monkey源码分析之运行流程

最后更新于:2022-04-01 19:56:52

在《[MonkeyRunner源码分析之与Android设备通讯方式](http://blog.csdn.net/zhubaitian/article/details/40295559)》中,我们谈及到MonkeyRunner控制目标android设备有多种方法,其中之一就是在目标机器启动一个monkey服务来监听指定的一个端口,然后monkeyrunner再连接上这个端口来发送命令,驱动monkey去完成相应的工作。 当时我们只分析了monkeyrunner这个客户端的代码是怎么实现这一点的,但没有谈monkey那边是如何接受命令,接受到命令又是如何处理的。 所以自己打开源码看了一个晚上,大概有了概念。但今天网上搜索了下,发现已经有网友“chenjie”对monkey的源码做过相应的分析了,而且文章写得非常有概括性,应该是高手所为,果断花了2个积分下载下来,不敢独享,本想贴上来分享给大家,但发现pdf的文档直接拷贝上来会丢失掉图片,所以只好贴上下载地址:[http://download.csdn.net/download/zqilu/6884491](http://download.csdn.net/download/zqilu/6884491) 但文章主要是架构性得去描述monkey是怎么工作的,按照我自己的习惯,我还是喜欢按照自己的思维和有目的性的去了解我想要的,在这里我想要的是搞清楚monkey是如何处理monkeyrunner过来的命令的。 本文我们就先看下monkey的运行流程。 ## 1. 运行环境设置 和monkeyrunner一样,monkey这个命令也是一个shell脚本,它是在我们的目标android设备的“/system/bin/monkey”,其实这是一个android上面java程序启动的标准流程。 ~~~ base=/system export CLASSPATH=$base/framework/monkey.jar trap "" HUP exec app_process $base/bin com.android.commands.monkey.Monkey $* ~~~ android中可以通过多种方式启动java应用,通过app_process命令启动就是其中一种,它可以帮忙注册android JNI,而绕过dalvik以使用Native API(如我般不清楚的请百度)所做的主要事情如下: - 设置monkey的CLASSPATH环境变量指向monkey.jar - 通过app_process指定monkey的入口和传进来的所有参数启动上面CLASSPATH设定的monkey.jar ## 2.命令行参数解析 通过以上的app_process指定的monkey入口,我们可以知道我们的入口函数main是在com.android.commands.Monkey这个类里面的: ~~~ /** * Command-line entry point. * * @param args The command-line arguments */ public static void main(String[] args) { // Set the process name showing in "ps" or "top" Process.setArgV0("com.android.commands.monkey"); int resultCode = (new Monkey()).run(args); System.exit(resultCode); } ~~~ 入口函数很简单,直接跳到run这个方法,这是一个很重要的方法,里面大概会做以下这些事情: - 处理命令行参数 - 根据命令行参数启动不同的事件源,也就是我们的测试事件究竟是从网络如monkeyrunner过来的还是monkey内部的random测试数据集过来的还是脚本过来的如此之类 - 跳入runMonkeyCyncle方法针对不同的事件源开始获取并执行不同的事件 这个方法会比较长,我们只看我们现在想要的关键片段,可以看到里面调用了命令行处理函数processOptions. ~~~ private int run(String[] args) { ... if (!processOptions()) { return -1; } ... } ~~~ 进去之后就是很普通的读取命令行的参数然后一个个进行解析保存了,没有太多特别的东西,这里就直接贴出monkey的参数选项大家看看就好了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755ba9b92.jpg) ## 3. 初始化测试事件源 如前所述,run方法里面在获得命令行参数后会进入下一个环节,就是根据不同的参数去初始化不同的事件源 ~~~ private int run(String[] args) { ... if (mScriptFileNames != null && mScriptFileNames.size() == 1) { // script mode, ignore other options mEventSource = new MonkeySourceScript(mRandom, mScriptFileNames.get(0), mThrottle, mRandomizeThrottle, mProfileWaitTime, mDeviceSleepTime); mEventSource.setVerbose(mVerbose); mCountEvents = false; } else if (mScriptFileNames != null && mScriptFileNames.size() > 1) { if (mSetupFileName != null) { mEventSource = new MonkeySourceRandomScript(mSetupFileName, mScriptFileNames, mThrottle, mRandomizeThrottle, mRandom, mProfileWaitTime, mDeviceSleepTime, mRandomizeScript); mCount++; } else { mEventSource = new MonkeySourceRandomScript(mScriptFileNames, mThrottle, mRandomizeThrottle, mRandom, mProfileWaitTime, mDeviceSleepTime, mRandomizeScript); } mEventSource.setVerbose(mVerbose); mCountEvents = false; } else if (mServerPort != -1) { try { mEventSource = new MonkeySourceNetwork(mServerPort); } catch (IOException e) { System.out.println("Error binding to network socket."); return -5; } mCount = Integer.MAX_VALUE; } else { // random source by default if (mVerbose >= 2) { // check seeding performance System.out.println("// Seeded: " + mSeed); } mEventSource = new MonkeySourceRandom(mRandom, mMainApps, mThrottle, mRandomizeThrottle); mEventSource.setVerbose(mVerbose); // set any of the factors that has been set for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) { if (mFactors[i] <= 0.0f) { ((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]); } } // in random mode, we start with a random activity ((MonkeySourceRandom) mEventSource).generateActivity(); } ... mNetworkMonitor.start(); int crashedAtCycle = runMonkeyCycles(); mNetworkMonitor.stop(); ... } ~~~ 事件源代表测试数据的事件是从哪里过来的,不同的event source会有不同的类来做相应的实现: - **MonkeySourceNetwork.java**: 事件是从网络如monkeyrunner过来的,处理的是《[MonkeyRunner源码分析之与Android设备通讯方式](http://blog.csdn.net/zhubaitian/article/details/40295559)》描述的界面控制操作事件 - **MonkeySourceNetworkVars.java:**事件也是从网络如monkeyrunner过来的,处理的是《[MonkeyRunner源码分析之与Android设备通讯方式](http://blog.csdn.net/zhubaitian/article/details/40295559)》提到的getPropery事件 - **MonkeySourceNetworkViews.java**:事件也是从网络如monkeyrunner过来的,处理的是《[MonkeyRunner源码分析之与Android设备通讯方式](http://blog.csdn.net/zhubaitian/article/details/40295559)》提到的Views相关的事件 - **MonkeySourceRandom.java**:事件是从monkey内部生成的随机事件集,也就是我们通过命令行启动monkey测试目标app的常用方式 - **MonkeySourceRanodomeScript.java**: 上面的随机内部数据源也可以通过指定setup脚本来创建 - **MonkeySourceScript.java**: 用户也可以遵循一定的规则编写monkey脚本来驱动monkey进行相关测试,与上面不同的是它不再是随机的 往后的文章我们会针对其中一个事件源进行分析,在这里我们只需要知道这些事件源代表了事件的不同的来源,它会 - 从指定的源获取命令 - 把命令翻译成monkey事件然后放到命令队列EventQueue 这里需要注意的是每一个EventSource类都是实现了MonkeyEventSource这个接口的,这个接口最重要的就是要求实现类必须实现getNextEvent这个方法来生成并获取事件。这样子做的好处就是屏蔽了每一个具体事件源的实现细节,其他地方的代码只需要调用这个接口的getNextEvent方法获得事件源就行了,而无需关心这些事件是从哪个源过来的。这些都是面向对象的面向接口编程的基础了,大家有不清楚的最好先去了解下java的一些基本知识,这样理解起来会快很多。 ## 4. 循环执行事件 run方法根据参数从不同的事件源获得事件并放入到EventQueue后,就会开始执行一个循环去从EventQueue里获取事件进行执行 ~~~ private int run(String[] args) { ... int crashedAtCycle = runMonkeyCycles(); ... } ~~~ 如前所述,runMonkeyCyles方法会根据不同的数据源开始一条条的获取事件并进行执行: ~~~ /** * Run mCount cycles and see if we hit any crashers. *

* TODO: Meta state on keys * * @return Returns the last cycle which executed. If the value == mCount, no * errors detected. */ private int runMonkeyCycles() { int eventCounter = 0; int cycleCounter = 0; boolean shouldReportAnrTraces = false; boolean shouldReportDumpsysMemInfo = false; boolean shouldAbort = false; boolean systemCrashed = false; // TO DO : The count should apply to each of the script file. while (!systemCrashed && cycleCounter < mCount) { ... MonkeyEvent ev = mEventSource.getNextEvent(); if (ev != null) { int injectCode = ev.injectEvent(mWm, mAm, mVerbose); ... } ... } .... } ~~~ 注意这里的mEventSource就是我们上面提到的事件源的接口,它屏蔽了每个事件实现类的具体细节,我们只需要告诉这个接口我们现在需要取一条事件然后执行它,该结构根据面向对象的多态原理,就会自动取事件的实现类获得对应的事件进行返回。 所以这里大家还需要对多态这个概念有所了解,特别是一些从手动测试转到自动化测试的朋友,可能之前没有接触过太多面向对象的知识。本人以前做过开发,所以还ok。这里只是做一个善意的提醒。 获得事件后下一步就是去执行相应的事件了,不同的事件会有不同的处理方式,或只是执行个命令,或调用WindowManager隐藏接口做事件注入等,这些都会在今后文章进行进一步阐述 这一篇文章就到此为止了,目的就是让大家对整一个monkey执行的流程有个初步的了解,方便理解往下的相关文章。

 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyRunner源码分析之启动

最后更新于:2022-04-01 19:56:50

在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的。但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解。 从几个月前开始接触ios和android的自动化测试,原来是本着仅仅为了提高测试团队工作效率的心态先行作浅尝即止式的研究,然后交给测试团队去边实现边自己研究,最后因为各种原因果然是浅尝然后就止步了,而自己最终也离开了上一家公司。换了工作这段时间抛开所有杂念和以前的困扰专心去学习研究各个框架的使用,逐渐发现这还是很有意思的事情,期间也会使得你没有太多的时间去胡思乱想,所以说,爱好还真的是需要培养的。换工作已经有大半个月时间了,但算来除去国庆和跑去香港参加电子展的时间,真正上班的时间可能两个星期都不到,但自己在下班和假日期间还是继续花时间去学习研究这些东西,这让我觉得有那么一点像以前还在学校的时候研究minix操作系统源码的那个劲头,这可能应了我的兄弟Red.Lin所说的我的骨子里还是挺喜欢去作研究的。 所以这也就催生了我打算把MonkeyRunner,Robotium,Uiautomator,Appium以及今后会接触到的iOS相关的自动化测试框架的原理好好研究一下的想法。了解一个事物的工作原理是什么往往我们需要去深入到事物的内部看它是怎么构成的。对于我们这些框架来说,它的内部也就是它的源代码的。 其实上几天我已经开始尝试对MonkeyRunner的源码进行过一些分析了,有兴趣的同学可以去看下本人以下的两篇文章: - 《[MonkeyRunner和Android设备通讯方式源码分析](http://blog.csdn.net/zhubaitian/article/details/40295559)》 - 《[谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析](http://blog.csdn.net/zhubaitian/article/details/40262831)》 好,就不废话了,我们今天就来看看MonkeyRunner是怎么启动起来以及启动过程它究竟做了什么事情。但敬请注意的一点是,大家写过博客的都应该知道,写一篇文章其实是挺耗时间的,所以我今后的分析都会尝试在一篇文章中不会把代码跟踪的太深,对涉及到的重要但不影响对文章主旨理解的会考虑另行开篇描述。 ## 1. MonkeyRunner 运行环境初始化 这里我们首先应该去看的不是MonkeyRunnerStarter这个类里面的main这个入口函数,因为monkeyrunner其实是个shell脚本,它就在你的sdk/tools下面,这个shell脚本会先初始化一些变量,然后调用最后面也是最关键的一个命令: ~~~ exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir:$swtpath" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@" ~~~ 这个命令很明显就是通过java来执行一个指定的jar包,究竟是哪个jar包呢?我们往下会描述,但在此之前我们先看下这个命令的‘-D‘参数是怎么回事。我们如果对java不是很熟悉的话可以在命令行执行'java -h'来查看帮助: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755b1308c.jpg) '-D'参数是通过指定一个键值对来设置系统属性,而这个系统属性是保存在JVM里面的,最终我们可以通过如以下的示例代码调用取得对应的一个键的值: ~~~ /* */ private String findAdb() /* */ { /* 74 */ String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir"); /* */ ~~~ 这里我们把这些变量都打印出来,看下都设置了哪些值以及启动的是哪个jar包: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755b85cdd.jpg) 我们可以看到monkeyrunner这个shell脚本其实最终就是通过java执行启动了sdk里面的哪个monkeyrunner.jar这个jar包。除此之外还设置了如图的几个系统属性,这里请注意'com.android.monkeyrunner.bindir'这个属性,我们今天的分析会碰到,它指定的就是monkeyrunner这个可执行shell 脚本在sdk中的绝对位置。 这里还要注意参数'$@',它的内容是要传送给monkeyrunner的参数,可以从它的help去了解每个选项是什么意思: ~~~ Usage: monkeyrunner [options] SCRIPT_FILE -s MonkeyServer IP Address. -p MonkeyServer TCP Port. -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF) ~~~ 迄今我们就了解了启动monkeyrunn这个shell脚本所作的事情就是涉及了以上几个系统属性然后通过用户指定的相应参数来用java执行sdk里面的monkerunner.jar这个jar包,往下我们就需要去查看monkeyrunner的入口函数main了。 ## 2.命令行显式和隐藏参数处理 我们先看下MonkeyRunner的入口函数,它是在MonkeyRunnerStart这个类里面的: ~~~ /* */ public static void main(String[] args) { /* 179 */ MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(args); /* */ /* 181 */ if (options == null) { /* 182 */ return; /* */ } /* */ /* */ /* 186 */ replaceAllLogFormatters(MonkeyFormatter.DEFAULT_INSTANCE, options.getLogLevel()); /* */ /* 188 */ MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options); /* 189 */ int error = runner.run(); /* */ /* */ /* 192 */ System.exit(error); /* */ } /* */ } ~~~ 这里主要做了三件事情: - 179行去处理用户启动monkeyrunner的时候输入的命令行参数 - 188行去初始化MonkeyRunnerStarter,里面主要是初始化了ChimpChat,ChimpChat又去开启AndroidDebugBridge进程和开启DeviceMonitor设备监控线程 - 189行去把monkeyrunner运行起来,包括带脚本参数的情况和不待脚本参数直接提供jython命令行的情况 我们这一章节会先去分析下monkeyrunner是如何对参数进行处理的,我们跳转到MonkeyRunnerOptions这个类里面的processOptions这个方法里面: ~~~ /* */ public static MonkeyRunnerOptions processOptions(String[] args) /* */ { /* 95 */ int index = 0; /* */ /* 97 */ String hostname = DEFAULT_MONKEY_SERVER_ADDRESS; /* 98 */ File scriptFile = null; /* 99 */ int port = DEFAULT_MONKEY_PORT; /* 100 */ String backend = "adb"; /* 101 */ Level logLevel = Level.SEVERE; /* */ /* 103 */ ImmutableList.Builder pluginListBuilder = ImmutableList.builder(); /* 104 */ ImmutableList.Builder argumentBuilder = ImmutableList.builder(); /* 105 */ while (index < args.length) { /* 106 */ String argument = args[(index++)]; /* */ /* 108 */ if ("-s".equals(argument)) { /* 109 */ if (index == args.length) { /* 110 */ printUsage("Missing Server after -s"); /* 111 */ return null; /* */ } /* 113 */ hostname = args[(index++)]; /* */ } /* 115 */ else if ("-p".equals(argument)) /* */ { /* 117 */ if (index == args.length) { /* 118 */ printUsage("Missing Server port after -p"); /* 119 */ return null; /* */ } /* 121 */ port = Integer.parseInt(args[(index++)]); /* */ } /* 123 */ else if ("-v".equals(argument)) /* */ { /* 125 */ if (index == args.length) { /* 126 */ printUsage("Missing Log Level after -v"); /* 127 */ return null; /* */ } /* */ /* 130 */ logLevel = Level.parse(args[(index++)]); /* 131 */ } else if ("-be".equals(argument)) /* */ { /* 133 */ if (index == args.length) { /* 134 */ printUsage("Missing backend name after -be"); /* 135 */ return null; /* */ } /* 137 */ backend = args[(index++)]; /* 138 */ } else if ("-plugin".equals(argument)) /* */ { /* 140 */ if (index == args.length) { /* 141 */ printUsage("Missing plugin path after -plugin"); /* 142 */ return null; /* */ } /* 144 */ File plugin = new File(args[(index++)]); /* 145 */ if (!plugin.exists()) { /* 146 */ printUsage("Plugin file doesn't exist"); /* 147 */ return null; /* */ } /* */ /* 150 */ if (!plugin.canRead()) { /* 151 */ printUsage("Can't read plugin file"); /* 152 */ return null; /* */ } /* */ /* 155 */ pluginListBuilder.add(plugin); /* 156 */ } else if (!"-u".equals(argument)) /* */ { /* 158 */ if ((argument.startsWith("-")) && (scriptFile == null)) /* */ { /* */ /* */ /* 162 */ printUsage("Unrecognized argument: " + argument + "."); /* 163 */ return null; /* */ } /* 165 */ if (scriptFile == null) /* */ { /* */ /* 168 */ scriptFile = new File(argument); /* 169 */ if (!scriptFile.exists()) { /* 170 */ printUsage("Can't open specified script file"); /* 171 */ return null; /* */ } /* 173 */ if (!scriptFile.canRead()) { /* 174 */ printUsage("Can't open specified script file"); /* 175 */ return null; /* */ } /* */ } else { /* 178 */ argumentBuilder.add(argument); /* */ } /* */ } /* */ } /* */ /* 183 */ return new MonkeyRunnerOptions(hostname, port, scriptFile, backend, logLevel, pluginListBuilder.build(), argumentBuilder.build()); /* */ } /* */ } ~~~ 这里首先请看97-101行的几个变量初始化,如果用户在命令行中没有指定对应的参数,那么这些默认参数就会被使用,我们且看下这些默认值分别是什么: - **hostname**:对应‘-s'参数,默认值是'127.0.0.1',也就是本机,将会forward给目标设备运行的monkey,所以加上下面的转发port等同于目标机器在listen的monkey服务 - **port:**对应‘-p'参数,默认值是'12345' - **backend:**对应'-be'参数,默认值是‘adb‘,其实往后看代码我们会发现它也只是支持’adb‘而已。这里需要注意的是这是一个隐藏参数,命令行的help没有显示该参数 - **logLevel:**对应‘-v'参数,默认值是'SEVERE',也就是说只打印严重的log 代码往下就是对用户输入的参数的解析并保存了,这里要注意几个隐藏的参数: - -u:咋一看以为这是一个什么特别的参数,从156-178行可以看到这个参数处理的意义是:当用户输入'-u'的时候不会作任何处理,但当用户输入的是由‘-’开始的但又不是monkeyrunner声称支持的那几个参数的时候,就会根据不同的情况给用户报错。所以这段代码的意思其实就是在用户输入了不支持的参数的时候根据不同的情况给用户提示而已。 - -be:backend,如前所述,只支持‘adb' - -plugin:这里需要一个背景知识,在google官网又说明,用户可以通过遵循一定的规范去编写插件来扩展monkeyrunner的功能,比如在monkeydevice里面按下这个动作是需要通过MonkeyDevice.DOWN这个参数来传给press这个方法的,如果你觉得这样子不好,你希望增加个pressDown这样的方法,里面默认就是用MonkeyDevice.DOWN来驱动MonkeyDevice的press方法,而用户只需要给出坐标点就可以了,那么你就可以遵循google描述的规范去编写一个这方面的插件,到时使用的时候就可以通过python方式直接import进来使用了。往后有机会的话会尝试另开一篇文章编写一个例子放上来大家共同学习下插件应该怎么编写,这里如文章开始所述,就不深究下去了,只需要知道插件这个概念就足够了 ## 3. 开启ChimpChat之启动AndroidDebugBridge和DeviceMonitor 处理好命令行参数之后,monkeyrunner入口函数的下一步就是去尝试根据这些参数来调用MonkeyRunnerStarter的构造函数: ~~~ /* 188 */ MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options); ~~~ 我们进入到该构造函数看下它究竟做了什么事情: ~~~ /* */ public MonkeyRunnerStarter(MonkeyRunnerOptions options) /* */ { /* 57 */ Map chimp_options = new TreeMap(); /* 58 */ chimp_options.put("backend", options.getBackendName()); /* 59 */ this.options = options; /* 60 */ this.chimp = ChimpChat.getInstance(chimp_options); /* 61 */ MonkeyRunner.setChimpChat(this.chimp); /* */ } ~~~ 仅从这个方法的几行代码我们可以看到它其实做的事情就是去根据‘backend’来初始化ChimpChat ,然后用组合(这里要大家有面向对象的聚合和耦合的概念)的方式的方式把该ChimpChat对象保留到MonkeyRunner的静态成员变量里面,为什么说它一定是静态成员变量呢?因为第61行保存该实例调用的是MonkeyRunner这个类的方法,而不是一个实例,所以该方法肯定就是静态的,而一个静态方法里面的成员函数也必然是静态的。大家跳进去MonkeyRunner这个类就可以看到: ~~~ /* */ private static ChimpChat chimpchat; /* */ static void setChimpChat(ChimpChat chimp) /* */ { /* 53 */ chimpchat = chimp; /* */ } ~~~ 好,我们返回来继续看ChimpChat是怎么启动的,首先我们看58行的optionsGetBackendName()是怎么获得backend的名字的,从上面命令行参数分析我们可以知道它默认是用‘adb’的,所以它获得的就是‘adb’,或者用户指定的其他backend(其实这种情况不支持,往下继续分析我们就会清楚了). 取得backend的名字之后就会调用60行的ChimpChat.getInstance来对ChimpChat进行实例化: ~~~ /* */ public static ChimpChat getInstance(Map options) /* */ { /* 48 */ sAdbLocation = (String)options.get("adbLocation"); /* 49 */ sNoInitAdb = Boolean.valueOf((String)options.get("noInitAdb")).booleanValue(); /* */ /* 51 */ IChimpBackend backend = createBackendByName((String)options.get("backend")); /* 52 */ if (backend == null) { /* 53 */ return null; /* */ } /* 55 */ ChimpChat chimpchat = new ChimpChat(backend); /* 56 */ return chimpchat; /* */ } ~~~ ChimpChat实例化所做的事情有两点,这就是我们这一章节的重点。 - 根据backend的名字来创建一个backend,其实就是创建一个AndroidDebugBridge - 调用构造函数把这个backend保存到ChimChat的成员变量 往下我们继续看ChimpChat中AndroidDebugBridge这个backend是怎么创建的,我们进入到51行调用的createBackendByName这个函数: ~~~ /* */ private static IChimpBackend createBackendByName(String backendName) /* */ { /* 77 */ if ("adb".equals(backendName)) { /* 78 */ return new AdbBackend(sAdbLocation, sNoInitAdb); /* */ } /* 80 */ return null; /* */ } ~~~ 这里注意第77行,这就是为什么我之前说backend其实只是支持‘adb’而已,起码暂时的代码是这样子,如果今后google决定支持其他更新的backend,就另当别论了。这还是有可能的,毕竟google留了这个接口。 ~~~ /* */ public AdbBackend(String adbLocation, boolean noInitAdb) /* */ { /* 58 */ this.initAdb = (!noInitAdb); /* */ /* */ /* 61 */ if (adbLocation == null) { /* 62 */ adbLocation = findAdb(); /* */ } /* */ /* 65 */ if (this.initAdb) { /* 66 */ AndroidDebugBridge.init(false); /* */ } /* */ /* 69 */ this.bridge = AndroidDebugBridge.createBridge(adbLocation, true); /* */ } ~~~ 创建AndroidDebugBridge之前我们先要确定我们的adb程序的位置,这就是通过61行来实现的,我们进去findAdb去看下它是怎么找到我们的sdk中的adb的: ~~~ /* */ private String findAdb() /* */ { /* 74 */ String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir"); /* */ /* */ /* */ /* */ /* */ /* 80 */ if ((mrParentLocation != null) && (mrParentLocation.length() != 0)) /* */ { /* 82 */ File platformTools = new File(new File(mrParentLocation).getParent(), "platform-tools"); /* */ /* 84 */ if (platformTools.isDirectory()) { /* 85 */ return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB; /* */ } /* */ /* 88 */ return mrParentLocation + File.separator + SdkConstants.FN_ADB; /* */ } /* */ /* 91 */ return SdkConstants.FN_ADB; /* */ } ~~~ 首先它通过查找JVM中的System Property来找到"com.android.monkeyrunner.bindir"这个属性的值,记得第一章节运行环境初始化的时候在monkeyrunner这个shell脚本里面它是怎么通过java的-D参数把该值保存到System Property的吧?其实它就是你的文件系统中保存sdk的monkeyrunner这个bin(shell)文件的路径,在我的机器上是"com.android.monkeyrunner.bindir:/Users/apple/Develop/sdk/tools". 找到这个路径后通过第82行的代码再取得它的父目录,也就是sdk的目录,再加上'platform-tools'这个子目录,然后再通过84或者85这行加上adb这个名字,这里的FN_ADB就是 adb的名字,在windows下会加上个'.exe'变成'adb.exe' ,类linux系统下就只是‘adb’。在本人的机器里面就是"Users/apple/Develop/sdk/platform-tools/adb" 好,找到了adb所在路经后,AdbBackend的构造函数就会根据这个参数去调用AndroidDebugBridge的createBridge这个静态方法,里面重要的是以下代码: ~~~ /* */ try /* */ { /* 325 */ sThis = new AndroidDebugBridge(osLocation); /* 326 */ sThis.start(); /* */ } catch (InvalidParameterException e) { /* 328 */ sThis = null; /* */ } ~~~ 第325行AndroidDebugBridge的构造函数做的事情就是实例化AndroidDebugBridge,去检查一下adb的版本是否满足要求,设置一些成员变量之类的。adb真正启动起来是调用326行的start()这个成员方法: ~~~ /* */ boolean start() /* */ { /* 715 */ if ((this.mAdbOsLocation != null) && (sAdbServerPort != 0) && ((!this.mVersionCheck) || (!startAdb()))) { /* 716 */ return false; /* */ } /* */ /* 719 */ this.mStarted = true; /* */ /* */ /* 722 */ this.mDeviceMonitor = new DeviceMonitor(this); /* 723 */ this.mDeviceMonitor.start(); /* */ /* 725 */ return true; /* */ } ~~~ 这里做了几个很重要的事情: 1. startAdb:开启AndroidDebugBridge 1. New DeviceMonitor并传入已经开启的adb:初始化android设备监控 1. DeviceMonitor.start:启动DeviceMonitor设备监控线程。 我们先看第一个startAdb: ~~~ /* */ synchronized boolean startAdb() /* */ { /* 945 */ if (this.mAdbOsLocation == null) { /* 946 */ Log.e("adb", "Cannot start adb when AndroidDebugBridge is created without the location of adb."); /* */ /* 948 */ return false; /* */ } /* */ /* 951 */ if (sAdbServerPort == 0) { /* 952 */ Log.w("adb", "ADB server port for starting AndroidDebugBridge is not set."); /* 953 */ return false; /* */ } /* */ /* */ /* 957 */ int status = -1; /* */ /* 959 */ String[] command = getAdbLaunchCommand("start-server"); /* 960 */ String commandString = Joiner.on(',').join(command); /* */ try { /* 962 */ Log.d("ddms", String.format("Launching '%1$s' to ensure ADB is running.", new Object[] { commandString })); /* 963 */ ProcessBuilder processBuilder = new ProcessBuilder(command); /* 964 */ if (DdmPreferences.getUseAdbHost()) { /* 965 */ String adbHostValue = DdmPreferences.getAdbHostValue(); /* 966 */ if ((adbHostValue != null) && (!adbHostValue.isEmpty())) /* */ { /* 968 */ Map env = processBuilder.environment(); /* 969 */ env.put("ADBHOST", adbHostValue); /* */ } /* */ } /* 972 */ Process proc = processBuilder.start(); /* */ /* 974 */ ArrayList errorOutput = new ArrayList(); /* 975 */ ArrayList stdOutput = new ArrayList(); /* 976 */ status = grabProcessOutput(proc, errorOutput, stdOutput, false); /* */ } catch (IOException ioe) { /* 978 */ Log.e("ddms", "Unable to run 'adb': " + ioe.getMessage()); /* */ } /* */ catch (InterruptedException ie) { /* 981 */ Log.e("ddms", "Unable to run 'adb': " + ie.getMessage()); /* */ } /* */ /* */ /* 985 */ if (status != 0) { /* 986 */ Log.e("ddms", String.format("'%1$s' failed -- run manually if necessary", new Object[] { commandString })); /* */ /* 988 */ return false; /* */ } /* 990 */ Log.d("ddms", String.format("'%1$s' succeeded", new Object[] { commandString })); /* 991 */ return true; /* */ } ~~~ 这里所做的事情就是 - 准备好启动db server的command字串 - 通过ProcessBuilder启动command字串指定的adb server - 错误处理 command字串通过959行的getAdbLauncherCommand('start-server')来实现: ~~~ /* */ private String[] getAdbLaunchCommand(String option) /* */ { /* 996 */ List command = new ArrayList(4); /* 997 */ command.add(this.mAdbOsLocation); /* 998 */ if (sAdbServerPort != 5037) { /* 999 */ command.add("-P"); /* 1000 */ command.add(Integer.toString(sAdbServerPort)); /* */ } /* 1002 */ command.add(option); /* 1003 */ return (String[])command.toArray(new String[command.size()]); /* */ } ~~~ 整个函数玩的就是字串组合,最后获得的字串就是'adb -P $port start-server',也就是开启adb服务器的命令行字串了,最终把这个字串打散成字串array返回。 获得命令之后下一步就是直接调用java的ProcessBuilder够着函数来创建一个adb服务器进程了。创建好后就可以通过972行的‘processBuilder.start()‘把这个进程启动起来。 迄今为止AndroidDebugBridge启动函数start()所做事情的第一点“1. 启动AndroidDebugBridge"已经完成了,adb服务器进程已经运行起来了。那么我们往下看第二点“2.初始化DeviceMonitor". AndroidDebugBridge启动起来后,下一步就是把这个adb实例传到DeviceMonitor来去监测所有连接到adb服务器也就是pc主机端的android设备的状态: ~~~ /* */ DeviceMonitor(AndroidDebugBridge server) /* */ { /* 72 */ this.mServer = server; /* */ /* 74 */ this.mDebuggerPorts.add(Integer.valueOf(DdmPreferences.getDebugPortBase())); /* */ } ~~~ 然后就是继续AndroidDebugBridge启动函数start()做的第三个事情“3. 启动DeviceMonitor设备监控线程“: ~~~ /* */ void start() /* */ { /* 81 */ new Thread("Device List Monitor") /* */ { /* */ public void run() { /* 84 */ DeviceMonitor.this.deviceMonitorLoop(); /* */ } /* */ }.start(); /* */ } ~~~ 其实DeviceMonitor这个类在本人上一篇文章<<[MonkeyRunner和Android设备通讯方式源码分析](http://blog.csdn.net/zhubaitian/article/details/40295559)>>中已经做过分析,所做的事情就是通过一个无限循环不停的检查android设备的变化,维护一个设备“adb devices -l”列表,并记录下每个设备对应的'adb shell getprop'获得的所有property等信息。这里就不做深入的解析了,大家有兴趣的话可以返回该文章去查看。 ## 4. 启动MonkeyRunner MonkeyRunner入口函数main在开启了AndroidDebugBridge进程和开启了DeviceMonitor设备监控线程之后,下一步要做的是事情就是去把MonkeyRunner真正启动起来: ~~~ /* */ private int run() /* */ { /* 68 */ String monkeyRunnerPath = System.getProperty("com.android.monkeyrunner.bindir") + File.separator + "monkeyrunner"; /* */ /* */ /* 71 */ Map> plugins = handlePlugins(); /* 72 */ if (this.options.getScriptFile() == null) { /* 73 */ ScriptRunner.console(monkeyRunnerPath); /* 74 */ this.chimp.shutdown(); /* 75 */ return 0; /* */ } /* 77 */ int error = ScriptRunner.run(monkeyRunnerPath, this.options.getScriptFile().getAbsolutePath(), this.options.getArguments(), plugins); /* */ /* 79 */ this.chimp.shutdown(); /* 80 */ return error; /* */ } ~~~ 这里又分了两种情况: - 开启一个jython的console:在用户没有指定脚本参数的情况下。直接调用eclipse上Preference设定的jython这个interpreter的console,其实就类似于你直接在命令行打个'python'命令,然后弹出一个console让你可以直接在上面编写代码运行了 - 直接执行脚本:调用我们在eclipse上Preference设定的jython这个interpreter来直接解析运行指定的脚本 至于jython编辑器是怎么实现的,就超出了我们这篇文章的范畴了,本人也没有这样的精力去往里面挖,大家又兴趣的就自己去研究jython的实现原理吧。 这里值得一提的是直接运行脚本时classpath的设置: ~~~ /* */ public static int run(String executablePath, String scriptfilename, Collection args, Map> plugins) /* */ { /* 79 */ File f = new File(scriptfilename); /* */ /* */ /* 82 */ Collection classpath = Lists.newArrayList(new String[] { f.getParent() }); /* 83 */ classpath.addAll(plugins.keySet()); /* */ /* 85 */ String[] argv = new String[args.size() + 1]; /* 86 */ argv[0] = f.getAbsolutePath(); /* 87 */ int x = 1; /* 88 */ for (String arg : args) { /* 89 */ argv[(x++)] = arg; /* */ } /* */ /* 92 */ initPython(executablePath, classpath, argv); /* */ /* 94 */ PythonInterpreter python = new PythonInterpreter(); /* */ /* */ /* 97 */ for (Map.Entry> entry : plugins.entrySet()) { /* */ boolean success; /* */ try { /* 100 */ success = ((Predicate)entry.getValue()).apply(python); /* */ } catch (Exception e) { /* 102 */ LOG.log(Level.SEVERE, "Plugin Main through an exception.", e); } /* 103 */ continue; /* */ /* 105 */ if (!success) { /* 106 */ LOG.severe("Plugin Main returned error for: " + (String)entry.getKey()); /* */ } /* */ } /* */ /* */ /* 111 */ python.set("__name__", "__main__"); /* */ /* 113 */ python.set("__file__", scriptfilename); /* */ try /* */ { /* 116 */ python.execfile(scriptfilename); /* */ } catch (PyException e) { /* 118 */ if (Py.SystemExit.equals(e.type)) /* */ { /* 120 */ return ((Integer)e.value.__tojava__(Integer.class)).intValue(); /* */ } /* */ /* 123 */ LOG.log(Level.SEVERE, "Script terminated due to an exception", e); /* 124 */ return 1; /* */ } /* 126 */ return 0; /* */ } ~~~ 从82,83和92行可以看到MonkeyRunner会默认把以下两个位置加入到classpath里面 - 执行脚本的父目录 - plugins 也就说你编写的python脚本默认就能直接import你的plugins以及在你的脚本同目录下编写的其他模块,但是如果你编写的python模块是在子目录下面或者其他目录,默认import会失败的,这个大家写个简单模块验证下就可以了,本人已经简单验证过。 ## 5. 总结 最后我们对MonkeyRunner启动的过程做一个总结 - monkeyrunner这个shell脚本会先设置一些运行环境的系统属性保存到JVM的System.Propery里面 - 然后该脚本会通过java -jar直接运行sdk下面的monkeyruner.jar - 然后操作系统直接回调到monkeyrunner在MonkeyRunnerStarter里面的入口函数main - 入口函数会先尝试实例化MonkeyRunnerStarter的实例 - 实例化MonkeyRunnerStarter时会去实例化ChimpChat这个类 - 实例化ChimpChat这个类的时候会去创建AndroidDebugBridge对象启动一个adb进程来进行与adb服务器以及目标设备的adb守护进程通讯 - 实例化ChimpChat时还会在上面创建的adb对象的基础上创建DeviceMonitor对象并启动一个线程来监控和维护连接到主机pc的android设备信息,因为监控设备时需要通过adb来实现的 - 最后在以上都准备好后就会尝试启动jython编译器的console或者直接调用jython编译器去解析执行脚本 从中可以看到ChimpChat是一个多么重要的类,因为它同时启动了ddmlib里面的AndroidDebugBridge(adb)和DeviceMonitor,这里也是为什么我之前的文章说ChimpChat其实就是adb的一个wrapper的原因了。
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyRunner源码分析之与Android设备通讯方式

最后更新于:2022-04-01 19:56:48

如前文《[谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析](http://blog.csdn.net/zhubaitian/article/details/40262831)》所述,本文主要会尝试描述android的自动化测试框架MonkeyRunner究竟是如何和目标设备进行通信的。 在上一篇文章中我们其实已经描述了其中一个方法,就是通过adb协议发送adb服务器请求的方式驱动android设备的adbd守护进程去获取FrameBuffer的数据生成屏幕截图。那么MonkeyRunner还会用其他方式和目标设备进行通信吗?答案是肯定的,且看我们一步步分析道来。 ## 1.概述 MonkeyRunner和目标设备打交道都是通过ChimpChat层进行封装分发但最终是在ddmlib进行处理的,其中囊括的方法大体如下: - **发送monkey命令**:MonkeyRunner先通过adb shell发送命令"monkey -port  12345"在目标机器上启动monkey以监听端口接受连接,然后MonkeyRunner通过连接该端口建立socket并发送monkey命令。所有与界面相关的操作都是通过这种方式发送到目标机器的。 - **发送adb协议请求**:通过发送adb协议请求来与目标设备通信的,详情请查看<<[谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析](http://blog.csdn.net/zhubaitian/article/details/40262831)>>和<<[adb概览及协议参考](http://blog.csdn.net/zhubaitian/article/details/40260783)>>,其实adb命令行客户端的所有命令最终都是通过发送遵循adb协议的请求来实现的,只是做成命令行方式方便终端用户使用而已 - **发送adb shell命令**:模拟adb命令行工具发送adb shell命令,只是不是真正的直接命令行调用adb工具,而是在每一个命令执行之前先通过上面的“**发送adb协议请求**“发送“shell:”请求建立一个和adb服务器通信的adb shell的socket连接通道,adb服务器再和目标设备的adb守护进程进行通信 以下是MonkeyDevice所有请求对应的与设备通信方式

请求

是否需要和目标设备通信

通信方式

注解

发送adb shell命令

getSystemProperty

发送adb shell命令


installPackage

发送adb shell命令

传送数据时发送adb协议请求,发送安装命令时使用adb shell命令

startActivity

发送adb shell命令


broadcastIntent

发送adb shell命令


instrument

发送adb shell命令


shell

发送adb shell命令

命令为空,所以相当于直接执行”adb shell “

removePackage

发送adb shell命令


发送monkey命令

getProperty

发送monkey命令  


wake

发送monkey命令  


dispose 

发送monkey命令   


press

发送monkey命令  


type

发送monkey命令  


touch

发送monkey命令  


drag

发送monkey命令  


getViewIdList

发送monkey命令  


getView

发送monkey命令  


getViews

发送monkey命令  


getRootView

发送monkey命令  


发送adb协议请求

takeSnapshot

发送adb协议请求


reboot

发送adb协议命令


installPackage

发送adb协议请求

相当于直接发送adb命令行命令’adb push’

分析之前请大家准备好对应的几个库的源码: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755ac570e.jpg) ## 2. 发送monkey命令 在剖析如何发送monkey命令之前,我们需要先去了解一个类,因为这个类是处理所有monkey命令的关键,这就是ChimpChat库的ChimpManager类。 我们先查看其构造函数,看它是怎么初始化的: ~~~ /* */ private Socket monkeySocket; /* */ /* */ private BufferedWriter monkeyWriter; /* */ /* */ private BufferedReader monkeyReader; /* */ /* */ /* */ public ChimpManager(Socket monkeySocket) /* */ throws IOException /* */ { /* 62 */ this.monkeySocket = monkeySocket; /* 63 */ this.monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); /* */ /* 65 */ this.monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); /* */ } ~~~ 初始化所做的事情如下 - 把构造函数传进来的monkeySocket这个socket对象保存起来,往下会分析这个socket是如何创立的 - 初始化monkeyWriter这个BufferedWriter,今后往monkey的socket发送命令的时候用的就是它 - 初始化monkeyReader这个BufferedReader,今后从monkey的socket读返回的时候用的就是它 好,那么现在我们返回来看这个类是什么时候实例化的。请定位到AdbChimpDevice的构造函数: ~~~ /* */ private ChimpManager manager; /* */ /* */ public AdbChimpDevice(IDevice device) /* */ { /* 70 */ this.device = device; /* 71 */ this.manager = createManager("127.0.0.1", 12345); /* */ /* 73 */ Preconditions.checkNotNull(this.manager); /* */ } ~~~ 可以看到ChimpManager是在AdbChimpDevice构造的时候已经开始初始化的了,初始化传入的地址是"127.0.0.1"和端口是12345,这个是在下面分析的createManager这个方法中创建socket用的,也就是我们上面提到的monkeySocket.在继续之前这里我们先整理下思路,结合上一篇文章,我们看到几个重要的类的初始化流程是这样的: - MonkeyRunner在启动的时候会先启动MonkeyRunnerStarter这个类,该类的构造函数调用ChimpChat的getInstance方法实例化ChimpChat. - ChimpChat的getInstance方法会先实例化AdbBackend这个类,然后构建 ChimpChat自身这个实例 - 用户调用MonkeyRunner.waitForConnection()方法初始化MonkeyDevice - 以上的waitForConnection()又调用的是ChimpChat的waitforConnection()方法 - ChimpChat的waitForConnection方法调用的是AdbBackend的waitForConnection方法最终会findAttachedDevice找到目标设备然后用该设备初始化AdbChimpDevice 根据以上的流程我们就很清晰AdbChimpDevice其实在测试脚本一调用MonkeyRunner.waitForConnection方法的时候就已经会初始化的了,也就是说ChimpManager也在这个时候已经初始化的了。 好,那么我们继续看AdbChimpDevice里面的方法createManager是如何对ChimpManager进行初始化的: ~~~ /* */ private ChimpManager createManager(String address, int port) { /* */ try { /* 125 */ this.device.createForward(port, port); /* */ } catch (TimeoutException e) { /* 127 */ LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e); /* 128 */ return null; /* */ } catch (AdbCommandRejectedException e) { /* 130 */ LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e); /* 131 */ return null; /* */ } catch (IOException e) { /* 133 */ LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e); /* 134 */ return null; /* */ } /* */ /* 137 */ String command = "monkey --port " + port; /* 138 */ executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE)); /* */ /* */ try /* */ { /* 142 */ Thread.sleep(1000L); /* */ } catch (InterruptedException e) { /* 144 */ LOG.log(Level.SEVERE, "Unable to sleep", e); /* */ } /* */ InetAddress addr; /* */ try /* */ { /* 149 */ addr = InetAddress.getByName(address); /* */ } catch (UnknownHostException e) { /* 151 */ LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e); /* 152 */ return null; /* */ } /* */ /* */ /* */ /* */ /* */ /* 159 */ boolean success = false; /* 160 */ ChimpManager mm = null; /* 161 */ long start = System.currentTimeMillis(); /* */ /* 163 */ while (!success) { /* 164 */ long now = System.currentTimeMillis(); /* 165 */ long diff = now - start; /* 166 */ if (diff > 30000L) { /* 167 */ LOG.severe("Timeout while trying to create chimp mananger"); /* 168 */ return null; /* */ } /* */ try /* */ { /* 172 */ Thread.sleep(1000L); /* */ } catch (InterruptedException e) { /* 174 */ LOG.log(Level.SEVERE, "Unable to sleep", e); /* */ } /* */ Socket monkeySocket; /* */ try /* */ { /* 179 */ monkeySocket = new Socket(addr, port); /* */ } catch (IOException e) { /* 181 */ LOG.log(Level.FINE, "Unable to connect socket", e); /* 182 */ success = false; } /* 183 */ continue; /* */ /* */ try /* */ { /* 187 */ mm = new ChimpManager(monkeySocket); /* */ } catch (IOException e) { /* 189 */ LOG.log(Level.SEVERE, "Unable to open writer and reader to socket"); } /* 190 */ continue; /* */ /* */ try /* */ { /* 194 */ mm.wake(); /* */ } catch (IOException e) { /* 196 */ LOG.log(Level.FINE, "Unable to wake up device", e); /* 197 */ success = false; } /* 198 */ continue; /* */ /* 200 */ success = true; /* */ } /* */ /* 203 */ return mm; /* */ } ~~~ 这个方法比较长,但大体做的事情如下: - 通过调用ddmlib的device类里面的createForward方法来把主机pc端本地的端口转发给目标机器端的monkey监听端口,这样子做的好处是我们通过直接连接主机pc端的转发端口发送命令就会等同于通过网络连接上目标机器的monkey监听端口来发送monkey命令 - 调用executeAsyncCommand方法发送异步adb shell命令 “monkey -port"到目标机器开启monkey并监听以上描述的端口 - 创建连接到主机pc对应目标设备monkey监听端口的monkeySocket - 把该monkeySocket传递到本章节开头说的ChimpManager构造函数对ChimpManager进行实例化 分析到这里我们可以看到monkey已经在目标机器起来了,那么我们就需要去分析MonkeyRunner是如何发送monkey命令过去控制设备的了。这里我们会以典型的press这个方法作为例子来进行阐述。 我们先看AdbChimpDevice里面的press方法: ~~~ /* */ public void press(String keyName, TouchPressType type) /* */ { /* */ try /* */ { /* 326 */ switch (3.$SwitchMap$com$android$chimpchat$core$TouchPressType[type.ordinal()]) { /* */ case 1: /* 328 */ this.manager.press(keyName); /* 329 */ break; /* */ case 2: /* 331 */ this.manager.keyDown(keyName); /* 332 */ break; /* */ case 3: /* 334 */ this.manager.keyUp(keyName); /* */ } /* */ } /* */ catch (IOException e) { /* 338 */ LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e); /* */ } /* */ } ~~~ 方法很简单,就是根据不同的按下类型来调用ChimpManager中不同的press的方法,我们这里假设用户按下的是 DOWN_AND_UP这个类型,也就是说调用的是ChimpMananer里面的press方法: ~~~ /* */ public boolean press(String name) /* */ throws IOException /* */ { /* 135 */ return sendMonkeyEvent("press " + name); /* */ } ~~~ 跟着调用sendMonkeyEvent: ~~~ /* */ private boolean sendMonkeyEvent(String command) /* */ throws IOException /* */ { /* 234 */ synchronized (this) { /* 235 */ String monkeyResponse = sendMonkeyEventAndGetResponse(command); /* 236 */ return parseResponseForSuccess(monkeyResponse); /* */ } /* */ } ~~~ 跟着调用sendMonkeyEventAndGetResponse方法: ~~~ /* */ private String sendMonkeyEventAndGetResponse(String command) /* */ throws IOException /* */ { /* 182 */ command = command.trim(); /* 183 */ LOG.info("Monkey Command: " + command + "."); /* */ /* */ /* 186 */ this.monkeyWriter.write(command + "\n"); /* 187 */ this.monkeyWriter.flush(); /* 188 */ return this.monkeyReader.readLine(); /* */ } ~~~ 以上这几个方法都是在ChimpManager这个类里面的成员方法。从最后这个sendMonkeyEventAndGetResponse方法我们可以看到它所做的事情就是用我们前面描述的monkeyWritter和monkeyReader这两个成员变量往主机pc这边的终会转发给目标机器monkey那个端口(其实就是上面的monkeySocket)进行读写操作。 ## 3. 发送adb协议请求 请查看《[谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析](http://blog.csdn.net/zhubaitian/article/details/40262831)》 ## 4. 发送adb shell命令 通过上一篇文章《[谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析](http://blog.csdn.net/zhubaitian/article/details/40262831)》的分析,我们知道MonkeyRunner分发不同的设备控制信息是在ChimpChat库的AdbChimpDevice这个类里面进行的。所以这里我就不会从头开始分析我们是怎么进入到这个类里面的了,大家不清楚的请先查看上一篇投石问路的文章再返回来看本文。 这里我们尝试以getSystemProperty这个稍微复杂点的方法为例子分析下MonkeyRunner是真么通过adb shell发送命令的,我们首先定位到AdbChimpDevice的该方法: ~~~ /* */ public String getSystemProperty(String key) /* */ { /* 224 */ return this.device.getProperty(key); /* */ } ~~~ 这里的device成员函数指的就是ddmlib库里面的Device这个类(请查看上一篇文章),那么我们进去该类看下getProperty这个方法: ~~~ /* */ public String getProperty(String name) /* */ { /* 379 */ return (String)this.mProperties.get(name); /* */ } ~~~ 该方法直接使用mProperties这个Device类的成员变量的get方法根据property的名字获得返回值,从定义可以看出这是个map: ~~~ /* 65 */ private final Map mProperties = new HashMap(); ~~~ 且这个map是在初始化Device实例之前就已经定义好的了,因为其构造函数并没有代码提及,但是我们可以看到Device类里面有一个函数专门往这个map里面添加property: ~~~ /* */ void addProperty(String label, String value) { /* 779 */ this.mProperties.put(label, value); /* */ } ~~~ 那么这个addProperty又是在哪里被调用了呢?一番查看后发现是在ddmlib里面的GetPropertyReceiver这个类里面的processNewLines这个方法: ~~~ /* */ public void processNewLines(String[] lines) /* */ { /* 49 */ for (String line : lines) { /* 50 */ if ((!line.isEmpty()) && (!line.startsWith("#"))) /* */ { /* */ /* */ /* 54 */ Matcher m = GETPROP_PATTERN.matcher(line); /* 55 */ if (m.matches()) { /* 56 */ String label = m.group(1); /* 57 */ String value = m.group(2); /* */ /* 59 */ if (!label.isEmpty()) { /* 60 */ this.mDevice.addProperty(label, value); /* */ } /* */ } /* */ } /* */ } /* */ } ~~~ 给这个map增加所有property的地方是知道了,但是问题是什么时候增加呢?这里我们先卖个关子。 继续之前我们先要了解下ddmlib这个库里面的DeviceMonitor这个类,这个类会启动一个线程来监控所有连接到主机的设备的状态。 ~~~ /* */ boolean start() /* */ { /* 715 */ if ((this.mAdbOsLocation != null) && (sAdbServerPort != 0) && ((!this.mVersionCheck) || (!startAdb()))) { /* 716 */ return false; /* */ } /* */ /* 719 */ this.mStarted = true; /* */ /* */ /* 722 */ this.mDeviceMonitor = new DeviceMonitor(this); /* 723 */ this.mDeviceMonitor.start(); /* */ /* 725 */ return true; /* */ } ~~~ 线程的启动是在我们之前见过的AdbDebugBridge里面,一旦adb启动,就会去调用构造函数去初始化DeviceMonitor实例,并调用实例的上面这个start方法来启动一个线程。 ~~~ /* */ boolean start() /* */ { /* 715 */ if ((this.mAdbOsLocation != null) && (sAdbServerPort != 0) && ((!this.mVersionCheck) || (!startAdb()))) { /* 716 */ return false; /* */ } /* */ /* 719 */ this.mStarted = true; /* */ /* */ /* 722 */ this.mDeviceMonitor = new DeviceMonitor(this); /* 723 */ this.mDeviceMonitor.start(); /* */ /* 725 */ return true; /* */ } ~~~ 该线程会进行一个无限循环来检测设备的变动。 ~~~ private void deviceMonitorLoop() /* */ { /* */ do /* */ { /* */ try /* */ { /* 161 */ if (this.mMainAdbConnection == null) { /* 162 */ Log.d("DeviceMonitor", "Opening adb connection"); /* 163 */ this.mMainAdbConnection = openAdbConnection(); /* 164 */ if (this.mMainAdbConnection == null) { /* 165 */ this.mConnectionAttempt += 1; /* 166 */ Log.e("DeviceMonitor", "Connection attempts: " + this.mConnectionAttempt); /* 167 */ if (this.mConnectionAttempt > 10) { /* 168 */ if (!this.mServer.startAdb()) { /* 169 */ this.mRestartAttemptCount += 1; /* 170 */ Log.e("DeviceMonitor", "adb restart attempts: " + this.mRestartAttemptCount); /* */ } /* */ else { /* 173 */ this.mRestartAttemptCount = 0; /* */ } /* */ } /* 176 */ waitABit(); /* */ } else { /* 178 */ Log.d("DeviceMonitor", "Connected to adb for device monitoring"); /* 179 */ this.mConnectionAttempt = 0; /* */ } /* */ } /* */ /* 183 */ if ((this.mMainAdbConnection != null) && (!this.mMonitoring)) { /* 184 */ this.mMonitoring = sendDeviceListMonitoringRequest(); /* */ } /* */ /* 187 */ if (this.mMonitoring) /* */ { /* 189 */ int length = readLength(this.mMainAdbConnection, this.mLengthBuffer); /* */ /* 191 */ if (length >= 0) /* */ { /* 193 */ processIncomingDeviceData(length); /* */ /* */ /* 196 */ this.mInitialDeviceListDone = true; /* */ } /* */ } /* */ } /* */ catch (AsynchronousCloseException ace) {}catch (TimeoutException ioe) /* */ { /* 202 */ handleExpectionInMonitorLoop(ioe); /* */ } catch (IOException ioe) { /* 204 */ handleExpectionInMonitorLoop(ioe); /* */ } /* 206 */ } while (!this.mQuit); /* */ } ~~~ 一旦发现设备有变动,该循环会立刻调用processIncomingDeviceData这个方法来更新设备信息 ~~~ /* */ private void processIncomingDeviceData(int length) throws IOException /* */ { /* 298 */ ArrayList list = new ArrayList(); /* */ /* 300 */ if (length > 0) { /* 301 */ byte[] buffer = new byte[length]; /* 302 */ String result = read(this.mMainAdbConnection, buffer); /* */ /* 304 */ String[] devices = result.split("\n"); /* */ /* 306 */ for (String d : devices) { /* 307 */ String[] param = d.split("\t"); /* 308 */ if (param.length == 2) /* */ { /* 310 */ Device device = new Device(this, param[0], IDevice.DeviceState.getState(param[1])); /* */ /* */ /* */ /* 314 */ list.add(device); /* */ } /* */ } /* */ } /* */ /* */ /* 320 */ updateDevices(list); /* */ } ~~~ 该方法首先会取得所有的device列表(类似"adb devices -l"命令获得所有device列表),然后调用updateDevices这个方法来对所有设备信息进行一次更新: ~~~ private void updateDevices(ArrayList newList) /* */ { /* 329 */ synchronized () /* */ { /* */ /* */ /* 333 */ ArrayList devicesToQuery = new ArrayList(); /* 334 */ synchronized (this.mDevices) /* */ { /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* */ /* 344 */ for (int d = 0; d < this.mDevices.size();) { /* 345 */ Device device = (Device)this.mDevices.get(d); /* */ /* */ /* 348 */ int count = newList.size(); /* 349 */ boolean foundMatch = false; /* 350 */ for (int dd = 0; dd < count; dd++) { /* 351 */ Device newDevice = (Device)newList.get(dd); /* */ /* 353 */ if (newDevice.getSerialNumber().equals(device.getSerialNumber())) { /* 354 */ foundMatch = true; /* */ /* */ /* 357 */ if (device.getState() != newDevice.getState()) { /* 358 */ device.setState(newDevice.getState()); /* 359 */ device.update(1); /* */ /* */ /* */ /* 363 */ if (device.isOnline()) { /* 364 */ if ((AndroidDebugBridge.getClientSupport()) && /* 365 */ (!startMonitoringDevice(device))) { /* 366 */ Log.e("DeviceMonitor", "Failed to start monitoring " + device.getSerialNumber()); /* */ } /* */ /* */ /* */ /* */ /* 372 */ if (device.getPropertyCount() == 0) { /* 373 */ devicesToQuery.add(device); /* */ } /* */ } /* */ } /* */ /* */ /* 379 */ newList.remove(dd); /* 380 */ break; /* */ } /* */ } /* */ /* 384 */ if (!foundMatch) /* */ { /* */ /* 387 */ removeDevice(device); /* 388 */ this.mServer.deviceDisconnected(device); /* */ } /* */ else { /* 391 */ d++; /* */ } /* */ } /* */ /* */ /* */ /* 397 */ for (Device newDevice : newList) /* */ { /* 399 */ this.mDevices.add(newDevice); /* 400 */ this.mServer.deviceConnected(newDevice); /* */ /* */ /* 403 */ if ((AndroidDebugBridge.getClientSupport()) && /* 404 */ (newDevice.isOnline())) { /* 405 */ startMonitoringDevice(newDevice); /* */ } /* */ /* */ /* */ /* 410 */ if (newDevice.isOnline()) { /* 411 */ devicesToQuery.add(newDevice); /* */ } /* */ } /* */ } /* */ /* */ /* 417 */ for (Device d : devicesToQuery) { /* 418 */ queryNewDeviceForInfo(d); /* */ } /* */ } /* 421 */ newList.clear(); /* */ } ~~~ 该方法我们关注的是最后面它会循环每个设备,然后调用queryNewDeviceForInfo这个方法去更新每个设备所有的porperty信息。 ~~~ /* */ private void queryNewDeviceForInfo(Device device) /* */ { /* */ try /* */ { /* 446 */ device.executeShellCommand("getprop", new GetPropReceiver(device)); /* */ /* */ /* 449 */ queryNewDeviceForMountingPoint(device, "EXTERNAL_STORAGE"); /* 450 */ queryNewDeviceForMountingPoint(device, "ANDROID_DATA"); /* 451 */ queryNewDeviceForMountingPoint(device, "ANDROID_ROOT"); /* */ /* */ /* 454 */ if (device.isEmulator()) { /* 455 */ EmulatorConsole console = EmulatorConsole.getConsole(device); /* 456 */ if (console != null) { /* 457 */ device.setAvdName(console.getAvdName()); /* 458 */ console.close(); /* */ } /* */ } /* */ } catch (TimeoutException e) { /* 462 */ Log.w("DeviceMonitor", String.format("Connection timeout getting info for device %s", new Object[] { device.getSerialNumber() })); /* */ /* */ } /* */ catch (AdbCommandRejectedException e) /* */ { /* 467 */ Log.w("DeviceMonitor", String.format("Adb rejected command to get device %1$s info: %2$s", new Object[] { device.getSerialNumber(), e.getMessage() })); /* */ /* */ } /* */ catch (ShellCommandUnresponsiveException e) /* */ { /* 472 */ Log.w("DeviceMonitor", String.format("Adb shell command took too long returning info for device %s", new Object[] { device.getSerialNumber() })); /* */ /* */ } /* */ catch (IOException e) /* */ { /* 477 */ Log.w("DeviceMonitor", String.format("IO Error getting info for device %s", new Object[] { device.getSerialNumber() })); /* */ } /* */ } ~~~ 到了这里我们终于看到了该方法调用了一个ddmlib库的device类里面的executeShellCommand方法来执行‘getprop'这个命令。到目前位置我们达到的目的是知道了getSystemProperty这个MonkeyDevice的api最终确实是通过发送'adb shell getporp‘命令来获得设备属性的。 但这里遗留了两个问题 - 一个是之前提到的GetPropertyReceiver这个类里面的增加property的processNewLines方法是在哪里调用的 - 一个是executeShellCommand究竟是怎么工作的 各位看官不用着急,且看我们往下分析,很快就会水落石出了。我们继续跟踪executeShellCommand这个方法,在我们的例子中其以命令'getprop'和new的GetPropertyReceiver对象实例为参数,最终会调用到Device这个类里面的executeShellCommand这个方法。注意这个GetPropertyReceiver很重要,我们往后会看到。 ~~~ /* */ public void executeShellCommand(String command, IShellOutputReceiver receiver, int maxTimeToOutputResponse) /* */ throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException /* */ { /* 618 */ AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, receiver, maxTimeToOutputResponse); /* */ } ~~~ 方法中继续把调用直接抛给AdbHelper这个工具类, ~~~ /* */ static void executeRemoteCommand(InetSocketAddress adbSockAddr, String command, IDevice device, IShellOutputReceiver rcvr, long maxTimeToOutputResponse, TimeUnit maxTimeUnits) /* */ throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException /* */ { /* 378 */ long maxTimeToOutputMs = 0L; /* 379 */ if (maxTimeToOutputResponse > 0L) { /* 380 */ if (maxTimeUnits == null) { /* 381 */ throw new NullPointerException("Time unit must not be null for non-zero max."); /* */ } /* 383 */ maxTimeToOutputMs = maxTimeUnits.toMillis(maxTimeToOutputResponse); /* */ } /* */ /* 386 */ Log.v("ddms", "execute: running " + command); /* */ /* 388 */ SocketChannel adbChan = null; /* */ try { /* 390 */ adbChan = SocketChannel.open(adbSockAddr); /* 391 */ adbChan.configureBlocking(false); /* */ /* */ /* */ /* */ /* 396 */ setDevice(adbChan, device); /* */ /* 398 */ byte[] request = formAdbRequest("shell:" + command); /* 399 */ write(adbChan, request); /* */ /* 401 */ AdbResponse resp = readAdbResponse(adbChan, false); /* 402 */ if (!resp.okay) { /* 403 */ Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message); /* 404 */ throw new AdbCommandRejectedException(resp.message); /* */ } /* */ /* 407 */ byte[] data = new byte['䀀']; /* 408 */ ByteBuffer buf = ByteBuffer.wrap(data); /* 409 */ long timeToResponseCount = 0L; /* */ /* */ for (;;) /* */ { /* 413 */ if ((rcvr != null) && (rcvr.isCancelled())) { /* 414 */ Log.v("ddms", "execute: cancelled"); /* 415 */ break; /* */ } /* */ /* 418 */ int count = adbChan.read(buf); /* 419 */ if (count < 0) /* */ { /* 421 */ rcvr.flush(); /* 422 */ Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + count); /* */ /* 424 */ break; } /* 425 */ if (count == 0) { /* */ try { /* 427 */ int wait = 25; /* 428 */ timeToResponseCount += wait; /* 429 */ if ((maxTimeToOutputMs > 0L) && (timeToResponseCount > maxTimeToOutputMs)) { /* 430 */ throw new ShellCommandUnresponsiveException(); /* */ } /* 432 */ Thread.sleep(wait); /* */ } /* */ catch (InterruptedException ie) {} /* */ } /* */ else { /* 437 */ timeToResponseCount = 0L; /* */ /* */ /* 440 */ if (rcvr != null) { /* 441 */ rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position()); /* */ } /* 443 */ buf.rewind(); /* */ } /* */ } /* */ } finally { /* 447 */ if (adbChan != null) { /* 448 */ adbChan.close(); /* */ } /* 450 */ Log.v("ddms", "execute: returning"); /* */ } /* */ } ~~~ 方法中先创建一个面向adb服务器的socket通道,然后通过发送adb协议请求的'shell:'命令获得一个adb shell然后再把相应的adb shell命令发送到该socket。从这里可以看到,“发送adb shell命令“其实是基于”发送adb协议请求“的,因为在发送命令之前需要先通过组织基于adb协议的请求”shell:“来获得adb shell。对比上一篇文章《[谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析](http://blog.csdn.net/zhubaitian/article/details/40262831)》我们可以看到“发送adb协议请求”跟“发送adb shell命名”的最大区别就是: - 发送adb协议请求:不需要初始化adb shell,直接通过构造基于adb协议的请求把命令发送出去给adb服务器。 - 发送adb shell命令:每个命令都需要先发送“adb协议请求”的“shell:”来先建立一个adb shell,然后才能够发送命令到adb服务器,再由adb服务器转发到设备端的adb守护进程或者服务。 发送完请求后最终会调用rcvr.addOutput(buf.array(),buf.arrayOffset(), buf.position())这个方法,这里的rcvr就是通过参数传进来的我们上面提到的很重要的那个GetPropertyReceiver,那么我们去看下该类下面的addOutput究竟是怎么处理返回信息的,这里要查看的是GetPropertyReceiver父类MultiLineReceiver类的成员函数addOutPut: ~~~ /* */ public final void addOutput(byte[] data, int offset, int length) /* */ { /* 53 */ if (!isCancelled()) { /* 54 */ String s = new String(data, offset, length, Charsets.UTF_8); /* */ /* */ /* */ /* 58 */ if (this.mUnfinishedLine != null) { /* 59 */ s = this.mUnfinishedLine + s; /* 60 */ this.mUnfinishedLine = null; /* */ } /* */ /* */ /* 64 */ this.mArray.clear(); /* 65 */ int start = 0; /* */ for (;;) { /* 67 */ int index = s.indexOf("\r\n", start); /* */ /* */ /* */ /* 71 */ if (index == -1) { /* 72 */ this.mUnfinishedLine = s.substring(start); /* 73 */ break; /* */ } /* */ /* */ /* */ /* 78 */ String line = s.substring(start, index); /* 79 */ if (this.mTrimLines) { /* 80 */ line = line.trim(); /* */ } /* 82 */ this.mArray.add(line); /* */ /* */ /* 85 */ start = index + 2; /* */ } /* */ /* 88 */ if (!this.mArray.isEmpty()) /* */ { /* */ /* 91 */ String[] lines = (String[])this.mArray.toArray(new String[this.mArray.size()]); /* */ /* */ /* 94 */ processNewLines(lines); /* */ } /* */ } /* */ } ~~~ 这个函数所作的事情就是把'adb shell getprop‘返回的所有信息一行一行的进行处理,注意最终处理的函数就是processNewLines。还记得这个函数吧?这个就是我们上面提到的GetPropertyReceiver这个类中用来往mProperties这个map增加property的了。迄今为止我们算是把以上留下了两个疑问给解决完了
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyRunner源码分析之-谁动了我的截图?

最后更新于:2022-04-01 19:56:45

本文章的目的是通过分析monkeyrunner是如何实现截屏来作为一个例子尝试投石问路为下一篇文章做准备,往下一篇文章本人有意分析下monkeyrunner究竟是如何和目标测试机器通信的,所以最好的办法本人认为是先跟踪一个调用示例从高层到底层进行分析,本人以前分析操作系统源代码的时候就是先从用户层的write这个api入手,然后一路打通到vfs文件系统层,到设备驱动层的,其效果比单纯的理论描述更容易理解和接受。 在整个代码分析过程中会设计到以下的库,希望想动手分析的同学们准备好源码: - monkeyrunner - chimpchat - ddmlib 想来google对自动化测试框架的命名很有趣,有叫猴子(Monkey)的,也有叫大猩猩(Chimp)的。 ## 1. 究竟是哪个禽兽动了我的截图? 首先我们先看takeSnapshot的入口函数是在MonkeyDevice这个class里面的(因为所有的代码都是反编译的,所以代码排版方便可能有点别扭). MonkeyDevice.class takeSnapshot(): ~~~ /* */ @MonkeyRunnerExported(doc="Gets the device's screen buffer, yielding a screen capture of the entire display.", returns="A MonkeyImage object (a bitmap wrapper)") /* */ public MonkeyImage takeSnapshot() /* */ { /* 92 */ IChimpImage image = this.impl.takeSnapshot(); /* 93 */ return new MonkeyImage(image); /* */ } ~~~ 这是我们的monkeyrunner测试脚本尝试去截屏的入口函数,所做的事情大概如下 - 调用MonkeyDevice的成员变量impl的takeSnapshot()函数(往下我们会看impl是怎么传进来的)去获得截图并赋予给IChimpImage的变量 - 把截图转换成MonkeyImage并返回给用户 这里重点是impl这个变量是怎么回事,它是在MonkeyDevice的构造函数中被赋值的: ~~~ public MonkeyDevice(IChimpDevice impl) /* */ { /* 75 */ this.impl = impl; /* */ } ~~~ 其中IChimpDevice是一个接口,里面定义好了MonkeyDevice需要和目标测试机器通讯的规范: ~~~ public abstract interface IChimpDevice { public abstract ChimpManager getManager(); public abstract void dispose(); public abstract HierarchyViewer getHierarchyViewer(); public abstract IChimpImage takeSnapshot(); public abstract void reboot(@Nullable String paramString); public abstract Collection getPropertyList(); public abstract String getProperty(String paramString); public abstract String getSystemProperty(String paramString); public abstract void touch(int paramInt1, int paramInt2, TouchPressType paramTouchPressType); public abstract void press(String paramString, TouchPressType paramTouchPressType); public abstract void press(PhysicalButton paramPhysicalButton, TouchPressType paramTouchPressType); public abstract void drag(int paramInt1, int paramInt2, int paramInt3, int paramInt4, int paramInt5, long paramLong); public abstract void type(String paramString); public abstract String shell(String paramString); public abstract String shell(String paramString, int paramInt); public abstract boolean installPackage(String paramString); public abstract boolean removePackage(String paramString); public abstract void startActivity(@Nullable String paramString1, @Nullable String paramString2, @Nullable String paramString3, @Nullable String paramString4, Collection paramCollection, Map paramMap, @Nullable String paramString5, int paramInt); public abstract void broadcastIntent(@Nullable String paramString1, @Nullable String paramString2, @Nullable String paramString3, @Nullable String paramString4, Collection paramCollection, Map paramMap, @Nullable String paramString5, int paramInt); public abstract Map instrument(String paramString, Map paramMap); public abstract void wake(); public abstract Collection getViewIdList(); public abstract IChimpView getView(ISelector paramISelector); public abstract IChimpView getRootView(); public abstract Collection getViews(IMultiSelector paramIMultiSelector); } ~~~ MonkeyDevice的构造函数运用了面向对象的多态技术把某一个实现了IChimpDevice接口的对象赋予给成员函数IChimpDevice类型的impl成员变量,那么“某一个设备对象”又是在哪里传进来的呢? 在我们的测试代码中我们很清楚一个MonkeyDevice对象的初始化都不是直接调用构造函数实现的,而是通过调用MonkeyRunner实例的waitForConnection实现的,代码如下: ~~~ /* */ @MonkeyRunnerExported(doc="Waits for the workstation to connect to the device.", args={"timeout", "deviceId"}, argDocs={"The timeout in seconds to wait. The default is to wait indefinitely.", "A regular expression that specifies the device name. See the documentation for 'adb' in the Developer Guide to learn more about device names."}, returns="A ChimpDevice object representing the connected device.") /* */ public static MonkeyDevice waitForConnection(PyObject[] args, String[] kws) /* */ { /* 64 */ ArgParser ap = JythonUtils.createArgParser(args, kws); /* 65 */ Preconditions.checkNotNull(ap); /* */ long timeoutMs; /* */ try /* */ { /* 69 */ double timeoutInSecs = JythonUtils.getFloat(ap, 0); /* 70 */ timeoutMs = (timeoutInSecs * 1000.0D); /* */ } catch (PyException e) { /* 72 */ timeoutMs = Long.MAX_VALUE; /* */ } /* */ /* 75 */ IChimpDevice device = chimpchat.waitForConnection(timeoutMs, ap.getString(1, ".*")); /* */ /* 77 */ MonkeyDevice chimpDevice = new MonkeyDevice(device); /* 78 */ return chimpDevice; /* */ } ~~~ 该函数所做的事情就是根据用户输入的函数等待连接上一个测试设备然后返回设备并赋值给上面的MonkeyDevice中的impl成员变量。返回的device是通过chimpchat.jar这个库里面的com.android.chimpchat.ChimpChat模块中的waitForConnection方法实现的: ~~~ /* */ public IChimpDevice waitForConnection(long timeoutMs, String deviceId) /* */ { /* 91 */ return this.mBackend.waitForConnection(timeoutMs, deviceId); /* */ } ~~~ 这里面又调用了ChimpChat这个类的成员变量mBackend的waitForConnection方法来获得设备,这个变量是在ChimpChat的构造函数初始化的: ~~~ /* */ private ChimpChat(IChimpBackend backend) /* */ { /* 39 */ this.mBackend = backend; /* */ } ~~~ 那么这个backend参数又是从哪里传进来的呢?也就是说ChimpChat的构造函数是在哪里被调用的呢?其实就是在ChimpChat里面的getInstance的两个重载方法里面: ~~~ /* */ public static ChimpChat getInstance(Map options) /* */ { /* 48 */ sAdbLocation = (String)options.get("adbLocation"); /* 49 */ sNoInitAdb = Boolean.valueOf((String)options.get("noInitAdb")).booleanValue(); /* */ /* 51 */ IChimpBackend backend = createBackendByName((String)options.get("backend")); /* 52 */ if (backend == null) { /* 53 */ return null; /* */ } /* 55 */ ChimpChat chimpchat = new ChimpChat(backend); /* 56 */ return chimpchat; /* */ } /* */ /* */ /* */ /* */ public static ChimpChat getInstance() /* */ { /* 63 */ Map options = new TreeMap(); /* 64 */ options.put("backend", "adb"); /* 65 */ return getInstance(options); /* */ } ~~~ 从代码可以看到backend最终是通过createBackendByName这个方法进行初始化的,那么我们看下该方法做了什么事情: ~~~ /* */ private static IChimpBackend createBackendByName(String backendName) /* */ { /* 77 */ if ("adb".equals(backendName)) { /* 78 */ return new AdbBackend(sAdbLocation, sNoInitAdb); /* */ } /* 80 */ return null; /* */ } ~~~ 其实它最终实例化的就是ChimpChat.jar库里面的AdbBackend这个Class。其实这个类就是封装了adb的一个wrapper类。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755aa09a4.jpg) 到了现在我们终于定位到ChimpChat这个类里面的成员变量mBackend实际上就是AdbBackend了。那么我们就要去看下它里面的waitForConnection方法究竟是如何获得一个接口是IChimpDevice的device的(也就是我们文章开头描述的impl这个MonkeyDevice的成员变量). ~~~ /* */ public IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex) /* */ { /* */ do { /* 119 */ IDevice device = findAttachedDevice(deviceIdRegex); /* */ /* 121 */ if ((device != null) && (device.getState() == IDevice.DeviceState.ONLINE)) { /* 122 */ IChimpDevice chimpDevice = new AdbChimpDevice(device); /* 123 */ this.devices.add(chimpDevice); /* 124 */ return chimpDevice; /* */ } /* */ try /* */ { /* 128 */ Thread.sleep(200L); /* */ } catch (InterruptedException e) { /* 130 */ LOG.log(Level.SEVERE, "Error sleeping", e); /* */ } /* 132 */ timeoutMs -= 200L; /* 133 */ } while (timeoutMs > 0L); /* */ /* */ /* 136 */ return null; /* */ } ~~~ 方法首先通过findAttachedDevice方法获得目标设备(其实该方法里面所做的事情可以类比直接执行命令"adb devices",下文有更详细的描述), 如果该设备存在且是ONLINE状态(关于各总状态的描述请查看上一篇文章《[adb概览及协议参考](http://blog.csdn.net/zhubaitian/article/details/40260783)》)的话就去实例化一个AdbChimpDevice设备对象并返回。 经过以上的一大堆描述,最终我们的目的就是确定文章开头的takeSnapshot入口函数所用到的获取截图的device(impl)究竟是什么device,这里我们终于确定了就是ChimChat.jar这个库里面的AdbChimpDevice这个设备。 ~~~ IChimpImage image = this.impl.takeSnapshot(); ~~~ ## 2. 大猩猩是如何通过AdbChimpDevice进行怒吼传递信息的 其实chimpchat这个大猩猩并不是最终处理我们的截图的库,细究下去会发现AdbChimpDevice其实只是相当于一个信息的传递着的角色,只是过程中加入了自己的一些特有信息而已。这就好比大猩猩在原始森林中没有通讯设备,只能使用原始的怒吼来通知伙伴有危险等情况了。 既然我们已经定位到截图设备是AdbChimpDevice,那么我们就去看看它里面的tapeSnapshot方法是怎么实现的: ~~~ /* */ public IChimpImage takeSnapshot() /* */ { /* */ try { /* 209 */ return new AdbChimpImage(this.device.getScreenshot()); /* */ } catch (TimeoutException e) { /* 211 */ LOG.log(Level.SEVERE, "Unable to take snapshot", e); /* 212 */ return null; /* */ } catch (AdbCommandRejectedException e) { /* 214 */ LOG.log(Level.SEVERE, "Unable to take snapshot", e); /* 215 */ return null; /* */ } catch (IOException e) { /* 217 */ LOG.log(Level.SEVERE, "Unable to take snapshot", e); } /* 218 */ return null; /* */ } ~~~ 方法代码很少,一眼就可以看到它是调用了自己的成员变量device的getScreenshot这个方法获得截图然后转换成AdbChimpImage,至于怎么转换的我们不需要去管它,无非就是不同的类如何一层层继承,最终如何通过多态继承机制进行转换而已。 这里我们关键是先去找到成员变量device又是什么设备,它里面的截图又是怎么回事。 继续分析代码可以看到该device变量也是在AdbChimpDevice的构造函数中进行定义的: ~~~ /* */ public AdbChimpDevice(IDevice device) /* */ { /* 70 */ this.device = device; /* 71 */ this.manager = createManager("127.0.0.1", 12345); /* */ /* 73 */ Preconditions.checkNotNull(this.manager); /* */ } ~~~ 那么我们一如既往的需要找到该参数的device是在哪里传进来的。相信大家还记得上一章节描述的AdbBackend是如何实例化AdbChimpDevice的,在实例化之前会调用一个findAttachedDevice的方法的先获得一个实现了IDevice接口的对象,然后传给这里的AdbChimpDevice构造函数进行实例化的。 ~~~ /* */ public IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex) /* */ { /* */ do { /* 119 */ IDevice device = findAttachedDevice(deviceIdRegex); /* */ /* 121 */ if ((device != null) && (device.getState() == IDevice.DeviceState.ONLINE)) { /* 122 */ IChimpDevice chimpDevice = new AdbChimpDevice(device); /* 123 */ this.devices.add(chimpDevice); /* 124 */ return chimpDevice; /* */ } /* */ try /* */ { /* 128 */ Thread.sleep(200L); /* */ } catch (InterruptedException e) { /* 130 */ LOG.log(Level.SEVERE, "Error sleeping", e); /* */ } /* 132 */ timeoutMs -= 200L; /* 133 */ } while (timeoutMs > 0L); /* */ /* */ /* 136 */ return null; /* */ } ~~~ 那么我们就需要分析下findAttachedDevice这个方法究竟找到的是怎么样的一个IDevice对象了,在分析之前先要注意这里的IDevice接口定义的都是一些底层的操作目标设备的接口方法,由此可知我们已经慢慢接近真相了。以下是其代码片段: ~~~ /* */ public abstract interface IDevice extends IShellEnabledDevice /* */ { /* */ public static final String PROP_BUILD_VERSION = "ro.build.version.release"; /* */ public static final String PROP_BUILD_API_LEVEL = "ro.build.version.sdk"; /* */ public static final String PROP_BUILD_CODENAME = "ro.build.version.codename"; /* */ public static final String PROP_DEVICE_MODEL = "ro.product.model"; /* */ public static final String PROP_DEVICE_MANUFACTURER = "ro.product.manufacturer"; /* */ public static final String PROP_DEVICE_CPU_ABI = "ro.product.cpu.abi"; /* */ public static final String PROP_DEVICE_CPU_ABI2 = "ro.product.cpu.abi2"; /* */ public static final String PROP_BUILD_CHARACTERISTICS = "ro.build.characteristics"; /* */ public static final String PROP_DEBUGGABLE = "ro.debuggable"; /* */ public static final String FIRST_EMULATOR_SN = "emulator-5554"; /* */ public static final int CHANGE_STATE = 1; /* */ public static final int CHANGE_CLIENT_LIST = 2; /* */ public static final int CHANGE_BUILD_INFO = 4; /* */ @Deprecated /* */ public static final String PROP_BUILD_VERSION_NUMBER = "ro.build.version.sdk"; /* */ public static final String MNT_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; /* */ public static final String MNT_ROOT = "ANDROID_ROOT"; /* */ public static final String MNT_DATA = "ANDROID_DATA"; /* */ /* */ @NonNull /* */ public abstract String getSerialNumber(); /* */ /* */ @Nullable /* */ public abstract String getAvdName(); /* */ /* */ public abstract DeviceState getState(); /* */ /* */ public abstract java.util.Map getProperties(); /* */ /* */ public abstract int getPropertyCount(); /* */ /* */ public abstract String getProperty(String paramString); /* */ /* */ public abstract boolean arePropertiesSet(); /* */ /* */ public abstract String getPropertySync(String paramString) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException; /* */ /* */ public abstract String getPropertyCacheOrSync(String paramString) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException; /* */ /* */ public abstract boolean supportsFeature(@NonNull Feature paramFeature); /* */ /* */ public static enum Feature /* */ { /* 53 */ SCREEN_RECORD, /* 54 */ PROCSTATS; /* */ /* */ private Feature() {} /* */ } /* */ /* 59 */ public static enum HardwareFeature { WATCH("watch"); /* */ /* */ private final String mCharacteristic; /* */ /* */ private HardwareFeature(String characteristic) { /* 64 */ this.mCharacteristic = characteristic; /* */ } ~~~ 我们继续看findAttachedDevice的源码: ~~~ /* */ private IDevice findAttachedDevice(String deviceIdRegex) /* */ { /* 101 */ Pattern pattern = Pattern.compile(deviceIdRegex); /* 102 */ for (IDevice device : this.bridge.getDevices()) { /* 103 */ String serialNumber = device.getSerialNumber(); /* 104 */ if (pattern.matcher(serialNumber).matches()) { /* 105 */ return device; /* */ } /* */ } /* 108 */ return null; /* */ } ~~~ 简单明了,一个循环所有列出来的(好比"adb devices -l"命令)所有设备,找到想要的那个。这里的AdbChimDevice里面的this.bridge成员变量其实代表的就是一个通过socket连接到adb服务器的一个adb客户端,这就是为什么我之前说chimpchat的AdbBackend事实上就是adb的一个wrapper。 往下我们继续跟踪看这个adb的wrapper是如何getDevices的,代码跳转到ddmlib这个库里面的AndroidDebugBridge这个class: ~~~ /* */ public IDevice[] getDevices() /* */ { /* 484 */ synchronized (sLock) { /* 485 */ if (this.mDeviceMonitor != null) { /* 486 */ return this.mDeviceMonitor.getDevices(); /* */ } /* */ } ~~~ 里面调用了AndroidDebugBridge的成员变量mDeviceMonitor的getDevices函数,那么我们看下这个成员变量究竟是定义成什么类型的: ~~~ /* */ private DeviceMonitor mDeviceMonitor; ~~~ 然后我们再跑到该DeviceMonitor类中去查看getDevices这个方法的代码: ~~~ /* */ Device[] getDevices() /* */ { /* 131 */ synchronized (this.mDevices) { /* 132 */ return (Device[])this.mDevices.toArray(new Device[this.mDevices.size()]); /* */ } /* */ } ~~~ 代码是取得成员函数mDevices的所有device列表然后返回,那么我们看下mDevices究竟是什么类型的列表: ~~~ /* 60 */ private final ArrayList mDevices = new ArrayList(); ~~~ 最终mDevices实际上是Device类型的一个列表,那么找到Device这个类就达到我们的目标了,就知道本章节开始的“return new AdbChimpImage(this.device.getScreenshot());”中的那个device究竟是什么device,也就是说知道去哪里去查找getScreenshot这个方法是在什么地方实现的了。 最终定位到com.android.ddmlib.Device这个类。 通过这个章节的分析可以看出来在chimpchat里面的AdbChimpDevice其实不是最终负责去获得截图的类,它只是作为一个信息重新包装的再分发的中转站而已。ddmlib才是最终处理我们的截图的库。 ## 3. ddmlib库如何通过请求adb服务器读取FrameBuffer设备进行截图 好我们继续看Device这个类究竟是如何获得我们的截图的: ~~~ /* */ public RawImage getScreenshot() /* */ throws TimeoutException, AdbCommandRejectedException, IOException /* */ { /* 558 */ return AdbHelper.getFrameBuffer(AndroidDebugBridge.getSocketAddress(), this); /* */ } ~~~ 里面只有一行代码,通过调用工具类AdbHelper的getFramebBuffer方法来获得截图,其实这里单看名字getFrameBuffer就应该猜到MonkeyRunner究竟是怎么截图的了,不就是读取目标机器的FrameBuffer 设备的缓存嘛,至于FrameBuffer设备的详细解析请大家自行google了,这里你只需要知道它是一个可以映射到用户空间的代表显卡内容的一个设备,获得它就相当于获得当前的屏幕截图就足够了。 好,我们还是继续往下分析,看getFrameBuffer是怎么实现截屏的,定位到ddmlib的AdbHelper类: ~~~ /* */ static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device) /* */ throws TimeoutException, AdbCommandRejectedException, IOException /* */ { /* 272 */ RawImage imageParams = new RawImage(); /* 273 */ byte[] request = formAdbRequest("framebuffer:"); /* 274 */ byte[] nudge = { 0 }; /* */ /* */ /* */ /* */ /* 279 */ SocketChannel adbChan = null; /* */ try { /* 281 */ adbChan = SocketChannel.open(adbSockAddr); /* 282 */ adbChan.configureBlocking(false); /* */ /* */ /* */ /* 286 */ setDevice(adbChan, device); /* */ /* 288 */ write(adbChan, request); /* */ /* 290 */ AdbResponse resp = readAdbResponse(adbChan, false); /* 291 */ if (!resp.okay) { /* 292 */ throw new AdbCommandRejectedException(resp.message); /* */ } /* */ /* */ /* 296 */ byte[] reply = new byte[4]; /* 297 */ read(adbChan, reply); /* */ /* 299 */ ByteBuffer buf = ByteBuffer.wrap(reply); /* 300 */ buf.order(ByteOrder.LITTLE_ENDIAN); /* */ /* 302 */ int version = buf.getInt(); /* */ /* */ /* 305 */ int headerSize = RawImage.getHeaderSize(version); /* */ /* */ /* 308 */ reply = new byte[headerSize * 4]; /* 309 */ read(adbChan, reply); /* */ /* 311 */ buf = ByteBuffer.wrap(reply); /* 312 */ buf.order(ByteOrder.LITTLE_ENDIAN); /* */ /* */ /* 315 */ if (!imageParams.readHeader(version, buf)) { /* 316 */ Log.e("Screenshot", "Unsupported protocol: " + version); /* 317 */ return null; /* */ } /* */ /* 320 */ Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size=" + imageParams.size + ", width=" + imageParams.width + ", height=" + imageParams.height); /* */ /* */ /* */ /* 324 */ write(adbChan, nudge); /* */ /* 326 */ reply = new byte[imageParams.size]; /* 327 */ read(adbChan, reply); /* */ /* 329 */ imageParams.data = reply; /* */ } finally { /* 331 */ if (adbChan != null) { /* 332 */ adbChan.close(); /* */ } /* */ } /* */ /* 336 */ return imageParams; /* */ } ~~~ 其实过程就是根据adb协议整合命令请求字串"framebuffer:",然后连接到adb服务器把请求发送到adb服务器,最终发送到设备上的adb守护进程通过读取framebuffer设备获得当前截图。 具体流程和adb协议详细解析请看本人上一篇文章: - [adb概览及协议参考](http://blog.csdn.net/zhubaitian/article/details/40260783)
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

adb概览及协议参考

最后更新于:2022-04-01 19:56:43

原文:https://github.com/android/platform_system_core/blob/master/adb/OVERVIEW.TXT) *Implementation notes regarding ADB.* ADB实现注解 ## 1. General Overview: 1概要 *The Android Debug Bridge (ADB) is used to:* ADB在以下情况下使用: - *keep track of all Android devices and emulators instances connected to or running on a given host developer machine* - 对所有连接到开发机器上的android真机和模拟器进行跟踪管理 - *implement various control commands (e.g. "adb shell", "adb pull", etc..) for the benefit of clients (command-line users, or helper programs like DDMS). These commands are what is called a 'service' in ADB.* - 实现了大量的控制命令(比如: "adb shell", "adb pull",等等)来方便用户使用(包括命令行用户和助手类程序如ddms),这些命令往往被我们叫做adb中的一个‘服务’。 *As a whole, everything works through the following components:* 总而言之,所有的事情都是围绕着以下这几个模块进行的: ### 1.1 The ADB server 1.1 ADB服务器 *This is a background process that runs on the host machine. Its purpose if to sense the USB ports to know when devices are attached/removed,as well as when emulator instances start/stop.* 这是在**主机设备(PC/开发机器)上运行的**一个后台进程。它的目的是嗅探何时有设备在主机的usb口上挂载/移除,以及模拟器何时开启/关闭。 *It thus maintains a list of "connected devices" and assigns a 'state' to each one of them: OFFLINE, BOOTLOADER, RECOVERY or ONLINE (more on this below).* 因此它会维护着一个"已连接设备"列表,并且为每个设备指定一个‘状态’:OFFLINE, BOOTLOADER, RECOVERY 或 ONLINE (下文会详述)。 *The ADB server is really one giant multiplexing loop whose purpose is to orchestrate the exchange of data (packets, really) between clients, services and devices.* ADB服务器确实可以称为是一个强大的多路路由,它的目的就是去协调组织客户端,各种服务和设备之间的数据交换(数据包,真实数据)。 ### 1.2 The ADB daemon (adbd) 1.2 ADB守护进程(adbd) *The 'adbd' program runs as a background process within an Android device or emulated system. Its purpose is to connect to the ADB server (through USB for devices, through TCP for emulators) and provide a few services for clients that run on the host.* adbd是一个在android真实机器或者模拟器上运行的后台伺服程序。它的目的是为了连接pc端的adb服务器(真实机器用usb,模拟器用tcp协议(译者注:其实真实机器也可以用tcp来连接,这篇文章没有及时更新过来))并且为在主机pc上运行的adb客户端应用提供一些服务。 *The ADB server considers that a device is ONLINE when it has successfully connected to the adbd program within it. Otherwise, the device is OFFLINE, meaning that the ADB server detected a new device/emulator, but could not connect to the adbd daemon.* 当adb服务器成功连接上android机器上的adbd伺服程序的时候就会认为该设备已经online,否者就会认为该设备是offline,指的是adb服务器有检测到一个新的设备连接上来,但是却没有成功连接上该设备的的adbd。 *the BOOTLOADER and RECOVERY states correspond to alternate states of devices when they are in the bootloader or recovery mode.* BOOTLOADER和RECOVERY着两个状态分别代表android设备处于bootloader或者recovery模式下的对应的可选状态。 ### 1.3. The ADB command-line client 1.3 ADB命令行客户端 *The 'adb' command-line program is used to run adb commands from a shell or a script. It first tries to locate the ADB server on the host machine, and will start one automatically if none is found.* adb命令行客户端是给shell或者脚本调用来跑各种adb命令的。它首先会尝试找到主机pc上运行的adb服务器,如果没有找到的话就会自动启动一个adb服务器。 *then, the client sends its service requests to the ADB server. It doesn't need to know.* 然后该adb命令行客户端会往adb服务器发送服务请求,而这些对于adb服务器来说是无需知道的。 *Currently, a single 'adb' binary is used for both the server and client. this makes distribution and starting the server easier.* 就当前来说,adb服务器和adb客户端使用的其实是同一个二进制文件,这样使得发布和启动服务器会更方便。 ### *1.4. Services* 1.4. 服务 *There are essentially two kinds of services that a client can talk to.* 本质上一个adb命令行客户端会和两类服务进行通信。 *Host Services: these services run within the ADB Server and thus do not need to communicate with a device at all. A typical example is "adb devices" which is used to return the list of currently known devices and their state. They are a few couple other services though.* 主机服务:这些服务是在adb服务器自身内部运行的所以根本不需要和任何的android设备进行交互。一个典型的命令就是列出当前连接的所有android设备和状态的命令“adb devices”。 当然还有一些其他的服务了。

命令

解释

host:version

 

host:kill

停止server

host:devices

 

host:track-devies

 

host:emulator:<port>

 

host:transport:<serial-number>

连接指定serial-number的设备或者模拟器

host:transport-usb

连接usb上的设备,如果usb上有不止一个设备,会失败。

host:transport-local

通过tcp方式连接模拟器,如果有多个模拟器在运行,会失败。

host:transport-any

连接usb设备或者模拟器都可以,但是如果有超过一个设备或模拟器,会失败。

host-serial:<serial-number>:<request>

host-usb:<request>

host-local:<request>

向指定的设备发送特定的请求。同样如果存在多个设备的冲突,会失败。

host:<request>

向当前连接的设备发送请求

<host-prefix>:get-serialno

获取设备的serial-number

<host-prefix>:get-state

获取设备状态

<host-prefix>:forward:<local>;<remote>

 


*Local Services: these services either run within the adbd daemon, or are started by it on the device. The ADB server is used to multiplex streams between the client and the service running in adbd. In this case its role is to initiate the connection, then of being a pass-through for the data.* 本地服务:这类服务是在adbd这个守护进程自身内部运行的,或者是由它启动运行的。adb服务器会在客户端和这些adbd中运行的服务之间进行数据路由。在这种情况下adb服务器扮演着初始化各种连接以及数据路信使的角色。

命令

解释

shell:command arg1 arg2 ...

在设备上执行命令行操作

shell:

参见commandline.c中的interactive_shell()

remount:

以读/写模式加载设备的文件系统

dev:<path>

为client打开设备上的特定路径,用于读写问题。有可能由于权限问题而失败。

tcp:<port>

尝试从设备连接本主机的某个tcp端口

tcp:<port>:<server-name>

尝试从设备连接特定主机名的某个tcp端口

local:<path>

尝试连接设备上的特定路径,路径是UNIX域名形式

localreserved:<path>

localabstract:<path>

localfilesystem:<path>

尝试连接设备上的特定路径。

log:<name>

打开设备上的特定日志文件,以便读取日志

framebuffer:

尝试获取framebuffer的快照。即涉笔的屏幕快照

dns:<server-name>

由serer执行来解析特定设备名

recover:<size>

更新设备的恢复镜像

jdwp:<pid>

连接特定VM进程上面的JDWP线程

track-jdwp

 

sync:

同步设备和主机上的文件

(注:以上两表整理来自网友 arm-linux:http://www.cnblogs.com/armlinux/archive/2011/02/16/2396845.html) ## 2 Protocol details: 2 协议细节 ### 2.1 Client <-> Server protocol: 2.1 客户端<--->服务器端 *This details the protocol used between ADB clients and the ADB server itself. The ADB server listens on TCP:localhost:5037.* 以下细节描述的是主机pc中adb客户端和adb服务器端通信用到的协议。adb服务器端会监听TCP:localhost:5037 *A client sends a request using the following format:* 客户端使用以下的协议格式发送请求: - *1. A 4-byte hexadecimal string giving the length of the payload* - 1. 前面是一个4字节的十六进制用来指定请求命令的长度 - *2. Followed by the payload itself.* - 2. 紧跟着请求命令自身的内容 *For example, to query the ADB server for its internal version number, the client will do the following:* 比如,为了得到adb服务器的内部版本号,客户端会做以下动作: - *1. Connect to tcp:localhost:5037* - 1. 连接到 tcp:localhost:5037 - *2. Send the string "000Chost:version" to the corresponding socket* - 2. 发送字串"000Chost:version"到对应套接字(译者注:十六进制000C就是十进制12,"host:version"刚好12个字节) *The 'host:' prefix is used to indicate that the request is addressed to the server itself (we will talk about other kinds of requests later). The content length is encoded in ASCII for easier debugging.* 'host'这个前缀是用来指定这个请求是发送给服务器自身的(我们晚点会谈下其他的请求类型),为了方便调试,请求内容长度是用ASCII编码的。 *The server should answer a request with one of the following:* 服务器端将会用以下的一种方式进行应答: - *1. For success, the 4-byte "OKAY" string* - 1. 成功:应答一个4字节的"OKAY"字串 - *2. For failure, the 4-byte "FAIL" string, followed by a 4-byte hex length, followed by a string giving the reason for failure.* - 2.失败:应答一个4字节的"FAIL"字串,紧跟着一个4字节十六进制描述错误描述内容长度,然后是描述错误的内容字串。 - *3. As a special exception, for 'host:version', a 4-byte hex string corresponding to the server's internal version number* - 3. 例外:'host:version'的返回将会是一个4字节字串代表着服务器的内部版本号。 *Note that the connection is still alive after an OKAY, which allows the client to make other requests. But in certain cases, an OKAY will even change the state of the connection.* 注意客户端和服务器端的连接在接收到OKAY的应答后将会继续保持,以便客户端继续其他请求。但在一些特定的情况下,OKAY应答会改变连接的状态。 *For example, the case of the 'host:transport:' request, where '' is used to identify a given device/emulator; after the "OKAY" answer, all further requests made by the client will go directly to the corresponding adbd daemon.* 比如,以命令'host:transport:‘请求为例(其中 ''用来指定一个指定的设备/模拟器),收到'OKAY'应答后,客户端往后的所有请求都将会直接发送到对应的设备/模拟器的adbd守护进程。 *The file SERVICES.TXT lists all services currently implemented by ADB.* 文件SERVICES.TXT列出了adb当前已经实现的所有服务(译者注:大家请自行google)。 ### *2.2. Transports:* *An ADB transport models a connection between the ADB server and one device or emulator. There are currently two kinds of transports:* adb传输指的是adb服务器和一个设备/模拟器之间的连接模型。当前有以下两种传输模型: - *USB transports, for physical devices through USB* - USB传输:真实机器通过usb连接的情况下 - *Local transports, for emulators running on the host, connected to the server through TCP* - 本地传输:本机上的模拟器通过tcp连接到adb服务器的情况下 *In theory, it should be possible to write a local transport that proxies a connection between an ADB server and a device/emulator connected to/ running on another machine. This hasn't been done yet though.* 理论上说,我们可以编写一个本地运行的传输代理来处理adb服务器和连接/运行在其他主机pc上的设备/模拟器的连接,但这个还没有实现。 *Each transport can carry one or more multiplexed streams between clients and the device/emulator they point to. The ADB server must handle unexpected transport disconnections (e.g. when a device is physically unplugged) properly.* 每一种传输方式都可以承载多路客户端和其指定的设备/模拟器之间的数据流传输。adb服务器必须合理的处理传输断开等异常(比如:当一个设备从pc主机上拔掉的情况)
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

EasyMonkeyDevice vs MonkeyDevice&amp;HierarchyViewer API Mapping Matrix

最后更新于:2022-04-01 19:56:41

## 1. 前言 本来这次文章的title是写成和前几篇类似的《EasyMonkeyDevice API实践全记录》,内容也打算把每个API的实践和建议给记录下来,但后来想了下觉得这样子并不是最好的方法,鉴于EasyMonkeyDevice其实就是在前几章描述的MonkeyDevice和HierarchyViewer的基础上加了一层Wrapper,把原来的通过接受坐标点或者ViewNode来操作控件的思想统一成通过控件ID来操作,其实最终它们都会转换成坐标点或ViewNode进行操作。以touch和visible这两个API为例子,大家看下以下的源码就很清楚了。 MonkeyDevice里面的touch是用坐标点作为参数的,而下面的EasyMonkeyDevice用得是id(By这个类里面就一个ID而已,有兴趣查其源码),最终还是转成坐标点: ~~~ public void touch(By selector, TouchPressType type) { Point p = getElementCenter(selector); mDevice.getImpl().touch(p.x, p.y, type); } ~~~ HierarchyViewer里面的Visible用的是ViewNode,EasyMonkeyDevice用得是id,最终还是转成ViewNode: ~~~ public boolean visible(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return mHierarchyViewer.visible(node); } ~~~ 所以本文应该除了给出API的实践之外还应该把每个API和其与MonkeyDevice和HierarchyViewer的API所对应的API给列出来做一个对应的Map,方便我们参考。 实践中我们还是用SDK自带的NotePad APK,假设已经有一个Note存在的情况下,通过以下步骤来走一遍EasyMonkeyDevice的所有API: - 使用MonkeyDevice对象实例化EasyMonkeyDevice - 通过ID Touch一个Note - 获得进入NoteEditor activity后的WindowId并验证是否正确 - 通过ID检查Note的内容这个EditText是否存在和可见 - 通过Note的ID获得Text - 通过Note的ID Type进新Text - 通过Note的ID获得Location 以下是我们操作过程中会看到的两个Activity的截图,先贴上来给大家对以上步骤有一个感性认识,最后我会贴出实践验证性代码。 NotesList Activity截图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755a4c165.jpg) NoteEditor Activity截图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755a707eb.jpg) ## 2. EaysyMonkeyDevice API List and Sample EasyMonkeyDevice是在MonkeyDevice和HierarchyViewer的基础上出来的一个类,按照本人的理解,主要增加的功能就是: - 在MonkeyDevice和HierarchyViewer的基础上针对部分API增加了对控件ID的支持以操作控件 以下是个人整理的列表:尝试对所有的API进行一个描述和示例Demo

Return

EasyMonkeyDevice

Demo

Comment

 

 

EasyMonkeyDevice(

MonkeyDevice device)

Use Monkey device to construct

an EasyMonkeyDevice object,

note that it would instantiate a

HierarchyViewer member by

device within this constructor

 

device = MonkeyRunner.waitForConnection()

 

eDevice=EasyMonkeyDevice(device)

Constructor

Void

touch(By selector,

TouchPressType type)

Sends a touch event specified

by ‘type’ to the screen location

specified by ‘by’

 触摸点击主窗口:

#Step 1: try touching on the first note

eDevice.touch(By.id('id/text1'),

              MonkeyDevice.DOWN_AND_UP)

触摸弹出框或Menu Options会失败:

MonkeyRunner.sleep(2)

print 'Show Menu Options'

device.press('KEYCODE_MENU',

             MonkeyDevice.DOWN_AND_UP);

 

MonkeyRunner.sleep(3)

print 'Press on the menu entry of \

 \"Add note\"'

eDevice.touch(By.id('id/title'),

               MonkeyDevice.DOWN)

 

参数By实际上

只有By.ID,从

其源码可以看

出来;

type参数跟

MonkeyDevice

一样就那几个

DOWN/UP之类的

 

根据个人实践

和网上评论,

对系统菜单和

弹出框的支持

有问题

Void

type(By selector, String text)

Types a string into the specified

object

 

#Step 5: setText

eDevice.type(By.id(noteId), 'New')

 

Boolean

exists(By selector)

Checks if the specified object

exists.

 

#Step3: is note EditText exist?

noteId = 'id/note'

if True == eDevice.exists(By.id(noteId)):

    print 'Note exist'

else:

    print 'Note not found!'

    exit(2)

 

Boolean

visible(By selector)

Checks if the specified object is visible.

 

#Step4: is note EditText visible?

if True == eDevice.visible(By.id(noteId)):

    print 'Note is visible'

else:

    print 'Note is invisible'

    exit(3)

 

String

getText(By selector)

Obtain the text in the selected

input box.

 

#Step 4: getText

text = eDevice.getText(By.id(noteId))

print 'Note text:',text.encode('utf-8')

 

String

getFocusedWindowId()

Gets the id of the focused window.

returns = "The symbolic id of the

focused window or None."

 

#Step 2: Get the window ID

winId = 'com.example.android.notepad/\

    com.example.android.notepad.NoteEditor'

#Need to sleep a while till ready

MonkeyRunner.sleep(3)

winId = eDevice.getFocusedWindowId()

if(winId == winId):

    print "Edit Note WinId is:",\

        winId.encode('utf-8')

else:

    print "Failed"

    exit(1)



 结果跟

HierarchyViewer

getFocusedWin

dowName

返回值一模

一样,所以

猜想WindowID

WindowName

是同一回事

PyTuple

locate(By selector)

Locates the coordinates of the

selected object

returns = "Tuple containing

(x,y,w,h) location and size.")

 

#Step 6: locate

locate = eDevice.locate(By.id(noteId))

print 'Location(x,y,w,h) is:',locate

 

## 3. EasyMonkeyDevice vs MonkeyDevice API Mapping Matrix 这里会列出MonkeyDevice的所有API已经EasyMonkeyDevice与其对应的API,没有去掉冗余是因为方便今后Reference的时候知道EasyMonkeyDevice并不是完全把所有MonkeyDevice的API都进行Wrap的,只有以下两个。下面一章理同。 - **touch**:MonkeyDevice通过坐标点touch;EasyMonkeyDevice通过控件ID去touch - **type**:MonkeyDevice往当前focused地方输入;EasyMonkeyDevice往由ID指定控件输入

EasyMonkeyDevice API vs MonkeyDevice API

 

MonkeyDevice

EasyMonkeyDevice

Comment

 

Void broadcastIntent (string uri, string action,

 string data, string mimetype, 

iterable categories dictionary extras, 

component component, iterable flags)

Broadcasts an Intent to this device, as if the

Intent were coming from an application.

 

 

Void drag (tuple start, tuple end, float duration, 

integer steps)

Simulates a drag gesture (touch, hold, and

move) on this device's screen.

 

 

ObjectgetProperty (string key)

Given the name of a system environment

variable, returns its value for this device.

The available variable names are listed in

the detailed description of this method.

 

 

ObjectgetSystemProperty (string key)

The API equivalent of adb shell getprop

<key>. This is provided for

use by platform developers.

 

 

Void installPackage (string path)

Installs the Android application or test package

 contained in packageFile onto this device.

If the application or test package is already

installed, it is replaced.

 

 

Dictionaryinstrument (string className, dictionary args)

Runs the specified component under

Android instrumentation, and returns the results

 in a dictionary whose exact format is dictated

by the component being run.

 The component must already be present on

this device.

 

 

Void press (string name, dictionary type)

Sends the key event specified by type to the

key specified by keycode.

 

 

Void reboot (string into)

Reboots this device into the bootloader

specified by bootloadType.

 

 

Void removePackage (string package)

Deletes the specified package from this device,

 including its data and cache.

 

 

Objectshell (string cmd)

Executes an adb shell command and returns

the result, if any.

 

 

Void startActivity (string uri, string action, 

string data, string mimetype, iterable categories 

dictionary extras, component component, flags)

Starts an Activity on this device by sending an

Intent constructed from the supplied arguments.

 

 

MonkeyImagetakeSnapshot()

Captures the entire screen buffer of this device,

yielding a MonkeyImage object containing

a screen capture of the current display.

 

 

Void touch (integer x, integer y, integer type)

Sends a touch event specified by type to the

screen location specified by x and y.

Void touch(By selector, TouchPressType type)

Sends a touch event specified by

‘type’ to the screen location specified

 by ‘by’

MonkeyDevice通过坐标点touch

EasyMonkeyDevice通过控件IDtouch

Void type (string message)

Sends the characters contained in message to

this device, as if they had been typed on

the device's keyboard. This is equivalent to

calling press() for each keycode in message 

using the key event type DOWN_AND_UP.

Void type(By selector, String text)

Types a string into the specified

object

MonkeyDevice往当前focused地方输入;

EasyMonkeyDevice往由ID指定控件输入

Void wake ()

Wakes the screen of this device.

 

 

HierarchyViewer getHierarchyViewer()

Get the HierarchyViewer object for the device.

 

 

PyListgetPropertyList()

Retrieve the properties that can be queried

 

 

PyListgetViewIdList()

Retrieve the view ids for the current application

 

 

MonkeyViewgetViewById(String id)

doc = "Obtains the view with the specified id.",

args = {"id"},

argDocs = {"The id of the view to retrieve."},

returns = "The view object with the specified id."

 

 

MonkeyViewgetViewByAccessibilityIds(String WinId, String accessId)

args = {"windowId", "accessibility id"}

argDocs = {"The window id of the view to

retrieve.", "The accessibility id of the view to

retrieve."},

returns = "The view object with the specified id.")

 

 

MonkeyViewgetRootView()

Obtains current root view

 

 

PyListgetViewsByText(String text)

Obtains a list of views that contain the specified

text.",

args = {"text"},

returns = "A list of view objects that contain the specified text.")

 

 

## 4. EasyMonkeyDevice vs HierarchyViewer Mapping Matrix EasyMonkeyDevice Wrap了HiearchyViewer 的相应API而得到以下这些API - getFocusedWindowId:Wrap了hierarchyviewer的getFocusedWindowName,不再使用ViewNode而使用ID来的获得Window id/Name,其实根据我的实践id/name是同一回事 - locate:其实就是把HierarchyViewer的getAbsolutePositionOfView和getAbsoluteCenterOfView整合在一起获得起始坐标和Width/Hight,前者获得其实坐标,后者获得中心位置,相减后乘以2就是EasyMonkeyDevice想要的Width/Hight了 - visible: 同样是把参数由ViewNode改成id - getText:同上

EasyMonkeyDevice API vs HierarchViewer API

 

HierarchyViewer

EasyMonkeyDevice

Comment

 

public ViewNode findViewById(String id)

 

 

 

public ViewNode findViewById(String id, ViewNode rootNode)

 

 

public String getFocusedWindowName()

 

String getFocusedWindowId()

Gets the id of the focused window.

returns = "The symbolic id of the focused window or None."

 

public static Point getAbsolutePositionOfView(ViewNode node)/**

 

PyTuple locate(By selector)

Locates the coordinates of the selected object

returns = "Tuple containing (x,y,w,h) location and size.")

 

public static Point getAbsoluteCenterOfView(ViewNode node)

public boolean visible(ViewNode node)

 

boolean visible(By selector)

Checks if the specified object is visible.

 

public String getText(ViewNode node)

 

String getText(By selector)

Obtain the text in the selected input box.

 

## 5. EasyMonkeyDevice Standalone API 剩下一个API是没有跟以上的MonkeyDevice和HierarchyViewer有任何对应关系的: - visible:通过id检查该控件是否存在,猜想应该是google在代码重构的时候增加的一个方法方便大家判断而已

EasyMonkeyDevice Standalone API

 

EasyMonkeDevice

Comment

 

boolean exists(By selector)

Checks if the specified object exists.

 

## 6. 验证性代码 ~~~ from com.android.monkeyrunner import MonkeyRunner,MonkeyDevice,MonkeyImage from com.android.monkeyrunner.easy import EasyMonkeyDevice,By from com.android.chimpchat.hierarchyviewer import HierarchyViewer from com.android.hierarchyviewerlib.models import ViewNode, Window from java.awt import Point #from com.android.hierarchyviewerlib.device import #Connect to the target device device = MonkeyRunner.waitForConnection() eDevice=EasyMonkeyDevice(device) device.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") ''' MonkeyRunner.sleep(2) print 'Show Menu Options' device.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); MonkeyRunner.sleep(3) print 'Press on the menu entry of \ \"Add note\"' eDevice.touch(By.id('id/title'), MonkeyDevice.DOWN) MonkeyRunner.sleep(2) device.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); ''' #Step 1: try touching on the first note eDevice.touch(By.id('id/text1'), MonkeyDevice.DOWN_AND_UP) #Step 2: Get the window ID winId = 'com.example.android.notepad/\ com.example.android.notepad.NoteEditor' #Need to sleep a while till ready MonkeyRunner.sleep(3) winId = eDevice.getFocusedWindowId() if(winId == winId): print "Edit Note WinId is:",\ winId.encode('utf-8') else: print "Failed" exit(1) #Step3: is note EditText exist? noteId = 'id/note' if True == eDevice.exists(By.id(noteId)): print 'Note exist' else: print 'Note not found!' exit(2) #Step4: is note EditText visible? if True == eDevice.visible(By.id(noteId)): print 'Note is visible' else: print 'Note is invisible' exit(3) #Step 4: getText text = eDevice.getText(By.id(noteId)) print 'Note text:',text.encode('utf-8') #Step 5: setText eDevice.type(By.id(noteId), 'New') #Step 6: locate locate = eDevice.locate(By.id(noteId)) print 'Location(x,y,w,h) is:',locate ~~~ ## 7. EasyMonkeyDevice Source Code for Your Reference  ~~~ /*jadclipse*/// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov. package com.android.monkeyrunner.easy; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.monkeyrunner.JythonUtils; import com.google.common.base.Preconditions; import org.python.core.*; public class By extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(com/android/monkeyrunner/easy/By, dict); } By(String id) { this.id = id; } public static By id(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String id = ap.getString(0); return new By(id); } public static By id(String id) { return new By(id); } public ViewNode findView(HierarchyViewer viewer) { return viewer.findViewById(id); } private String id; } /* DECOMPILATION REPORT Decompiled from: D:\Projects\Workspace\JarPackages\monkeyrunner.jar Total time: 69 ms Jad reported messages/errors: The class file version is 50.0 (only 45.3, 46.0 and 47.0 are supported) Exit status: 0 Caught exceptions: */ ~~~ ## 8. By Class Source Code for Your Better Reference ~~~ /*jadclipse*/// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov. package com.android.monkeyrunner.easy; import com.android.chimpchat.core.IChimpDevice; import com.android.chimpchat.core.TouchPressType; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.monkeyrunner.JythonUtils; import com.android.monkeyrunner.MonkeyDevice; import com.google.common.base.Preconditions; import java.util.Set; import org.eclipse.swt.graphics.Point; import org.python.core.*; // Referenced classes of package com.android.monkeyrunner.easy: // By public class EasyMonkeyDevice extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(com/android/monkeyrunner/easy/EasyMonkeyDevice, dict); } public EasyMonkeyDevice(MonkeyDevice device) { mDevice = device; mHierarchyViewer = device.getImpl().getHierarchyViewer(); } public void touch(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); String tmpType = ap.getString(1); TouchPressType type = TouchPressType.fromIdentifier(tmpType); Preconditions.checkNotNull(type, (new StringBuilder()).append("Invalid touch type: ").append(tmpType).toString()); touch(selector, type); } public void touch(By selector, TouchPressType type) { Point p = getElementCenter(selector); mDevice.getImpl().touch(p.x, p.y, type); } public void type(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); String text = ap.getString(1); type(selector, text); } public void type(By selector, String text) { Point p = getElementCenter(selector); mDevice.getImpl().touch(p.x, p.y, TouchPressType.DOWN_AND_UP); mDevice.getImpl().type(text); } public PyTuple locate(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); ViewNode node = selector.findView(mHierarchyViewer); Point p = HierarchyViewer.getAbsolutePositionOfView(node); PyTuple tuple = new PyTuple(new PyObject[] { new PyInteger(p.x), new PyInteger(p.y), new PyInteger(node.width), new PyInteger(node.height) }); return tuple; } public boolean exists(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); return exists(selector); } public boolean exists(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return node != null; } public boolean visible(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); return visible(selector); } public boolean visible(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return mHierarchyViewer.visible(node); } public String getText(PyObject args[], String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); return getText(selector); } public String getText(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return mHierarchyViewer.getText(node); } public String getFocusedWindowId(PyObject args[], String kws[]) { return getFocusedWindowId(); } public String getFocusedWindowId() { return mHierarchyViewer.getFocusedWindowName(); } public PyObject __findattr_ex__(String name) { if(!EXPORTED_METHODS.contains(name)) return mDevice.__findattr_ex__(name); else return super.__findattr_ex__(name); } private By getSelector(ArgParser ap, int i) { return (By)ap.getPyObject(i).__tojava__(com/android/monkeyrunner/easy/By); } private Point getElementCenter(By selector) { ViewNode node = selector.findView(mHierarchyViewer); if(node == null) { throw new PyException(Py.ValueError, String.format("View not found: %s", new Object[] { selector })); } else { Point p = HierarchyViewer.getAbsoluteCenterOfView(node); return p; } } private MonkeyDevice mDevice; private HierarchyViewer mHierarchyViewer; private static final Set EXPORTED_METHODS = JythonUtils.getMethodNames(com/android/monkeyrunner/easy/EasyMonkeyDevice); } /* DECOMPILATION REPORT Decompiled from: D:\Projects\Workspace\JarPackages\monkeyrunner.jar Total time: 920 ms Jad reported messages/errors: The class file version is 50.0 (only 45.3, 46.0 and 47.0 are supported) Exit status: 0 Caught exceptions: */ ~~~
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyImage API 实践全记录

最后更新于:2022-04-01 19:56:39

## 1.背景 鉴于网上使用MonkeyImage的实例除了方法sameAs外很难找到,所以本人把实践各个API的过程记录下来然自己有更感性的认识,也为往后的工作打下更好的基础。同时也和上一篇文章《[MonkeyDevcie API 实践全记录](http://blog.csdn.net/zhubaitian/article/details/39926209)》起到相互呼应的作用。 因为并没有MonkeyRunner的项目背景,所以这里更多的是描述各个API是怎么一回事,而不是描述在什么场景下需要用到。也就是说是去回答What,而不是How。 首先我们先看下官方给出的MonkeyImage的API描述,对比我现在反编译的最新的源码是一致的:

Return Type

Methods

Comment

string

convertToBytes (string format)

Converts the current image to a particular format and returns it as a string that you can then access as an iterable of binary bytes.

 

tuple

getRawPixel (integer x, integer y)

Returns the single pixel at the image location (x,y), as an a tuple of integer, in the form (a,r,g,b).

 

integer

getRawPixelInt (integer x, integer y)

Returns the single pixel at the image location (x,y), as a 32-bit integer.

 

MonkeyImage

getSubImage (tuple rect)

Creates a new MonkeyImage object from a rectangular selection of the current image.

 

boolean

sameAs (MonkeyImage other, float percent)

Compares this MonkeyImage object to another and returns the result of the comparison. Thepercent argument specifies the percentage difference that is allowed for the two images to be "equal".

 

void

writeToFile (string path, string format)

Writes the current image to the file specified by filename, in the format specified by format.

 

## 2.String convertToBytes(string format) ### 2.1  示例 ~~~ img = device.takeSnapshot() png1 = img.convertToBytes() png2 = img.convertToBytes() bmp = img.convertToBytes('bmp') jpg = img.convertToBytes('JPG') gif = img.convertToBytes('gif') raw = img.convertToBytes('raw') invalid = img.convertToBytes('xxx') #is the 2 pngs equal? print "Two png is equal in bytes:",png1 == png2 #is the png equals to bmp? print "png and bmp is equal in bytes:", png1 == bmp #is the jpg eqals to the raw? print "jpg and bmp is equals in bytes:",jpg == bmp #is the jpg eqals to the xxx? print "jpg is a valid argument:",jpg != invalid #is the gif eqals to the xxx? print "gif is a valid argument:",gif != invalid #is the bmp eqals to the xxx? print "bmp is a valid argument:",bmp != invalid #is the raw equas to xxxx? aims at checking whether argument 'raw' is invalid like 'xxx' print 'raw is a valid argument:',raw != invalid #would invalid argument drop to png by default? print 'Would invalid argument drop to png by default:',png1 == invalid ~~~ 输出: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17559701d2.jpg) ### 2.2 分析 除了默认的png,常用格式jpg,gif都支持,但bmp格式无效,至于还支持什么其他格式,尝试跟踪了下代码,没有找到想要的结果 ## 3. tuple getRawPixel(integer x, integer y)和Integer getRawPixelInt (integer x, integer y) ### 3.1 示例 ~~~ viewer = device.getHierarchyViewer() note = viewer.findViewById('id/title') text = viewer.getText(note) print text.encode('utf-8') point = viewer.getAbsoluteCenterOfView(note) x = point.x y = point.y img = device.takeSnapshot() pixelTuple = img.getRawPixel(x,y) pixelInt = img.getRawPixelInt(x,y) print "Pixel in tuple:",pixelTuple print "Pixel in int:", pixelInt ~~~ 输出: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17559bc80d.jpg) ### 3.2 分析 这里把两个相似的方法放到一起来比较,他们都是获得指定一个坐标的argb值,其中a就是alpha(透明度),rgb就是颜色三元组红绿蓝了。但前者返回的是一个元组,后者返回的是整型。 那么两个类型的值是怎么对应起来的呢?其实就是第一个方法的元组的返回值(a,r,g,b)中的每个值转换成8个bit的二进制值然后按顺序从左到右排列起来再转换成十进制整型就是第二个方法的返回值了。 以示例输出为例,比如b在第一个返回值中是141,换成二进制就是1001101,其他雷同。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17559dcc86.jpg) 再看第二个方法的整型返回值是-7500403,转换成二进制其实就是11111111100011011000110110001101(至于下图calculator转换后为什么前面那么多个1,其实不用管他,因为是负数所以前面要加上FF之类而已),那么最后的8个bit转换成十进制其实就是上面的的141. ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755a1c028.jpg) ## 4. MonkeyImage getSubImage(tuple rect) ### 4.1 示例 ~~~ from com.android.monkeyrunner import MonkeyRunner,MonkeyDevice,MonkeyImage from com.android.monkeyrunner.easy import EasyMonkeyDevice,By from com.android.chimpchat.hierarchyviewer import HierarchyViewer from com.android.hierarchyviewerlib.models import ViewNode, Window from java.awt import Point #from com.android.hierarchyviewerlib.device import #Connect to the target targetDevice targetDevice = MonkeyRunner.waitForConnection() easy_device = EasyMonkeyDevice(targetDevice) #touch a button by id would need this targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") #invoke the menu options MonkeyRunner.sleep(6) #targetDevice.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); ''' public ViewNode findViewById(String id) * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. ''' #MonkeyRunner.alert("Continue?", "help", "Ok?") pic = targetDevice.takeSnapshot() pic = pic.getSubImage((0,38,480,762)) newPic = targetDevice.takeSnapshot() newPic = newPic.getSubImage((0,38,480,762)) print (newPic.sameAs(pic,1.0)) newPic.writeToFile('./shot1.png','png') ~~~ ### 4.2 分析 以上示例流程是 - 打开NotePad的NotesList Activity - 按下Menu Options按钮弹出“Add note”这个Menu Entry - 截取一个屏幕 - 调用getSubImage来取得去掉屏幕最上面的状态栏(因为有时间不断变化,所以每截屏一次可能都会有所改变)和最下面的Menu Options的一个Image - 再重复以上两个步骤取得另外一个Image - 比较以上两个image是否相同 - 把第二个image写到本地。 ## 5 boolean sameAs(MonkeyImage other, float percent) ### 5.1 示例 见4.1 ### 5.2 分析 流程见4.1,这里要注意第二个浮点型的参数是从0.0到1.0, 1.0代表必须100%相同,0.5代表可以有50%的Error Tolerance. ## 6. void writeToFile (string path, string format) ### 6.1 示例 请参见第4章节 ### 6.2 分析 参数很明了,这里需要提一下的是第一个参数路径,如果你填写的是相对路径的话,base用得是MonkeyRunner。也就是说示例中的图片最终是保存在我的monkeyrunner可执行程序的上一层目录。
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyDevcie API 实践全记录

最后更新于:2022-04-01 19:56:36

## 1.背景 使用SDK自带的NotePad应用作为实践目标应用,目的是对MonkeyDevice拥有的成员方法做一个初步的了解。 以下是官方列出的方法的Overview。

Return Type

Methods

Comment

void

broadcastIntent (string uri, string action, string data, string mimetype, 

iterable categories dictionary extras, component component, iterable flags)

Broadcasts an Intent to this device, as if the Intent were coming from an application.

 

void

drag (tuple start, tuple end, float duration, integer steps)

Simulates a drag gesture (touch, hold, and move) on this device's screen.

 

object

getProperty (string key)

Given the name of a system environment variable, returns its value for this device.

The available variable names are listed in the detailed description of this method.

 

object

getSystemProperty (string key)

. The API equivalent of adb shell getprop <key>. This is provided for

use by platform developers.

 

void

installPackage (string path)

Installs the Android application or test package contained in packageFile onto this device.

If the application or test package is already installed, it is replaced.

Obsolete,返回值是Boolean

dictionary

instrument (string className, dictionary args)

Runs the specified component under Android instrumentation, and returns the results

 in a dictionary whose exact format is dictated by the component being run.

 The component must already be present on this device.

 

void

press (string name, dictionary type)

Sends the key event specified by type to the key specified by keycode.

 

void

reboot (string into)

Reboots this device into the bootloader specified by bootloadType.

 

void

removePackage (string package)

Deletes the specified package from this device, including its data and cache.

 Obsolete,返回值是Boolean

object

shell (string cmd)

Executes an adb shell command and returns the result, if any.

 

void

startActivity (string uri, string action, string data, string mimetype, iterable categories 

dictionary extras, component component, flags)

Starts an Activity on this device by sending an Intent constructed from the supplied arguments.

 

MonkeyImage

takeSnapshot()

Captures the entire screen buffer of this device, yielding a MonkeyImage object containing

a screen capture of the current display.

 

void

touch (integer x, integer y, integer type)

Sends a touch event specified by type to the screen location specified by x and y.

 

void

type (string message)

Sends the characters contained in message to this device, as if they had been typed on

the device's keyboard. This is equivalent to callingpress() for each keycode in message 

using the key event type DOWN_AND_UP.

 

void

wake ()

Wakes the screen of this device.

 

其实官方这个表是没有及时更新的,我现在手头上用到的MonkeyRunner是当前最新的,里面就拥有好几个官网没有列出来的API,我怀疑是不是自从UIAutomator在03年出来后,google就不打算再继续维护MonkeyRunner了?如果有朋友知道事实的话,还麻烦告知。 以下是我整理出来的源码多出来的可用公共API列表

Return Type

Methods

Comment

HierarchyViewer

getHierarchyViewer(PyObject args[], String kws[])

获取一个HierarchyViewer对象

 请查看《MonkenRunner通过HierarchyViewer定位控件的方法和建议

PyList

getPropertyList(PyObject args[], String kws[])

 取得所有的property属性键值

PyList

getViewIdList(PyObject args[], String kws[])

 Failed

MonkeyView

getViewById(PyObject args[], String kws[])

 Failed

MonkeyView

getViewByAccessibilityIds(PyObject args[], String kws[])

 Failed

MonkeyView

getRootView(PyObject args[], String kws[])

 Failed

PyList

getViewsByText(PyObject args[], String kws[])

 Failed

但可惜的是在本人尝试以上多出来的API的时候,发现除了最上面两个可用之外,其他的都不可用并抛出错误。且网上资料少的可怜,别人碰到同样的问题也找不到解决办法。所以本人怀疑这些“隐藏”API是不是并没有完善,或者说google不准备完善,所以才没有列出到官网上面去。本人用的SDK tools和Platform tools已经是当前最新的23.0.2和20. 一个台湾网友碰到的问题描述:[http://imsardine.simplbug.com/note/monkeyrunner/api/hierarchy-viewer.html](http://imsardine.simplbug.com/note/monkeyrunner/api/hierarchy-viewer.html) ## 2.Void broadcastIntent  (string uri, string action,string data, string mimetype, iterable categories dictionary extras, componentcomponent, iterable flags) ### 2.1 分析 本人理解的此方法的本意是想广播一个Intent给我们的AndroidDevice,目标应用接收到该Intent做相应的处理,比如打开一个Activity等。但在我的多次尝试下并没有成功! ~~~ targetDevice.broadcastIntent(action='android.intent.action.INSERT', mimetype='vnd.android.cursor.dir/contact', extras = {'name':'user1501488', 'phone':'123-15489'} ~~~ 如果使用同样的参数,使用下面的startActivity是没有问题的。 ~~~ targetDevice.startActivity(action='android.intent.action.INSERT', mimetype='vnd.android.cursor.dir/contact', extras = {'name':'user1501488', 'phone':'123-15489'}) ~~~ google了半天网上根本找不到这个方法的使用例子,倒是stackOverFlow上有人建议用Shell来达到同样的效果。 ~~~ targetDevice.shell("am start -a android.intent.action.INSERT -t vnd.android.cursor.dir/contact -e name 'Donald Duck' -e phone 555-1234"). ~~~ 所以可见这个方法并没有多少人在用,原因应该是它完全可以用上面介绍的两个方法替代。 ## 3. void startActivity  (string uri, string action, string data, string mimetype, iterable categories dictionary extras, component component, flags) ### 3.1 示例 使用Action和mimetype来启动一个Activity: ~~~ targetDevice.startActivity(action='android.intent.action.VIEW', mimetype='vnd.android.cursor.dir/vnd.google.note') ~~~ 使用component来启动一个Activity: ~~~ targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") ~~~ 使用action,mimetype和指定extras参数来启动一个Activity: ~~~ targetDevice.startActivity(action='android.intent.action.INSERT', mimetype='vnd.android.cursor.dir/contact', extras = {'name':'user1501488', 'phone':'123-15489'}) ~~~ ### 3.2 分析 这个方法存在和以上的broadcastInt方法一样拥有同样的一大串参数,实例中只给出了本人已经跑通的例子,其他参数怎么用还有待深入研究,不过我相信对于我自己来说暂时这样子已经足够了。 另外需要注意的是参数中如果我们的填写不是按顺序从第一个参数开始的话,记得要在每个参数值前面指定你要传入的是哪个参数,不然默认就会从第一个参数开始算,这是python的基本语法了,这里就不展开了。 ## 4.  void drag (tuple start, tuple end, floatduration, integer steps) 这个方法的目的是按住一个控件然后把她拖动到其他位置 ### 4.1 实例 ~~~ viewer = targetDevice.getHierarchyViewer() note = viewer.findViewById('id/text1') point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y targetDevice.drag((startX,startY),(startX,startY),1) targetDevice.press('KEYCODE_BACK', MonkeyDevice.DOWN_AND_UP) ~~~ ### 4.2 分析和建议 以上示例是通过drag的方法来模拟LongPress,只要把参数中的起始坐标和目标坐标都设置成同样一个值,然后时常设置成一个有效的值就好了。 ## 5 Object getProperty (string key) 通过环境变量的key来获得其对应的值。以下是官方提供的可用化境变量列表

Property Group

Property

Description

Notes

build

board

Code name for the device's system board

See Build

brand

The carrier or provider for which the OS is customized.

 

device

The device design name.

 

fingerprint

A unique identifier for the currently-running build.

 

host

 

 

ID

A changelist number or label.

 

model

The end-user-visible name for the device.

 

product

The overall product name.

 

tags

Comma-separated tags that describe the build, such as "unsigned" and "debug".

 

type

The build type, such as "user" or "eng".

 

user

 

 

CPU_ABI

The name of the native code instruction set, in the form CPU type plus ABI convention.

 

manufacturer

The product/hardware manufacturer.

 

version.incremental

The internal code used by the source control system to represent this version of the software.

 

version.release

The user-visible name of this version of the software.

 

version.sdk

The user-visible SDK version associated with this version of the OS.

 

version.codename

The current development codename, or "REL" if this version of the software has been released.

 

display

width

The device's display width in pixels.

SeeDisplayMetricsfor details.

height

The device's display height in pixels.

 

density

The logical density of the display. This is a factor that scales DIP (Density-Independent Pixel) units to the device's resolution. DIP is adjusted so that 1 DIP is equivalent to one pixel on a 160 pixel-per-inch display. For example, on a 160-dpi screen, density = 1.0, while on a 120-dpi screen, density = .75.

The value does not exactly follow the real screen size, but is adjusted to conform to large changes in the display DPI. See density for more details.

 

am.current

package

The Android package name of the currently running package.

The am.currentkeys return information about the currently-running Activity.

action

The current activity's action. This has the same format as the name attribute of the action element in a package manifest.

 

comp.class

The class name of the component that started the current Activity. See comp.package for more details.

 

comp.package

The package name of the component that started the current Activity. A component is specified by a package name and the name of class that the package contains.

 

data

The data (if any) contained in the Intent that started the current Activity.

 

categories

The categories specified by the Intent that started the current Activity.

 

clock

realtime

The number of milliseconds since the device rebooted, including deep-sleep time.

SeeSystemClock for more information.

### 5.1示例 ~~~ displayWidth =targetDevice.getProperty ('display.width') printdisplayWidth.encode('utf-8') displayHight =targetDevice.getProperty('display.width') printdisplayHight.encode('utf-8') ~~~ ### 5.2  分析和建议 以上示例的目的是获得目标设备的长和高,当我们使用坐标点来操作控件的时候,调试的时候在一台机器上通过了,但是如果换了另外一个屏幕大小不一样的机器的话就会失败,因为控件的坐标点位置可能就变了。这个时候我们就需要用到示例中的连个属性来动态计算控件在不同屏幕大小的设备上面的坐标点了。 这里需要注意参数应该填写的格式是以上列表中前两列的组合PropertyGroup.Property ## 6. Object getSystemProperty (string key) ### 6.1 示例 ~~~ displayWidth = targetDevice.getSystemProperty ('service.adb.tcp.port') print displayWidth.encode('utf-8') ~~~ ### 6.2 分析和建议 根据官网的描述,这个函数和getProperty函数应该有同样的功能(Synonym for [getProperty()](http://developer.android.com/tools/help/MonkeyDevice.html#getProperty).),使用的属性表也如上面的属性列表一样。但是按照我的实践并非如此,不过它确实如官方描述的等同于命令“adb shell getprop .”倒是真的。 以上的例子是获取adb这个服务所打开的TCP端口,等同于如下的shell命令:“adb shell getprop service.adb.tcp.port“ 如果我尝试使用下面的方法去获得设备的长度,返回的结果其实会是None ~~~ displayWidth = targetDevice.getSystemProperty ('display.width') print displayWidth.encode('utf-8') ~~~ ## 7 Boolean installPackage (string path) ### 7.1示例 ~~~ if True == targetDevice.installPackage('D:\\Projects\\Workspace\\PythonMonkeyRunnerDemo\\apps\\MPortal.apk'): print "Installationfinished successfully" else: print "Failedto install the apk" ~~~ ### 7.2 分析和建议 这里有两点需要注意的: - 官方网站描述的这个API是没有返回值的(见背景中的表),而最新版本的API里面是Boolean值。 - 参数输入的应该是PC端这边的Local路径而非目标系统的Local路径。 注意这里碰到一个路径问题,如果我的路径写成: *'D:\Projects\Workspace\PythonMonkeyRunnerDemo\apps\MPortal.apk'* 那么会出现下图这样的错误: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17558b01ca.jpg) 如果在“apps”前面加多个转义符就没有问题: *'D:\Projects\Workspace\PythonMonkeyRunnerDemo\\apps\MPortal.apk'* 所以这里MonkeyRunner处理路径字串应该是存在Bug的,但是时间问题就先不去研究,建议所有路径的话“\“都加上转义符就好了 *'D:\\Projects\\Workspace\\PythonMonkeyRunnerDemo\\apps\MPortal.apk'* ## 8 boolean removePackage(string package) ### 8.1 示例 ~~~ if True == targetDevice.removePackage('com.majcit.portal'): print "Succeed toremove the package" else: print "Failedto remove teh package" ~~~ ### 8.2 注意事项 ·        如上面的installPackageAPI一样,官网描述的该API是没有返回值的,而最新的是返回boolean值 ## 9. dictionary instrument (string className, dictionary args) ### 9.1 示例 只指定第一个参数(将会跑所有指定Component下的所有case+ NotePad项目本身的使用了Instrumentation的用例,具体意思请看下一节的分析) ~~~ dict = targetDevice.instrument('com.example.android.notepad.tryout/android.test.InstrumentationTestRunner') print dict ~~~ 指定只跑其中的一个Case: ~~~ dict = dict = targetDevice.instrument('com.example.android.notepad.tryout/android.test.InstrumentationTestRunner', {'class':'com.example.android.notepad.tryout.TCCreateNote'}) print dict ~~~ ### 9.2 分析 这里首先需要看下官方的描述 Runs the specifiedcomponent under Android instrumentation, and returns the results in adictionary whose exact format is dictated by the component being run. Thecomponent must already be present on this device 这里重点是第一句“Runs the specified component under Android instrumentation“,剩下的是说返回值根据模块不一样而有所差别。那么第一句翻译过来就是”运行使用了Anroid Instrumentation的指定模块“。那么究竟什么是使用了Android Instrumentation的指定模块呢?其实如果你有用过Robotium的话应该知道,脚本的每个case都是从Instrumentation相关的类中继承下来的。 以下是本人之前写的一个Robotium的测试case(具体请查看本人之前的一篇《[Robotium创建一个Note的实例](http://blog.csdn.net/zhubaitian/article/details/39502119)》,可以看到它是继承于“ActivityInstrumentationTestCase2”的,其实它就满足了刚才的描述“使用了Anroid Instrumentation的指定模块“ ~~~ package com.example.android.notepad.tryout; import com.robotium.solo.Solo; import android.test.ActivityInstrumentationTestCase2; import android.app.Activity; @SuppressWarnings("rawtypes") public class TCCreateNote extends ActivityInstrumentationTestCase2{ private static Solo solo = null; public Activity activity; private static final int NUMBER_TOTAL_CASES = 2; private static int run = 0; private static Class launchActivityClass; //对应re-sign.jar生成出来的信息框里的两个值 private static String mainActiviy = "com.example.android.notepad.NotesList"; private static String packageName = "com.example.android.notepad"; static { try { launchActivityClass = Class.forName(mainActiviy); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } @SuppressWarnings("unchecked") public TCCreateNote() { super(packageName, launchActivityClass); } @Override public void setUp() throws Exception { //setUp() is run before a test case is started. //This is where the solo object is created. super.setUp(); //The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated // which would lead to soto to re-instantiated to be null if it's not set as static if(solo == null) { TCCreateNote.solo = new Solo(getInstrumentation(), getActivity()); } } @Override public void tearDown() throws Exception { //Check whether it's the last case executed. run += countTestCases(); if(run >= NUMBER_TOTAL_CASES) { solo.finishOpenedActivities(); } } public void testAddNoteCNTitle() throws Exception { solo.clickOnMenuItem("Add note"); solo.enterText(0, "中文标签笔记"); solo.clickOnMenuItem("Save"); solo.clickInList(0); solo.clearEditText(0); solo.enterText(0, "Text 1"); solo.clickOnMenuItem("Save"); solo.assertCurrentActivity("Expected NotesList Activity", "NotesList"); solo.clickLongOnText("中文标签笔记"); solo.clickOnText("Delete"); } public void testAddNoteEngTitle() throws Exception { solo.clickOnMenuItem("Add note"); solo.enterText(0, "English Title Note"); solo.clickOnMenuItem("Save"); solo.clickInList(0); solo.clearEditText(0); solo.enterText(0, "Text 1"); solo.clickOnMenuItem("Save"); solo.assertCurrentActivity("Expected NotesList Activity", "NotesList"); solo.clickLongOnText("English Title Note"); solo.clickOnText("Delete"); } } ~~~ 那么我们先在目标机器上执行命令“pm list instrumentation”看是否能列出这个“Component”: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17558eb89f.jpg) 这其中哪一个是我们想要的呢?这要看我们的Robotium测试项目中的AndroidManifest.xml的定义了,根据下图的packageName再对照上图的输出我们就定位到我们想要的Component了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175591fc76.jpg) 如果我们调用这个方法的时候只是填写了第一个参数的话,上图处在”com.example.andriod.notepad.tryout”这个Component下的所有三个方法都会执行。但在我的测试中,发现除了跑这个Case之外,它还跑多了一些步骤,就是额外的多创建了一个Note1。我查了半天,发现原来我之前除了编写了基于ActivityInstrumentationTestCase2的Robotium的这个项目之外,还在NoetePad这个项目上直接用InstrumentationTestCase写了一个基于Instrumentation的测试用例,做得事情就是增加一个Note1,具体请看本人另外一篇blog《[SDK Instrumentation创建一个Note的实例](http://blog.csdn.net/zhubaitian/article/details/39546371)》 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755943887.jpg) 其实调用这个方法相当于在shell脚本上执行如下的命令: *am instrument -w -r -e class com.example.android.notepad.tryout.TCCreateNote com.example.android.notepad.tryout/android.test.InstrumentationTestRunner* 最后提一下的是,我们的脚本在执行示例的时候大概5秒左右就会失败,但事实上在设备端所有的指定component的测试用例已经在执行的了。要解决这个问题需要重新编译源码,这里就免了,具体请查看:[http://stackoverflow.com/questions/4264057/android-cts-is-showing-shellcommandunresponsiveexception-on-emulator](http://stackoverflow.com/questions/4264057/android-cts-is-showing-shellcommandunresponsiveexception-on-emulator) 这里尝试做一个简单的总结 - 调用此方法之前先要找到第一个参数怎么写:执行命令“pm list instrumentation” -  调用次方法如果不指定第一个参数会执行指定Component下面的所有测试脚本和目标应用包中所有用到Instrumentation的测试用例 - 调用此方法最终事实上是在目标机器上根据指定的参数执行“am instrument xxxx” ## 10 void press (string name, integer type) ### 10.1 示例 ~~~ targetDevice.press('KEYCODE_BACK',MonkeyDevice.DOWN_AND_UP) ~~~ ### 10.2 分析 这个方法的目的是发送一个指定的Key事件到Android设备以触发相应的动作,比如示例中发送“KEYCODE_BACK”这个key到设备端去触发”返回“键的按下和升起(也就是点击)的动作。 具体Key的Code请查看:[http://developer.android.com/reference/android/view/KeyEvent.html](http://developer.android.com/reference/android/view/KeyEvent.html) 第二个参数是指导该Key应该如何Behave,比如先按下去,休眠一秒再弹起来(其实就相当于一个长按的动作)。具体支持behavior在本MonkeyDevice这个类的成员变量里有定义好:

Type

Constants

Notes

Comment

int官网是string,下同

DOWN

Use this with the type argument of press() or touch() to send a DOWN event.

 

int

UP

Use this with the type argument of press() or touch() to send an UP event.

 

int

DOWN_AND_UP

Use this with the type argument of press() or touch() to send a DOWN event immediately followed by an UP event.

 

int

MOVE

 TBD

官网没有列出

## 11. void touch (integer x,integer y, integer type) ### 11.1 示例 ~~~ viewer = targetDevice.getHierarchyViewer() note = viewer.findViewById('id/text1') point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y targetDevice.touch(startX,startY,MonkeyDevice.DOWN_AND_UP) ~~~ ### 11.2 分析 这个和上面的press方法在结果上有点类似,但是本方法是接受坐标进行点击的,而上面方法接受的是keycode。 第二个参数同上。 ## 12. void type (string message) viewer =targetDevice.getHierarchyViewer() note = viewer.findViewById(*'id/text1'*) point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y targetDevice.touch(startX,startY,MonkeyDevice.DOWN_AND_UP) MonkeyRunner.sleep(3) targetDevice.type(*'NewContent'*) ### 12.1 示例 ~~~ viewer = targetDevice.getHierarchyViewer() note = viewer.findViewById('id/text1') point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y targetDevice.touch(startX,startY,MonkeyDevice.DOWN_AND_UP) MonkeyRunner.sleep(3) targetDevice.type('NewContent') ~~~ ### 12.2 分析 指定一串字符串进行模拟键盘输入,等同于调用press方法按照一个一个的keycode用MonkeyDevice的DOWN_AND_UP进行输入。 尝试时发现中文不支持,鉴于项目不需要用到这种手机键盘,所以节省时间暂不研究。 ## 13. Wake() ### 13.1示例 ~~~ targetDevice.wake() ~~~ ### 13.2 分析 注意这里只是唤醒屏保,如果手机已经锁屏的话你是不能唤醒的。所以尝试这个API的时候要注意把锁屏功能先关闭掉。 ## 14. MonkeyImange takeSnapshot() ### 14.1 示例 ~~~ #Connect to the target targetDevice targetDevice = MonkeyRunner.waitForConnection() easy_device = EasyMonkeyDevice(targetDevice) #touch a button by id would need this targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") #invoke the menu options MonkeyRunner.sleep(6) #targetDevice.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); ''' public ViewNode findViewById(String id) * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. ''' #MonkeyRunner.alert("Continue?", "help", "Ok?") pic = targetDevice.takeSnapshot() pic = pic.getSubImage((0,38,480,762)) newPic = targetDevice.takeSnapshot() newPic = newPic.getSubImage((0,38,480,762)) print (newPic.sameAs(pic,1.0)) newPic.writeToFile('./shot1.png','png') ~~~ ## 14 object shell(string cmd) ### 14.1 示例 ~~~ res = targetDevice.shell('ls /data/local/tmp|grep note') print res ~~~ ### 14.2 分析 这个命令等同于你直接在命令行上“adb shell $command” ##15 void reboot(string into) 其他参数没有用到所以也就没有尝试,仅仅尝试了不带参数的情况,结果就是直接reboot目标设备了 ~~~ targetDevice.reboot() ~~~ 这里有点需要提下的是,如果你是在MonkeyRunner命令行下执行这条命令的话,就算目标机器重启,整个MonkeyRunner的环境还是依然有效的。也就是说你如果继续打进一条"targetDevice.reboot()",你的设备就会再重启一次。
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkenRunner通过HierarchyViewer定位控件的方法和建议(Appium/UIAutomator/Robotium姊妹篇)

最后更新于:2022-04-01 19:56:34

## 1. 背景 在使用MonkeyRunner的时候我们经常会用到Chimchat下面的HierarchyViewer模块来获取目标控件的一些信息来辅助我们测试,但在MonkeyRunner的官网上是没有看到相应的API的描述的,上面只有以下三个类的API引用信息([http://developer.android.com/tools/help/MonkeyDevice.html](http://developer.android.com/tools/help/MonkeyDevice.html)) - MonkeyDevice - MonkeyImage - MonkeyRunner![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755741986.jpg) 所以在这里尝试整理下HierarchyViewer提供的API的用法并根据实践作出相应的建议,首先请看该类提供的所有可用的公共方法,内容并不多: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175575f6ac.jpg) 从图中可以看出HierarchyViewer类中提供的方法主要是用来定位控件相关的,包括根据ID取得控件,根据控件取得控件在屏幕的位置等。但还有一些其他方法,我们会顺带一并描述,毕竟内容并不多。 本文我们依然跟上几篇文章一样以SDK自带的NotePad为实验目标,看怎么定位到NotesList下面的Menu Options中的Add note这个Menu Entry。 以下是通过HierarchyViewer这个工具获得的目标设备界面的截图:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755778f2e.jpg) ## 2.findViewById(String id) ### 2.1 示例 ~~~ targetDevice = MonkeyRunner.waitForConnection() ''' public ViewNode findViewById(String id) * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. ''' viewer = targetDevice.getHierarchyViewer() button = viewer.findViewById('id/title') text = viewer.getText(button) print text.encode('utf-8') ~~~ ~~~ ~~~ ### 2.2 分析和建议 此API的目的就是通过控件的ID来获得代表用户控件的一个ViewNode对象。因为这个是第一个示例,所以这里有几点需要说明 - 一旦MonkeyRunner连接上设备,会立刻获得一个MonkeyDevice的对象代表了目标测试设备,我们就是通过这个设备对象来控制设备的 - 注意这里需要填写的id的格式和UIAutomatorViewer获得ResourceId是不一样的,请看下图UIAutomatorViewer截图中ResourceId前面多出了"android:"字串:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755799a2f.jpg) - 这个方法返回的一个ViewNode的对象,代表目标控件,拥有大量控件相关的属性,由于篇幅问题这里不详述,往后应该会另外撰文描述它的使用。在本文里知道它代表了目标控件就行了 - 最后打印的时候需要转换成UTF-8编码的原因跟Jython默认的编码格式有关系,具体描述和Workaround请查看:[http://www.haogongju.net/art/1636997](http://www.haogongju.net/art/1636997) ## 3. findViewById(String id, ViewNode rootNode) ### 3.1示例 ~~~ ''' public ViewNode findViewById(String id, ViewNode rootNode) * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. ''' iconMenuView = viewer.findViewById('id/icon_menu') button = viewer.findViewById('id/title',iconMenuView) print "Button Text:",text.encode('utf-8') ~~~ ### 3.2分析 这个方法是上面方法的一个重载,除了需要指定ID之外,还需要指定一个rootNode,该rootNode指的就是已知控件的父控件,父到什么层级就没有限制了。为什么需要这个方法了,我们可以想象下这种情况:同一界面上存在两个控件拥有相同的ID,但是他们某一个层级父控件开始发生分叉。那么我们就可以把rootNode指定为该父控件(不包括)到目标控件(不包含)路径中的其中一个父控件来精确定位我们需要的目标控件了。 如我们的示例就是明确指出我们需要的是在父控件“id/icon_menu"(请看背景的hierarchyviewer截图)下面的那个”id/title"控件。 ## 4 getAbsolutePositionOfView(ViewNode node) ### 4.1示例 ~~~ ''' public static Point getAbsoluteCenterOfView(ViewNode node) * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ ''' point = viewer.getAbsoluteCenterOfView(button) print "Button Absolute Center Position:",point ~~~ ### 4.2 分析和建议 这个API的目的是想定位一个已知ViewNode控件的左上角在屏幕上的绝对坐标。对于我们正常的APP里面的控件,本人实践过是没有问题的。但是有一种情况要特别注意:这个对Menu Options下面的控件是无效的! 以上示例最后一段代码的输出是(3,18),其实这里不用想都知道这个不可能是相对屏幕左上角坐标(0,0)的绝对坐标值了,就偏移这一点点像素,你真的当我的实验机器HTC Incredible S是可以植入脑袋的神器啊。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17557bc133.jpg) 那么这个数据是如何获得的呢?其实按照我的理解(真的只是我自己的理解,不对的话就指正吧,但请描述详细点以供我参考),这个函数的定义应该是“获得从最上层的DecorView(具体DectorView的描述请查看我以前转载的一篇文章《[Android DecorView浅析](http://blog.csdn.net/zhubaitian/article/details/39552069)》)左上角坐标到目标控件的的偏移坐标”,只是这个最上层的DecorView的坐标一般都是从(0,0)开始而已。如下图我认为最上面的那个FrameLayout就代表了DecorView,或者说整个窗体 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17557e7eba.jpg) 那么在假设我的观点是对的情况下,这个就很好解析了,请看Menu Option的最上层FrameLayout的绝对坐标是(0,683) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175581712e.jpg) 而Add note的绝对坐标是(3,701) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755838d52.jpg) 两者一相减就是和我们的输出结果绝对吻合的(3,18)了。 ## 5. getAbsoluteCenterOfView(ViewNode node) ### 5.1 示例 ~~~ ''' public static Point getAbsoluteCenterOfView(ViewNode node) * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ ''' point = viewer.getAbsoluteCenterOfView(button) print "Button Absolute Center Position:",point ~~~ ### 5.2 分析和建议 这个方法的目的是获得目标ViewNode控件的中间点的绝对坐标值,但是对Menu Options下面的控件同样不适用,具体请查看第3章节。 以下两个方法都不是用来定位控件的,一并记录下来以供参考。 ## 6. getFocusedWindowName() ### 6.1 示例 ~~~ ''' public String getFocusedWindowName() * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. ''' window = viewer.getFocusedWindowName() print "Window Name:",window.encode('utf-8') ~~~ ### 6.2 解析 其实就是获得当前打开的窗口的packageName/activityName,输出与HierarchyViewer工具检测到的信息一致,所以猜想其用到同样的方法。 输出: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b1755857769.jpg) HierarchyViewer监控信息: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175587dd37.jpg) ## 7. visible(ViewNode node) ### 7.1 示例 ~~~ ''' public boolean visible(ViewNode node) * Gets the visibility of a given element. * @param selector selector for the view. * @return True if the element is visible. ''' isVisible = viewer.visible(button) print "is visible:",isVisible ~~~ 就是查看下控件是否可见,没什么好解析的了。 ## 8. 测试代码 ~~~ from com.android.monkeyrunner import MonkeyRunner,MonkeyDevice from com.android.monkeyrunner.easy import EasyMonkeyDevice,By from com.android.chimpchat.hierarchyviewer import HierarchyViewer from com.android.hierarchyviewerlib.models import ViewNode, Window from java.awt import Point #from com.android.hierarchyviewerlib.device import #Connect to the target targetDevice targetDevice = MonkeyRunner.waitForConnection() easy_device = EasyMonkeyDevice(targetDevice) #touch a button by id would need this targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") #time.sleep(2000) #invoke the menu options MonkeyRunner.sleep(6) targetDevice.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); ''' public ViewNode findViewById(String id) * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. ''' viewer = targetDevice.getHierarchyViewer() button = viewer.findViewById('id/title') text = viewer.getText(button) print text.encode('utf-8') ''' public ViewNode findViewById(String id, ViewNode rootNode) * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. ''' iconMenuView = viewer.findViewById('id/icon_menu') button = viewer.findViewById('id/title',iconMenuView) print "Button Text:",text.encode('utf-8') ''' public String getFocusedWindowName() * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. ''' window = viewer.getFocusedWindowName() print "Window Name:",window.encode('utf-8') ''' public static Point getAbsoluteCenterOfView(ViewNode node) * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ ''' point = viewer.getAbsoluteCenterOfView(button) print "Button Absolute Center Position:",point ''' public static Point getAbsolutePositionOfView(ViewNode node) * Gets the absolute x/y position of the view node. * * @param node view node to find position of. * @return point specifying the x/y position of the node. ''' point = viewer.getAbsolutePositionOfView(button) print "Button Absolute Position:", point ''' public boolean visible(ViewNode node) * Gets the visibility of a given element. * @param selector selector for the view. * @return True if the element is visible. ''' isVisible = viewer.visible(button) print "is visible:",isVisible ~~~ ## 9.附上HierarchyViewer类的源码方便参照 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.chimpchat.hierarchyviewer; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.device.DeviceBridge; import com.android.hierarchyviewerlib.device.ViewServerDevice; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import org.eclipse.swt.graphics.Point; /** * Class for querying the view hierarchy of the device. */ public class HierarchyViewer { public static final String TAG = "hierarchyviewer"; private IDevice mDevice; /** * Constructs the hierarchy viewer for the specified device. * * @param device The Android device to connect to. */ public HierarchyViewer(IDevice device) { this.mDevice = device; setupViewServer(); } private void setupViewServer() { DeviceBridge.setupDeviceForward(mDevice); if (!DeviceBridge.isViewServerRunning(mDevice)) { if (!DeviceBridge.startViewServer(mDevice)) { // TODO: Get rid of this delay. try { Thread.sleep(2000); } catch (InterruptedException e) { } if (!DeviceBridge.startViewServer(mDevice)) { Log.e(TAG, "Unable to debug device " + mDevice); throw new RuntimeException("Could not connect to the view server"); } return; } } DeviceBridge.loadViewServerInfo(mDevice); } /** * Find a view by id. * * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. */ public ViewNode findViewById(String id) { ViewNode rootNode = DeviceBridge.loadWindowData( new Window(new ViewServerDevice(mDevice), "", 0xffffffff)); if (rootNode == null) { throw new RuntimeException("Could not dump view"); } return findViewById(id, rootNode); } /** * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. */ public ViewNode findViewById(String id, ViewNode rootNode) { if (rootNode.id.equals(id)) { return rootNode; } for (ViewNode child : rootNode.children) { ViewNode found = findViewById(id,child); if (found != null) { return found; } } return null; } /** * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. */ public String getFocusedWindowName() { int id = DeviceBridge.getFocusedWindow(mDevice); Window[] windows = DeviceBridge.loadWindows(new ViewServerDevice(mDevice), mDevice); for (Window w : windows) { if (w.getHashCode() == id) return w.getTitle(); } return null; } /** * Gets the absolute x/y position of the view node. * * @param node view node to find position of. * @return point specifying the x/y position of the node. */ public static Point getAbsolutePositionOfView(ViewNode node) { int x = node.left; int y = node.top; ViewNode p = node.parent; while (p != null) { x += p.left - p.scrollX; y += p.top - p.scrollY; p = p.parent; } return new Point(x, y); } /** * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ public static Point getAbsoluteCenterOfView(ViewNode node) { Point point = getAbsolutePositionOfView(node); return new Point( point.x + (node.width / 2), point.y + (node.height / 2)); } /** * Gets the visibility of a given element. * * @param selector selector for the view. * @return True if the element is visible. */ public boolean visible(ViewNode node) { boolean ret = (node != null) && node.namedProperties.containsKey("getVisibility()") && "VISIBLE".equalsIgnoreCase( node.namedProperties.get("getVisibility()").value); return ret; } /** * Gets the text of a given element. * * @param selector selector for the view. * @return the text of the given element. */ public String getText(ViewNode node) { if (node == null) { throw new RuntimeException("Node not found"); } ViewNode.Property textProperty = node.namedProperties.get("text:mText"); if (textProperty == null) { // give it another chance, ICS ViewServer returns mText textProperty = node.namedProperties.get("mText"); if (textProperty == null) { throw new RuntimeException("No text property on node"); } } return textProperty.value; } } ~~~ ## 10. 参考阅读 以下是之前不同框架的控件定位的实践,一并列出来方便直接跳转参考: - [Robotium之Android控件定位实践和建议(Appium/UIAutomator姊妹篇)](http://blog.csdn.net/zhubaitian/article/details/39803857) - [UIAutomator定位Android控件的方法实践和建议(Appium姊妹篇)](http://blog.csdn.net/zhubaitian/article/details/39777951) - [Appium基于安卓的各种FindElement的控件定位方法实践和建议](http://blog.csdn.net/zhubaitian/article/details/39754041)
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyRunner在Windows下的Eclipse开发环境搭建步骤(兼解决网上Jython配置出错的问题)

最后更新于:2022-04-01 19:56:32

网上有一篇shangdong_chu网友写的文章介绍如何在Eclipse上配置MonkeyRunner,做了挺好的一个描述,但经过我的试验在我的环境上碰到了Jython解析器出错的问题,且该文章缺少Pydev安装的步骤,所以这里按照本人的情况从新撰文描述如何在Eclipse上把MonkeyRunner的开发环境搭建起来。 ## 1.环境 首先需要先描述下本人配置的环境,因为不确定其他环境下配置是否会有轻微的差别。 - Eclipse版本 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175559cba0.jpg) - 系统版本 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17555bf184.jpg) - 安卓SDK提供的Jython Jar包版本和路径 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17555dc652.jpg) ## 2. Pydev插件安装 步骤: - 打开Eclipse:Help>>Install New Software... - Work with中输入:http://pydev.org/updates - 如图选择PyDev根据向导进行安装![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175561a7cb.jpg) ## 3. Pydev配置Jython开发环境 这个时候不要急于去安装Jython的Interpreter,因为按照我的经验,这时在安装的过程中我们会碰到以下的错误:"Error:Python stdlib not found or stdlib found without .py files" ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17556422f8.jpg) - Step 1: 找到Jython解析器的Jar包并解压(该jar包应该可以在你的SDK\tools\lib下面找到) - Step 2: 解压后把解压文件夹下面的Lib文件拷贝到与Jython解析器Jar包同一层目录下(注意这一步很重要)![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17556677d7.jpg) - Step 3: 打开Windows>>References并定位到Jython Interpreter![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175569593c.jpg) - Step 4: 点击“New”按钮并选择SDK/Tools/Lib/下面的Jython jar包,然后点击Ok按钮![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17556bc851.jpg) - 安装好后Preferrences的界面应该如下所示,点击Ok完成PyDev的Jython配置![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b17556db113.jpg) ## 4. MonkeyRunner开发环境配置 现在为止我们已经配置好了Jython的开发环境了,我们已经可以创建一个Jython的项目了。但是现在为止我们还没有完成MonkeyRunner的配置,所以现在的Jython项目还不能很方便的开发MonkeyRunner。 其实搭配MonkeyRunner开发环境就是把我们需要的包加入到Jython的PYTHONPATH里面,方便我们直接引用。 - Step 1: 打开Window>>Preferrences - Step 2: 定位到PyDev>>Interpreters>>Jython Interpreter - Step 3: 点击右边的"New Jar/Zips"按钮,开始选择你的SDK下面的\tools\lib下面你需要用到的Jar包,以下是我用到的![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-15_57b175571827f.jpg) - 点击Ok按钮完成MonkeyRunner开发环境配置
 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian


';

MonkeyRunner创建一个Note的实例

最后更新于:2022-04-01 19:56:29

之前的系列给出了Appium,Robotium,Instrumentation和UIAutomator创建一个Note实例的例子: - 《[Appium创建一个Note的实例](http://blog.csdn.net/zhubaitian/article/details/39502069)》 - 《[Robotium创建一个Note的实例](http://blog.csdn.net/zhubaitian/article/details/39502119)》 - 《[UIAutomator创建一个Note的实例](http://blog.csdn.net/zhubaitian/article/details/39508513)》 - 《[SDK Instrumentation创建一个Note的实例](http://blog.csdn.net/zhubaitian/article/details/39546371)》 那么用MonkeyRunner又是如何实现这些功能的呢?今天花了点时间学习了下MonkeyRunner的基本API然后尝试实现了该功能,给我作为一个初学者的感触如下: - MonkeyRunner可以通过坐标点击对象,在引入EasyMonkeyDevice后可以根据ID进行点击 - Eclipse上Jython代码很多对象没有成员函数提示(jar包以导入),如MonkeyRunner.waitForConnection获得device对象后,后面devie.不能自动提示可用成员函数 - 感觉脚本跑得很慢 - 没有任何junit的继承,应该可以通过Junit4框架来使用MonkeyRunner,下次有时间再尝试下 - 也许是不熟悉,感觉跟Robotium,UIAutomator等的编写效率差一些 ~~~ from com.android.monkeyrunner import MonkeyRunner,MonkeyDevice from com.android.monkeyrunner.easy import EasyMonkeyDevice,By #Connect to the target device device = MonkeyRunner.waitForConnection("10000", "emulator-5554") easy_device = EasyMonkeyDevice(device) #touch a button by id would need this device.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList") #time.sleep(2000) #invoke the menu options MonkeyRunner.sleep(3) device.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); #Touch on the "Add note" menu entry by coordinate MonkeyRunner.sleep(3) device.touch(118,253,MonkeyDevice.DOWN_AND_UP) #Type in the text for the note MonkeyRunner.sleep(3) device.type('Note1') #easy_device.type(By.id('id/note'),'Note2') #invoke the menu options MonkeyRunner.sleep(3) device.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP); #Touch on the "save" menu entry by coordinate MonkeyRunner.sleep(3) device.touch(59,257,MonkeyDevice.DOWN_AND_UP) #Simulate long press on the new added note by id with EasyMonkeyDevice MonkeyRunner.sleep(3) easy_device.touch(By.id('id/text1'),MonkeyDevice.DOWN) #Touch down for 10 seconds MonkeyRunner.sleep(10) easy_device.touch(By.id('id/text1'),MonkeyDevice.UP) #Then release the touch #Touch on the "delete" menu entry of the context menu options to delete the note MonkeyRunner.sleep(6) device.touch(84,172,MonkeyDevice.DOWN_AND_UP) ~~~
';

前言

最后更新于:2022-04-01 19:56:27

> 原文出处:[MonkeyRunner从入门到原理](http://blog.csdn.net/column/details/mr-principle-kzhu.html) 作者:[朱佰添](http://blog.csdn.net/zhubaitian) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # MonkeyRunner从入门到原理 > 将自己从零基础接触MonkeyRunner到通过源代码研读学习MonkeyRunner整套框架的过程记录下来分享给大家
';