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 |
天地会珠海分舵 | 服务号:TechGoGoGo 扫描码: | http://blog.csdn.net/zhubaitian |