溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Appium Android Bootstrap源碼分析之啟動運(yùn)行

發(fā)布時間:2020-07-17 07:57:20 來源:網(wǎng)絡(luò) 閱讀:507 作者:zhukev 欄目:移動開發(fā)

通過前面的兩篇文章《Appium Android Bootstrap源碼分析之控件AndroidElement》和《Appium Android Bootstrap源碼分析之命令解析執(zhí)行》我們了解到了Appium從pc端發(fā)送過來的命令是如何定位到命令相關(guān)的控件以及如何解析執(zhí)行該命令。那么我們剩下的問題就是bootstrap是怎么啟動運(yùn)行的,我們會通過本篇文章的分析來闡述這個問題,以及把之前學(xué)習(xí)的相關(guān)的類給串起來看它們是怎么互動的。


1.啟動方式

Bootstrap的啟動是由Appium從pc端通過adb發(fā)送命令來控制的:
Appium Android Bootstrap源碼分析之啟動運(yùn)行
從上面的調(diào)試信息我們可以看到AppiumBootstrap.jar是通過uiautomator這個命令作為一個測試包,它指定的測試類是io.appium.android.bootstrap.Bootstrap這個類。大家如果看了本人之前的文章《UIAutomator源碼分析之啟動和運(yùn)行》的話應(yīng)該對uiautomator的啟動原理很熟悉了。
  • 啟動命令:uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
那么我們進(jìn)入到Bootstrap這個類看下它是怎么實(shí)現(xiàn)的:
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作為測試用例類來執(zhí)行了。
這個類只有一個測試方法testRunServer,所有事情發(fā)生的源頭就在這里:
  • 創(chuàng)建一個socket服務(wù)器并監(jiān)聽4724端口,Appium在pc端就是通過連接這么端口來把命令發(fā)送過來的
  • 循環(huán)監(jiān)聽獲取Appium從pc端發(fā)送過來的命令數(shù)據(jù),然后進(jìn)行相應(yīng)的處理

2. 創(chuàng)建socket服務(wù)器并初始化Action到CommandHandler的映射

我們先看下SocketServer的構(gòu)造函數(shù):
  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);     }    }
它做的第一個事情是先去創(chuàng)建一個AndroidCommandExecutor的實(shí)例,大家應(yīng)該還記得上一篇文章說到的這個類里面保存了一個靜態(tài)的很重要的action到命令處理類CommandHandler的實(shí)例的映射表吧?如果沒有看過的請先去看下。
建立好這個靜態(tài)映射表之后,構(gòu)造函數(shù)下一步就似乎去創(chuàng)建一個ServerSocket來給Appium從PC端進(jìn)行連接通信了。

3.獲取并執(zhí)行Appium命令數(shù)據(jù)

Bootstrap在創(chuàng)建好socket服務(wù)器后,下一步就是調(diào)用SocketServer的listenForever的方法去循環(huán)讀取處理appium發(fā)送出來的命令數(shù)據(jù)了:
  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");     }     ... }
首先調(diào)用server.accept去接受appium的連接請求,連接上后就去初始化用于讀取socket的BufferedReader和BufferredWriter這兩個類的實(shí)例,最后進(jìn)入到handleClicentData來進(jìn)行真正的數(shù)據(jù)讀取和處理
 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發(fā)送過來的數(shù)據(jù)
  • 把獲得的的json命令字串發(fā)送給getCommand方法來實(shí)例化我們的AndroidCommand這個類,然后我們就可以通過這個解析器來獲得我們想要的json命令項(xiàng)了
  private AndroidCommand getCommand(final String data) throws JSONException,       CommandTypeException {     return new AndroidCommand(data);   }
  • 調(diào)用runCommand方法來使用我們在第二節(jié)構(gòu)造ServerSocket的時候?qū)嵗腁ndroidComandExecutor對象的execute方法來執(zhí)行命令,這個命令最終會通過上面的AndroidCommand這個命令解析器的實(shí)例來獲得appium發(fā)送過來的action,然后根據(jù)map調(diào)用對應(yīng)的CommandHandler來處理命令。而如果命令是控件相關(guān)的,比如獲取一個控件的文本信息GetText,處理命令類又會繼續(xù)去AndroidElementHash維護(hù)的控件哈希表獲取到對應(yīng)的控件,然后再通過UiObject把命令發(fā)送出去等等..不清楚的請查看上篇文章
      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發(fā)送給appium

