Appium Android Bootstrap源碼分析之啟動運行,appiumandroid
通過前面的兩篇文章《Appium Android Bootstrap源碼分析之控制項AndroidElement》和《Appium Android Bootstrap源碼分析之命令解析執行》我們瞭解到了Appium從pc端發送過來的命令是如何定位到命令相關的控制項以及如何解析執行該命令。那麼我們剩下的問題就是bootstrap是怎麼啟動啟動並執行,我們會通過本篇文章的分析來闡述這個問題,以及把之前學習的相關的類給串起來看它們是怎麼互動的。
1.啟動方式Bootstrap的啟動是由Appium從pc端通過adb發送命令來控制的:
從上面的調試資訊我們可以看到AppiumBootstrap.jar是通過uiautomator這個命令作為一個測試包,它指定的測試類別是io.appium.android.bootstrap.Bootstrap這個類。大家如果看了本人之前的文章《UIAutomator源碼分析之啟動和運行》的話應該對uiautomator的啟動原理很熟悉了。
- 啟動命令:uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
那麼我們進入到Bootstrap這個類看下它是怎麼實現的:
public class Bootstrap extends UiAutomatorTestCase { public void testRunServer() { SocketServer server; try { server = new SocketServer(4724); server.listenForever(); } catch (final SocketServerException e) { Logger.error(e.getError()); System.exit(1); } }}從代碼中可以看到,這個類是繼承與UiAutomatorTestCase的,這樣它就能被uiautomator作為測試案例類來執行了。這個類只有一個測試方法testRunServer,所有事情發生的源頭就在這裡:
- 建立一個socket伺服器並監聽4724連接埠,Appium在pc端就是通過串連這麼連接埠來把命令發送過來的
- 迴圈監聽擷取Appium從pc端發送過來的命令資料,然後進行相應的處理
2. 建立socket伺服器並初始化Action到CommandHandler的映射我們先看下SocketServer的建構函式:
public SocketServer(final int port) throws SocketServerException { keepListening = true; executor = new AndroidCommandExecutor(); try { server = new ServerSocket(port); Logger.debug("Socket opened on port " + port); } catch (final IOException e) { throw new SocketServerException( "Could not start socket server listening on " + port); } }它做的第一個事情是先去建立一個AndroidCommandExecutor的執行個體,大家應該還記得上一篇文章說到的這個類裡面儲存了一個靜態很重要的action到命令處理類CommandHandler的執行個體的映射表吧?如果沒有看過的請先去看下。建立好這個靜態映射表之後,建構函式下一步就似乎去建立一個ServerSocket來給Appium從PC端進行串連通訊了。
3.擷取並執行Appium命令資料Bootstrap在建立好socket伺服器後,下一步就是調用SocketServer的listenForever的方法去迴圈讀取處理appium發送出來的命令資料了:
public void listenForever() throws SocketServerException { Logger.debug("Appium Socket Server Ready"); ... try { client = server.accept(); Logger.debug("Client connected"); in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF-8")); while (keepListening) { handleClientData(); } in.close(); out.close(); client.close(); Logger.debug("Closed client connection"); } catch (final IOException e) { throw new SocketServerException("Error when client was trying to connect"); } ...}首先調用server.accept去接受appium的串連請求,串連上後就去初始化用於讀取socket的BufferedReader和BufferredWriter這兩個類的執行個體,最後進入到handleClicentData來進行真正的資料讀取和處理
private void handleClientData() throws SocketServerException { try { input.setLength(0); // clear String res; int a; // (char) -1 is not equal to -1. // ready is checked to ensure the read call doesn't block. while ((a = in.read()) != -1 && in.ready()) { input.append((char) a); } String inputString = input.toString(); Logger.debug("Got data from client: " + inputString); try { AndroidCommand cmd = getCommand(inputString); Logger.debug("Got command of type " + cmd.commandType().toString()); res = runCommand(cmd); Logger.debug("Returning result: " + res); } catch (final CommandTypeException e) { res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage()) .toString(); } catch (final JSONException e) { res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, "Error running and parsing command").toString(); } out.write(res); out.flush(); } catch (final IOException e) { throw new SocketServerException("Error processing data to/from socket (" + e.toString() + ")"); } }
- 通過剛才建立的socket讀取對象去讀取appium發送過來的資料
- 把獲得的的json命令字串發送給getCommand方法來執行個體化我們的AndroidCommand這個類,然後我們就可以通過這個解析器來獲得我們想要的json命令項了
private AndroidCommand getCommand(final String data) throws JSONException, CommandTypeException { return new AndroidCommand(data); }
- 調用runCommand方法來使用我們在第二節構造ServerSocket的時候執行個體化的AndroidComandExecutor對象的execute方法來執行命令,這個命令最終會通過上面的AndroidCommand這個命令解析器的執行個體來獲得appium發送過來的action,然後根據map調用對應的CommandHandler來處理命令。而如果命令是控制項相關的,比如擷取一個控制項的文本資訊GetText,處理命令類又會繼續去AndroidElementHash維護的控制項雜湊表擷取到對應的控制項,然後再通過UiObject把命令發送出去等等..不清楚的請查看上篇文章
private String runCommand(final AndroidCommand cmd) { AndroidCommandResult res; if (cmd.commandType() == AndroidCommandType.SHUTDOWN) { keepListening = false; res = new AndroidCommandResult(WDStatus.SUCCESS, "OK, shutting down"); } else if (cmd.commandType() == AndroidCommandType.ACTION) { try { res = executor.execute(cmd); } ... }
- 通過上面建立的socket寫對象把返回資訊寫到socket發送給appium
4.控制項是如何加入到控制項雜湊表的大家可能奇怪,怎麼整個運行流程都說完了,提到了怎麼去控制項雜湊表擷取一個控制項,但怎麼沒有看到把一個控制項加入到控制項雜湊表呢?其實大家寫指令碼的時候給一個控制項發送click等命令的時候都需要先取找到這個控制項,比如:
WebElement el = driver.findElement(By.name("Add note"));這裡的finElement其實就是一個命令,擷取控制項並存放到控制項雜湊表就是由它對應的CommandHandler實作類別Find來完成的。
可以看到appium過來的命令包含幾項,有我們之間碰到過的,也有沒有碰到過的:
- cmd:指定是一個action
- action:指定這個action是一個find命令
- params
- strategy:指定選擇子的策略是根據空間名name來進行尋找
- selector: 指定選擇子的內容是"Add note"
- context: 指定空間雜湊表中目標控制項的鍵值id,這裡為空白,因為該控制項我們之前沒有用過
- multiple: 表明你指令碼代碼用的是findElements還是findElement,是否要擷取多個控制項
Find重寫父類的execute方法有點長,我們把它breakdown一步一步來看.
- 第一步:獲得控制項的選擇子策略,以便跟著通過該策略來建立uiautomator的UiSelector
public AndroidCommandResult execute(final AndroidCommand command) throws JSONException { final Hashtable<String, Object> params = command.params(); // only makes sense on a device final Strategy strategy; try { strategy = Strategy.fromString((String) params.get("strategy")); } catch (final InvalidStrategyException e) { return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage()); } ...}appium支援的策略有以下幾種,這其實在我們寫指令碼中findElement經常會指定:
public enum Strategy { CLASS_NAME("class name"), CSS_SELECTOR("css selector"), ID("id"), NAME("name"), LINK_TEXT("link text"), PARTIAL_LINK_TEXT("partial link text"), XPATH("xpath"), ACCESSIBILITY_ID("accessibility id"), ANDROID_UIAUTOMATOR("-android uiautomator");
- 第二步:擷取appium發過來的選擇子的其他資訊如內容,控制項雜湊表鍵值,是否是符合選擇子等
public AndroidCommandResult execute(final AndroidCommand command) throws JSONException { final Hashtable<String, Object> params = command.params(); ... final String contextId = (String) params.get("context"); final String text = (String) params.get("selector"); final boolean multiple = (Boolean) params.get("multiple"); ...}
- 第三步,在獲得一樣的選擇子的資訊後,就可以根據該選擇子資訊建立真正的UiSelector選擇子列表了,這裡用列表應該是考慮到今後的複合選擇子的情況,當前我們並沒有用到,整個列表只會有一個UiSelector選擇子
public AndroidCommandResult execute(final AndroidCommand command) throws JSONException { ... try { Object result = null; List<UiSelector> selectors = getSelectors(strategy, text, multiple); ... } ...}
- 第四步:組建好選擇子UiSelector列表後,Find會根據你是findElement還是findElement,也就是說是尋找一個控制項還是多個控制項來尋找控制項,但是無論是多個還是一個,最終都是調用fetchElement這個方法來取尋找的
public AndroidCommandResult execute(final AndroidCommand command) throws JSONException { ... try { Object result = null; List<UiSelector> selectors = getSelectors(strategy, text, multiple); if (!multiple) { for (final UiSelector sel : selectors) { try { Logger.debug("Using: " + sel.toString()); result = fetchElement(sel, contextId); } catch (final ElementNotFoundException ignored) { } if (result != null) { break; } } }else { List<AndroidElement> foundElements = new ArrayList<AndroidElement>(); for (final UiSelector sel : selectors) { // With multiple selectors, we expect that some elements may not // exist. try { Logger.debug("Using: " + sel.toString()); List<AndroidElement> elementsFromSelector = fetchElements(sel, contextId); foundElements.addAll(elementsFromSelector); } catch (final UiObjectNotFoundException ignored) { } } if (strategy == Strategy.ANDROID_UIAUTOMATOR) { foundElements = ElementHelpers.dedupe(foundElements); } result = elementsToJSONArray(foundElements); } ...}而fetchElement最終調用的控制項雜湊表類的getElements:
private ArrayList<AndroidElement> fetchElements(final UiSelector sel, final String contextId) throws UiObjectNotFoundException { return elements.getElements(sel, contextId); }AndroidElementHash的這個方法我們在前一篇文章《Appium Android Bootstrap源碼分析之控制項AndroidElement》已經分析過,我們今天再來溫習一下.從Appium發過來的控制項尋找命令大方向上分兩類:
- 1. 直接基於Appium Driver來尋找,這種情況下appium發過來的json命令是不包含控制項雜湊表的鍵值資訊的
WebElement addNote = driver.findElement(By.name("Add note"));
WebElement el = driver.findElement(By.className("android.widget.ListView")).findElement(By.name("Note1"));以上的指令碼會先嘗試找到Note1這個日記的父控制項ListView,並把這個控制項儲存到控制項雜湊表,然後再根據父控制項的雜湊表鍵值以及子控制項的選擇子找到想要的Note1:
AndroidElementHash的這個getElement命令要做的事情就是針對這兩點來根據不同情況獲得目標控制項的
[java] view plaincopy
- /**
- * Return an elements child given the key (context id), or uses the selector
- * to get the element.
- *
- * @param sel
- * @param key
- * Element id.
- * @return {@link AndroidElement}
- * @throws ElementNotFoundException
- */
- public AndroidElement getElement(final UiSelector sel, final String key)
- throws ElementNotFoundException {
- AndroidElement baseEl;
- baseEl = elements.get(key);
- UiObject el;
-
- if (baseEl == null) {
- el = new UiObject(sel);
- } else {
- try {
- el = baseEl.getChild(sel);
- } catch (final UiObjectNotFoundException e) {
- throw new ElementNotFoundException();
- }
- }
-
- if (el.exists()) {
- return addElement(el);
- } else {
- throw new ElementNotFoundException();
- }
- }
- 如果是第1種情況就直接通過選擇子構建UiObject對象,然後通過addElement把UiObject對象轉換成AndroidElement對象儲存到控制項雜湊表
- 如果是第2種情況就先根據appium傳過來的控制項雜湊表鍵值獲得父控制項,再通過子控制項的選擇子在父控制項的基礎上尋找到目標UiObject控制項,最後跟上面一樣把該控制項通過addElement把UiObject控制項轉換成AndroidElement控制項對象儲存到控制項雜湊表
以下就是把控制項添加到控制項雜湊表的addElement方法
public AndroidElement addElement(final UiObject element) { counter++; final String key = counter.toString(); final AndroidElement el = new AndroidElement(key, element); elements.put(key, el); return el; }
5. 小結
- Appium的bootstrap這個jar包以及裡面的o.appium.android.bootstrap.Bootstrap類是通過uiautomator作為一個uiautomator的測試包和測試方法類啟動起來的
- Bootstrap測試類別繼承於uiautomator可以使用的UiAutomatorTestCase
- bootstrap會啟動一個socket server並監聽來自4724連接埠的appium的串連
- 一旦appium串連上來,bootstrap就會不停的去擷取該連接埠的appium發送過來的命令資料進行解析和執行處理,然後把結果寫到該連接埠返回給appium
- bootstrap擷取到appium過來的json字串命令後,會通過AndroidCommand這個命令解析器解析出命令action,然後通過AndroidCommandExecutor的action到CommandHandler的map把action映射到真正的命令處理類,這些類都是繼承與CommandHandler的實作類別,它們都要重寫該父類的execute方法來最終通過UiObject,UiDevice或反射獲得UiAutomator沒有暴露出來的QueryController/InteractionController來把命令真正的在安卓系統中執行
- appium擷取控制項大概有兩類,一類是直接通過Appium/Android Driver獲得,這一種情況過來的appium尋找json命令字串是沒有帶控制項雜湊表的控制項鍵值的;另外一種是根據控制項的父類控制項在控制項雜湊表中的鍵值和子控制項的選擇子來獲得,這種情況過來的appium尋找json命令字串是既提供了父控制項在控制項雜湊表的鍵值又提供了子控制項的選擇子的
- 一旦擷取到的控制項在控制項雜湊表中不存在,就需要把這個AndroidElement控制項添加到該雜湊表裡面
在eclispse中運行Android程式時啟動tomcat服務親出錯,解釋
原因是出現了null 指標異常,所以程式崩潰了,起不來。
你查看一下你的代碼,是不是調用了Null 物件或者是對象沒有賦值。