依據上一篇文章《UiAutomator源代碼分析之注入事件》開始時提到的計划,這一篇文章我們要分析的是第二點:
- 怎樣獲取控件信息
我們在測試腳本中初始化一個UiObject的時候一般是像下面這個樣子:
UiObject appsTab = new UiObject(new UiSelector().text("Apps")); appsTab.click()那么這個過程發生了什么呢?這就是我們接下來要說的事情了。
1. 獲取控件信息順序圖
這里依舊是一個手畫的不規范的順序圖,描寫敘述了UiObject嘗試獲得一個控件的過程中與相關的類的交互,這些類的關系在《
UiAutomator源代碼分析之UiAutomatorBridge框架》中已經進行了描寫敘述。

這里整一個過程並不復雜,簡單說明下就這幾點:
- UiObject對象幾經周折通過不同的類終於聯系上UiAutomation,然后通知UiAutomation對象它想取得當前活動窗體的全部元素的AccessibilityNodeInfo類型的根節點
- AccessibilityNodeInfo代表了屏幕中控件元素的一個節點。同一時候它也擁有一些成員方法能夠以當前節點為基礎來獲得其它目標節點。
能夠把屏幕上的節點想像成是通過類似xml的格式組織起來的。所以一旦知道根節點和由選擇子UiSelector指定的目標控件信息,我們就能夠遍歷整個窗體控件
- QueryController對象獲得Root Node之后,就是調用tranlateCompoundSelector這種方法來遍歷窗體全部控件。直到找到選擇子UiSelector指定的那個控件為止。
- 注意一個AccessibilityNodeInfo僅僅代表一個控件,遍歷的時候一旦需要下一個控件的信息是必需要再次通過UiAutomation去獲取的。
2.觸發控件查找真正發生的地方
在我沒有去分析uiautomator的源碼之前。我一直以為空間查找是在通過UiSelector初始化一個UiObject的時候發生的:
UiObject appsTab = new UiObject(new UiSelector().text("Apps"));
這讓我有一種先入為主的感覺,一個控件對象初始化好后應該就已經得到了該控件所代表的節點的全部信息了。但看了源代碼后發現事實並不是如此,以上所做的事情僅僅是以一定的格式准備好UiSelector選擇子而已。真正觸發uiautomator去獲取控件節點信息的是在觸發控件事件的時候。比方:
appsTab.click()我們進入到代表一個控件的UiObject相應的操作控件的方法去看下就清楚了,以上面的click為例:
/* */ public boolean click() /* */ throws UiObjectNotFoundException /* */ { /* 389 */ Tracer.trace(new Object[0]); /* 390 */ AccessibilityNodeInfo node = findAccessibilityNodeInfo(this.mConfig.getWaitForSelectorTimeout()); /* 391 */ if (node == null) { /* 392 */ throw new UiObjectNotFoundException(getSelector().toString()); /* */ } /* 394 */ Rect rect = getVisibleBounds(node); /* 395 */ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), this.mConfig.getActionAcknowledgmentTimeout()); /* */ }正式290行的調用觸發uiautomator去調用UiAutomation去獲取到我們想要的控件節點AccessibilityNodeInfo信息的。
3.獲得根節點
以下我們看下uiautomator是怎么去獲取到代表窗體全部控件的根的Root Node的,我們進入UiObject的findAccessibilityNodeInfo這種方法:
/* */ protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout) /* */ { /* 164 */ AccessibilityNodeInfo node = null; /* 165 */ long startMills = SystemClock.uptimeMillis(); /* 166 */ long currentMills = 0L; /* 167 */ while (currentMills <= timeout) { /* 168 */ node = getQueryController().findAccessibilityNodeInfo(getSelector()); /* 169 */ if (node != null) { /* */ break; /* */ } /* */ /* 173 */ UiDevice.getInstance().runWatchers(); /* */ /* 175 */ currentMills = SystemClock.uptimeMillis() - startMills; /* 176 */ if (timeout > 0L) { /* 177 */ SystemClock.sleep(1000L); /* */ } /* */ } /* 180 */ return node; /* */ }UiObject對象會首先去獲得一個QueryController對象,然后調用該對象的findAccessibilityNodeInfo同名方法:
/* */ protected AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector, boolean isCounting) /* */ { /* 143 */ this.mUiAutomatorBridge.waitForIdle(); /* 144 */ initializeNewSearch(); /* */ /* 146 */ if (DEBUG) { /* 147 */ Log.d(LOG_TAG, "Searching: " + selector); /* */ } /* 149 */ synchronized (this.mLock) { /* 150 */ AccessibilityNodeInfo rootNode = getRootNode(); /* 151 */ if (rootNode == null) { /* 152 */ Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search"); /* 153 */ return null; /* */ } /* */ /* */ /* 157 */ UiSelector uiSelector = new UiSelector(selector); /* 158 */ return translateCompoundSelector(uiSelector, rootNode, isCounting); /* */ } /* */ }這里做了兩個重要的事情:
- 150行:通過調用getRootNode來獲得根節點,這個就是我們這個章節的重點
- 158行:通過調用translateCompoundSelector來依據用戶指定的UiSelector格式從上面獲得根節點開始遍歷窗體控件樹,以獲得我們的目標控件
好。我們繼續往下進入getRootNode:
/* */ protected AccessibilityNodeInfo getRootNode() /* */ { /* 168 */ int maxRetry = 4; /* 169 */ long waitInterval = 250L; /* 170 */ AccessibilityNodeInfo rootNode = null; /* 171 */ for (int x = 0; x < 4; x++) { /* 172 */ rootNode = this.mUiAutomatorBridge.getRootInActiveWindow(); /* 173 */ if (rootNode != null) { /* 174 */ return rootNode; /* */ } /* 176 */ if (x < 3) { /* 177 */ Log.e(LOG_TAG, "Got null root node from accessibility - Retrying..."); /* 178 */ SystemClock.sleep(250L); /* */ } /* */ } /* 181 */ return rootNode; /* */ }172調用的是UiAutomatorBridge對象的方法,通過我們上面的幾篇文章我們知道UiAutomatorBridge提供的方法大部分都是直接調用UiAutomation的方法的,我們進去看看是否如此:
/* */ public AccessibilityNodeInfo getRootInActiveWindow() { /* 66 */ return this.mUiAutomation.getRootInActiveWindow(); /* */ }果不其然,終於簡單明了的直接調用UiAutomation的getRootInActiveWindow來獲得根AccessibilityNodeInfo.
4.遍歷根節點獲得選擇子UiSelector指定的控件
如前所述,QueryController的方法findAccessibilityNodeInfo在獲得根節點后下來做的第二個事情:- 158行:通過調用translateCompoundSelector來依據用戶指定的UiSelector格式從上面獲得根節點開始遍歷窗體控件樹,以獲得我們的目標控件
里面的算法細節我就不打算去研究了,里面考慮到選擇子嵌套的情況,分析起來也比較費力,且了解了它的算法對我去立交uiautomator的執行原理並沒有很大的幫助,我僅僅須要知道給定一棵樹的根,然后制定了我想要的葉子的屬性,那么我遍歷整棵樹肯定是能夠找到我想要的那個/些滿足要求的控件的。大家由興趣了解其算法的話還是自行去研究吧。
5.終於還是通過坐標點來點擊控件
上面UiObject的Click方法通過UiAutomation這個高大上的新框架獲得了代表我們目標控件的AccessibilityNodeInfo后,跟着是不是就直接調用這個節點的Click方法進行點擊了呢?事實上不是的,首先AccessibilityNodeInfo並沒有click這種方法,我們繼續看代碼:/* */ public boolean click() /* */ throws UiObjectNotFoundException /* */ { /* 389 */ Tracer.trace(new Object[0]); /* 390 */ AccessibilityNodeInfo node = findAccessibilityNodeInfo(this.mConfig.getWaitForSelectorTimeout()); /* 391 */ if (node == null) { /* 392 */ throw new UiObjectNotFoundException(getSelector().toString()); /* */ } /* 394 */ Rect rect = getVisibleBounds(node); /* 395 */ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), this.mConfig.getActionAcknowledgmentTimeout()); /* */ }從395行能夠看到,終於還是把控件節點的信息轉換成控件的坐標點進行點擊的。至於怎么點擊。大家能夠參照上一篇文章。無非就是通過建立一個runnable的線程進行點擊事件的注入了
6.系列結語
UiAutomator源代碼分析這個系列到了這篇文章算是完結了,從啟動執行,到核心的UiAutomatorBridge架構。到實例解剖,通過這些文章我相信大家已經非常清楚uiautomator這個運用了UiAutomation框架與AccessibilityService通信的測試框架是怎么回事了。置於uiautomator那5個專供測試用例調用的類是怎么回事。網上可獲得的信息不少。我這里就沒有必要做從新造輪子的事情了,況且這些已經不是uiautomator這個框架的核心了,它們僅僅是運用了UiAutomatorBridge這個核心的一些類而已。
作者 |
自主博客 |
微信 |
CSDN |
天地會珠海分舵 |
|
服務號:TechGoGoGo 掃描碼: |
http://blog.csdn.net/zhubaitian |