UiAutomator源碼分析之擷取控制項資訊,uiautomator源碼
根據上一篇文章《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這個核心的一些類而已。
UiAutomator測試時,點擊一個clickable屬性為false,但父控制項clickable屬性為true的控制項,操作有效?
會相當於點擊父控制項!手動點擊什麼效果應該就是什麼效果
VB中Inet控制項擷取源碼不完整,怎處理
直接給個函數你 控制項也不用了
'============================
'XmlHttp函數
'msgbox getBody("www.baidu.com",["GB2312"|"BIG5"|...])
'============================
Public Function GetBody(ByVal URL$, Optional ByVal Coding$ = "GB2312")
Dim ObjXML
On Error Resume Next
Set ObjXML = CreateObject("Microsoft.XMLHTTP")
With ObjXML
.Open "Get", URL, False, "", ""
.setRequestHeader "If-Modified-Since", "0"
.SEnd
GetBody = .ResponseBody
End With
GetBody = BytesToBstr(GetBody, Coding)
Set ObjXML = Nothing
End Function
Public Function BytesToBstr(strBody, CodeBase)
Dim ObjStream
Set ObjStream = CreateObject("Adodb.Stream")
With ObjStream
.Type = 1
.Mode = 3
.Open
.Write strBody
.Position = 0
.Type = 2
.Charset = CodeBase
BytesToBstr = .ReadText
.Close
End With
Set ObjStream = Nothing
End Function
不清楚你寫的什麼代碼,msgbox 是有字數限制的,我用 Debug.Print全顯示出來了
Debug.Print GetBody("baidu.hexun.com/stock/h.php?code=600488.sh&t=d ")