本文章的目的是通过分析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);
- /* */ }
- 调用MonkeyDevice的成员变量impl的takeSnapshot()函数(往下我们会看impl是怎么传进来的)去获得截图并赋予给IChimpImage的变量
- 把截图转换成MonkeyImage并返回给用户
这里重点是impl这个变量是怎么回事,它是在MonkeyDevice的构造函数中被赋值的:
- public MonkeyDevice(IChimpDevice impl)
- /* */ {
- /* 75 */ this.impl = impl;
- /* */ }
- 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<String> 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<String> paramCollection, Map<String, Object> paramMap, @Nullable String paramString5, int paramInt);
- public abstract void broadcastIntent(@Nullable String paramString1, @Nullable String paramString2, @Nullable String paramString3, @Nullable String paramString4, Collection<String> paramCollection, Map<String, Object> paramMap, @Nullable String paramString5, int paramInt);
- public abstract Map<String, Object> instrument(String paramString, Map<String, Object> paramMap);
- public abstract void wake();
- public abstract Collection<String> getViewIdList();
- public abstract IChimpView getView(ISelector paramISelector);
- public abstract IChimpView getRootView();
- public abstract Collection<IChimpView> getViews(IMultiSelector paramIMultiSelector);
- }
在我们的测试代码中我们很清楚一个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;
- /* */ }
- /* */ public IChimpDevice waitForConnection(long timeoutMs, String deviceId)
- /* */ {
- /* 91 */ return this.mBackend.waitForConnection(timeoutMs, deviceId);
- /* */ }
- /* */ private ChimpChat(IChimpBackend backend)
- /* */ {
- /* 39 */ this.mBackend = backend;
- /* */ }
- /* */ public static ChimpChat getInstance(Map<String, String> 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<String, String> options = new TreeMap();
- /* 64 */ options.put("backend", "adb");
- /* 65 */ return getInstance(options);
- /* */ }
- /* */ private static IChimpBackend createBackendByName(String backendName)
- /* */ {
- /* 77 */ if ("adb".equals(backendName)) {
- /* 78 */ return new AdbBackend(sAdbLocation, sNoInitAdb);
- /* */ }
- /* 80 */ return null;
- /* */ }
到了现在我们终于定位到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;
- /* */ }
经过以上的一大堆描述,最终我们的目的就是确定文章开头的takeSnapshot入口函数所用到的获取截图的device(impl)究竟是什么device,这里我们终于确定了就是ChimChat.jar这个库里面的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又是什么设备,它里面的截图又是怎么回事。
继续分析代码可以看到该device变量也是在AdbChimpDevice的构造函数中进行定义的:
- /* */ public AdbChimpDevice(IDevice device)
- /* */ {
- /* 70 */ this.device = device;
- /* 71 */ this.manager = createManager("127.0.0.1", 12345);
- /* */
- /* 73 */ Preconditions.checkNotNull(this.manager);
- /* */ }
- /* */ 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;
- /* */ }
- /* */ 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<String, String> 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;
- /* */ }
- /* */ 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的wrapper是如何getDevices的,代码跳转到ddmlib这个库里面的AndroidDebugBridge这个class:
- /* */ public IDevice[] getDevices()
- /* */ {
- /* 484 */ synchronized (sLock) {
- /* 485 */ if (this.mDeviceMonitor != null) {
- /* 486 */ return this.mDeviceMonitor.getDevices();
- /* */ }
- /* */ }
- /* */ private DeviceMonitor mDeviceMonitor;
- /* */ Device[] getDevices()
- /* */ {
- /* 131 */ synchronized (this.mDevices) {
- /* 132 */ return (Device[])this.mDevices.toArray(new Device[this.mDevices.size()]);
- /* */ }
- /* */ }
- /* 60 */ private final ArrayList<Device> mDevices = new ArrayList();
最终定位到com.android.ddmlib.Device这个类。
通过这个章节的分析可以看出来在chimpchat里面的AdbChimpDevice其实不是最终负责去获得截图的类,它只是作为一个信息重新包装的再分发的中转站而已。ddmlib才是最终处理我们的截图的库。
通过这个章节的分析可以看出来在chimpchat里面的AdbChimpDevice其实不是最终负责去获得截图的类,它只是作为一个信息重新包装的再分发的中转站而已。ddmlib才是最终处理我们的截图的库。
3. ddmlib库如何通过请求adb服务器读取FrameBuffer设备进行截图
好我们继续看Device这个类究竟是如何获得我们的截图的:
- /* */ public RawImage getScreenshot()
- /* */ throws TimeoutException, AdbCommandRejectedException, IOException
- /* */ {
- /* 558 */ return AdbHelper.getFrameBuffer(AndroidDebugBridge.getSocketAddress(), this);
- /* */ }
好,我们还是继续往下分析,看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协议详细解析请看本人上一篇文章: