您好,登錄后才能下訂單哦!
如前文《誰動了我的截圖?--Monkeyrunner takeSnapshot方法源碼跟蹤分析》所述,本文主要會嘗試描述android的自動化測試框架MonkeyRunner究竟是如何和目標設備進行通信的。
在上一篇文章中我們其實已經描述了其中一個方法,就是通過adb協議發送adb服務器請求的方式驅動android設備的adbd守護進程去獲取FrameBuffer的數據生成屏幕截圖。那么MonkeyRunner還會用其他方式和目標設備進行通信嗎?答案是肯定的,且看我們一步步分析道來。
MonkeyRunner和目標設備打交道都是通過ChimpChat層進行封裝分發但最終是在ddmlib進行處理的,其中囊括的方法大體如下:
以下是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’ |
分析之前請大家準備好對應的幾個庫的源碼:
在剖析如何發送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())); /* */ }初始化所做的事情如下
/* */ 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.在繼續之前這里我們先整理下思路,結合上一篇文章,我們看到幾個重要的類的初始化流程是這樣的:
好,那么我們繼續看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; /* */ }這個方法比較長,但大體做的事情如下:
/* */ 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)進行讀寫操作。
通過上一篇文章《誰動了我的截圖?--Monkeyrunner takeSnapshot方法源碼跟蹤分析》的分析,我們知道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<String, String> 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<Device> 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<Device> newList) /* */ { /* 329 */ synchronized () /* */ { /* */ /* */ /* 333 */ ArrayList<Device> 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‘命令來獲得設備屬性的。
但這里遺留了兩個問題
各位看官不用著急,且看我們往下分析,很快就會水落石出了。我們繼續跟蹤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['
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。