4.控件是如何加入到控件哈希表的

大家可能奇怪,怎么整個運(yùn)行流程都說完了,提到了怎么去控件哈希表獲取一個控件,但怎么沒有看到把一個控件加入到控件哈希表呢?其實(shí)大家寫腳本的時候給一個控件發(fā)送click等命令的時候都需要先取找到這個控件,比如:
WebElement el = driver.findElement(By.name("Add note"));
這里的finElement其實(shí)就是一個命令,獲取控件并存放到控件哈希表就是由它對應(yīng)的CommandHandler實(shí)現(xiàn)類Find來完成的。
Appium Android Bootstrap源碼分析之啟動運(yùn)行
可以看到appium過來的命令包含幾項(xiàng),有我們之間碰到過的,也有沒有碰到過的:
  • cmd:指定是一個action
  • action:指定這個action是一個find命令
  • params
    • strategy:指定選擇子的策略是根據(jù)空間名name來進(jìn)行查找
    • selector: 指定選擇子的內(nèi)容是"Add note"
    • context: 指定空間哈希表中目標(biāo)控件的鍵值id,這里為空,因?yàn)樵摽丶覀冎皼]有用過
    • multiple: 表明你腳本代碼用的是findElements還是findElement,是否要獲取多個控件
Find重寫父類的execute方法有點(diǎn)長,我們把它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支持的策略有以下幾種,這其實(shí)在我們寫腳本中findElement經(jīng)常會指定:
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發(fā)過來的選擇子的其他信息如內(nèi)容,控件哈希表鍵值,是否是符合選擇子等
  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");    ... }
  • 第三步,在獲得一樣的選擇子的信息后,就可以根據(jù)該選擇子信息建立真正的UiSelector選擇子列表了,這里用列表應(yīng)該是考慮到今后的復(fù)合選擇子的情況,當(dāng)前我們并沒有用到,整個列表只會有一個UiSelector選擇子
  public AndroidCommandResult execute(final AndroidCommand command)       throws JSONException {    ...     try {       Object result = null;       List<UiSelector> selectors = getSelectors(strategy, text, multiple);        ...       }     ... }
  • 第四步:組建好選擇子UiSelector列表后,F(xiàn)ind會根據(jù)你是findElement還是findElement,也就是說是查找一個控件還是多個控件來查找控件,但是無論是多個還是一個,最終都是調(diào)用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最終調(diào)用的控件哈希表類的getElements:
  private ArrayList<AndroidElement> fetchElements(final UiSelector sel, final String contextId)       throws UiObjectNotFoundException {      return elements.getElements(sel, contextId);   }
AndroidElementHash的這個方法我們在前一篇文章《Appium Android Bootstrap源碼分析之控件AndroidElement》已經(jīng)分析過,我們今天再來溫習(xí)一下.
從Appium發(fā)過來的控件查找命令大方向上分兩類:
  • 1. 直接基于Appium Driver來查找,這種情況下appium發(fā)過來的json命令是不包含控件哈希表的鍵值信息的
WebElement addNote = driver.findElement(By.name("Add note"));
  • 2. 基于父控件查找:
WebElement el = driver.findElement(By.className("android.widget.ListView")).findElement(By.name("Note1"));
以上的腳本會先嘗試找到Note1這個日記的父控件ListView,并把這個控件保存到控件哈希表,然后再根據(jù)父控件的哈希表鍵值以及子控件的選擇子找到想要的Note1:
Appium Android Bootstrap源碼分析之啟動運(yùn)行
AndroidElementHash的這個getElement命令要做的事情就是針對這兩點(diǎn)來根據(jù)不同情況獲得目標(biāo)控件的
[java] view plaincopy
  1. /** 
  2.  * Return an elements child given the key (context id), or uses the selector 
  3.  * to get the element. 
  4.  *  
  5.  * @param sel 
  6.  * @param key 
  7.  *          Element id. 
  8.  * @return {@link AndroidElement} 
  9.  * @throws ElementNotFoundException 
  10.  */  
  11. public AndroidElement getElement(final UiSelector sel, final String key)  
  12.     throws ElementNotFoundException {  
  13.   AndroidElement baseEl;  
  14.   baseEl = elements.get(key);  
  15.   UiObject el;  
  16.   
  17.   if (baseEl == null) {  
  18.     el = new UiObject(sel);  
  19.   } else {  
  20.     try {  
  21.       el = baseEl.getChild(sel);  
  22.     } catch (final UiObjectNotFoundException e) {  
  23.       throw new ElementNotFoundException();  
  24.     }  
  25.   }  
  26.   
  27.   if (el.exists()) {  
  28.     return addElement(el);  
  29.   } else {  
  30.     throw new ElementNotFoundException();  
  31.   }  
  32. }  
  • 如果是第1種情況就直接通過選擇子構(gòu)建UiObject對象,然后通過addElement把UiObject對象轉(zhuǎn)換成AndroidElement對象保存到控件哈希表
  • 如果是第2種情況就先根據(jù)appium傳過來的控件哈希表鍵值獲得父控件,再通過子控件的選擇子在父控件的基礎(chǔ)上查找到目標(biāo)UiObject控件,最后跟上面一樣把該控件通過addElement把UiObject控件轉(zhuǎn)換成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. 小結(jié)

  • Appium的bootstrap這個jar包以及里面的o.appium.android.bootstrap.Bootstrap類是通過uiautomator作為一個uiautomator的測試包和測試方法類啟動起來的
  • Bootstrap測試類繼承于uiautomator可以使用的UiAutomatorTestCase
  • bootstrap會啟動一個socket server并監(jiān)聽來自4724端口的appium的連接
  • 一旦appium連接上來,bootstrap就會不停的去獲取該端口的appium發(fā)送過來的命令數(shù)據(jù)進(jìn)行解析和執(zhí)行處理,然后把結(jié)果寫到該端口返回給appium
  • bootstrap獲取到appium過來的json字串命令后,會通過AndroidCommand這個命令解析器解析出命令action,然后通過AndroidCommandExecutor的action到CommandHandler的map把a(bǔ)ction映射到真正的命令處理類,這些類都是繼承與CommandHandler的實(shí)現(xiàn)類,它們都要重寫該父類的execute方法來最終通過UiObject,UiDevice或反射獲得UiAutomator沒有暴露出來的QueryController/InteractionController來把命令真正的在安卓系統(tǒng)中執(zhí)行
  • appium獲取控件大概有兩類,一類是直接通過Appium/Android Driver獲得,這一種情況過來的appium查找json命令字串是沒有帶控件哈希表的控件鍵值的;另外一種是根據(jù)控件的父類控件在控件哈希表中的鍵值和子控件的選擇子來獲得,這種情況過來的appium查找json命令字串是既提供了父控件在控件哈希表的鍵值又提供了子控件的選擇子的
  • 一旦獲取到的控件在控件哈希表中不存在,就需要把這個AndroidElement控件添加到該哈希表里面

        1.  

          作者

          自主博客

          微信

          CSDN

          天地會珠海分舵

          http://techgogogo.com


          服務(wù)號:TechGoGoGo

          掃描碼:

          Appium Android Bootstrap源碼分析之啟動運(yùn)行

          向AI問一下細(xì)節(jié)

          免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

          AI