UiAutomator2 原理介紹 + 原始碼走讀

slink發表於2023-09-05

前言

如果 TesterHome 觀看不方便(目錄不方便),可以去:https://wenjie.store/archives/uiautomator2 看;TesterHome 有些格式問題可能還沒來得及改

  • 看完本篇部落格你可能會了解以下事情:
    • uiautomator2 原理基本介紹(常見的 uiautomator2 init 命令、預設的點選方案&坑)
    • UI 自動化穩定性問題解決(偽解決),適用場景,不適用怎麼辦(對比百度、位元組、面試公司撈到的一些資訊)
    • 或許可以幫你定位框架偶現的一些問題

PS:文中的物件 hash 如果出現上下文不一致的情況不要見怪,因為 usb 線有些不穩定,重新除錯時物件 hash 就會發生變化


uiautomator2 運作鏈路

  • 首先你需要知道構成 uiautomator2 整體運作的倉庫其實總共有三個:
    • python client 層:https://github.com/openatx/uiautomator2
    • Android server 層:https://github.com/openatx/atx-agent
    • Android App+Server 驅動:https://github.com/openatx/android-uiautomator-server
    • 當然了,還有一些第三方庫,比如:minicap、minitouch
  • 其中 android-uiautomator-server 雖然是一個倉庫,但實際打出來的包是兩個,從 github ci 的指令碼中也可以看出來這一點,如下圖所示:
  • 指令碼構建了兩個包
    • 兩個 APK 作用各有不同,不過自動化控制元件 dump、預設點選操作這些都是在 test 字尾的 apk 中
  • 整體運作鏈路如下圖所示:
  • uiautomator2運作鏈路
  • 基本上整篇文章都是圍繞這個呼叫鏈路進行除錯 + 講解,現在沒看懂也不要緊,跟著程式碼走一遍就清楚了,接下來看看常用的一些介面都做了什麼。

uiautomator2 init 做了什麼

  • 要測試這個功能,只需要給__main__.py加上init的啟動引數即可,如下圖所示:
  • ide啟動引數
  • 在進入cmd_init前,我們可以看下傳進來的預設引數,如下圖所示:
  • cmd_init預設傳參
    • 需要注意的其實只有預設傳的--addr 127.0.0.1:7912,這個並非在 python 層使用的,而是後續傳給 atx-agent 作為啟動引數使用
  • 接下來正式進入cmd_init函式了,可以看到一開始如果沒有指定裝置序列號的話,會自動遍歷所有裝置並初始化:
  • init不帶引數是初始化所有裝置
  • 我們繼續跟進install函式,核心邏輯如下圖所示:
  • install核心邏輯
  • install的邏輯其實有很多是重合的,下面只挑一些有差異的點來看
    • 下載邏輯
      • 首先無論你在國內外,下載連結都會被暴力替換成映象地址(我一開始還以為有啥別的判斷,結果沒有),程式碼如下圖所示:
      • 下載地址替換成國內映象
      • cache_download 就是先判斷快取有沒有已經下載的,有的話就判斷檔案資訊是否正確,正確就直接拿來用,這裡不再展開有興趣的可以自己看看
      • 後續的 minicap、atx-agent、2 個 apk 都是透過這樣的方式下載
      • 下載完後都會被 push 到/data/local/tmp/目錄下,程式碼如下圖所示:
      • push目錄
    • apk 版本校驗
      • 主要就是校驗了版本號、簽名,還有類似安裝時間的警告,程式碼如下:
      • apk校驗邏輯
      • apk 的資訊則是透過pm pathdumpsys package獲取的,下面展示部分程式碼:
      • apk資訊獲取
    • apk 安裝
      • 因為包含了 debug 的包,所以會加-t的引數,如下圖所示:
      • debug包需要加-t引數
      • 需要注意的是這裡會解除安裝掉原來的 APP 的,而 minicap、minitouch、atx-agent 不會刪除,個人猜測是因為 app 有的情況下無法覆蓋安裝,需要先解除安裝,而可執行檔案沒有這個問題。
    • atx-agent 啟動
      • 這裡只簡單看下啟動引數的含義(golang 程式碼中都有其含義),程式碼如下圖所示:
      • atx-agent啟動邏輯
      • server:表示啟動 atx-agent 內建的 server
      • --nouia:帶上此參數列示啟動 atx-agent 時,不要把 uiautomator 也拉起來
      • -d:表示後臺執行
      • --addr:指定監聽的ip:port
      • 埠對映
        • 上面的 atx-agent 雖然啟動完成了,atx-agent 也能獲取到手機的 ip,那麼後續 python client 直接使用ip:7912請求就完了?實際上並沒有,中間還進行了一次埠對映,python client 使用的其實是對映後的埠
        • 說白了就是執行了一次adb forward tcp:本地電腦隨機埠 tcp:7912,這個命令很好理解,比如{本地電腦隨機埠}是 8080,那麼你請求127.0.0.1:8080就等於在請求手機ip:7912:,程式碼實現如下:
        • 埠對映
    • uiautomator 啟動方式
      • 如果你看過 APP 層的程式碼,你一定會很疑惑為什麼會有一堆程式碼放在androidTest下,並且還引入了 JUnit 框架,如下圖所示:
      • 引入Junit
      • 實際上當我們去看 atx-agent 啟動邏輯就明白了,假設我們啟動的時候去掉--nouia引數,就表示啟動 atx-agent 時也啟動 uiautomator 服務,此時 golang 程式碼中fNoUiautomator的值為 false,如下圖所示:
      • fNoUiautomator為false
      • 之後的邏輯中會新增一個啟動 uiautomator 的任務程式碼,就是透過am instrument啟動單元測試的命令列,如下圖所示:
      • am instrument命令列
      • 上面的程式碼只是新增了一個任務,實際上還沒執行,到後面判斷!*fNoUiautomator為 True 時才會執行,如下圖所示:
      • 真正執行的位置
      • 此時如果你嘗試用 kill 命令殺掉 uiautomator 程序,會殺不掉:
      • uiautomator殺不掉
      • 難道這是am instrument的神奇力量嗎?並不是,實際上是因為 atx-agent 使用 goroutine 寫了個死迴圈佔有程序,要退出迴圈釋放程序的話只能自己傳入中斷引數,最後還是使用 kill 命令殺掉程序的,這會在後面的【重置 uiautomator_v2 如何進行】處講到。
  • 如何 debug 在 Android 中的 golang 程式碼我之後補充

adb forward 的好處

這部分比較偏向猜想,覺得不對歡迎補充

  • 先說一個東西,叫內網穿透,你可以試著訪問這個頁面(隨緣線上):https://wenjie.store/chat/,如果成功的話說明你可以間接使用我 4090 的算力了
  • 說白了我就是使用內網穿透使得你可以透過一個公網的伺服器訪問到我本地的物理主機,比如上面的連結,你實際上能訪問到的是我在自己電腦部署的 ChatGLM2(這東西總不能是一臺 1c1g 的電腦能跑起來的)
  • OK,這時候問題來了,既然我可以透過內網穿透訪問到電腦主機,那麼手機是不是也可以?答案是肯定的

以下操作看不懂就 SKIP 吧,你只需要知道能透過外網 adb 連線手機即可

  • 我們可以做一做實驗,先來對 Android 手機進行內網穿透,大概就是下面這個樣子
  • 我的小米手機先插上一臺 Ubuntu,使用 adb shell 啟動 atx-agent,便於之後有東西可以訪問
  • 老牌手機 adb connect 的埠預設是 5555,但目前我的小米比較奇葩要手動開啟無線模式才可用 adb connect 連線,且埠不為 5555+ 每次開關都會變:
  • 小米wifi埠
    • 小米的 wifi 連線只支援已配對的裝置,沒配對的裝置是連不上去的,內網穿透也一樣
  • 不管如何,對這個埠做對映即可(伺服器的埠也記得開):
  • 內網穿透埠配置
  • 手機啟動內網穿透:
  • 手機啟動
  • 然後試著在另一臺 win 電腦上使用 adb connect 連線,可以看到使用外網訪問完全沒問題:
  • 外網adb connect

  • 牛逼的就要來了,我在 win 電腦上執行adb forward,具體命令如下圖所示:
  • adb forward
  • 之後我可以透過 win 電腦瀏覽器輸入localhost:8888/info就訪問到手機上執行的 atx-agent 服務了,如下圖所示:
  • win電腦訪問atx-agent
  • 到這裡相信adb forward的好處已經體現出來了,假如你的裝置是透過某種代理的手段(如內網穿透)開放出來的,那麼 uiautomator 預設獲取的網路卡 IP 就只是內網 IP,如果你不在這內網之中而是透過代理手段訪問的,那返回給你的內網的 IP 你是肯定無法訪問的
  • adb forward的強大之處就在於它不會出現獲取錯 IP 這種情況,並且我上面的操作中,雲端無論是 8888 埠還是 7912 埠的防火牆都是開著的(生效著的),這還意味著adb forward本身能透過長連線繞過一些規則

PS:你可能會說我都知道adb connect的 ip 和 port 了,那我直接訪問不就完了?如果你問出這個問題,那你可能還沒完全理解上面的意思。在知道遠端手機 ip:port 的情況下,如果直接使用ip:7912/info訪問,是必須要開啟防火牆 7912 埠的,而我上面使用adb forward根本就沒開啟。

  • OK,到此為止uiautomator2 init指令的流程就基本解釋清楚了,uiautomator2 stop就不多說了,有個意料之外的地方在於它沒有停下 atx-agent。
  • 下面的篇幅基本就是看一些常見函式的呼叫鏈路了,上面一不小心費了點口水導致開頭的流程圖還沒用上,下面應該就開始對上了。

click 流程

  • 注意講的這裡是執行uiautomator2 purge後,再執行如下程式碼走的click邏輯:
import uiautomator2 as u2

if __name__ == '__main__':

    d = u2.connect_usb(serial="af80d1e4") # connect to device
    d(text="首頁").click(timeout=3)

關於 u2.connect_usb 就不過多講解了,返回的 Devices 物件裡面由多個父類介面組合而成,click 函式也是眾多父類的實現之一


d(text="首頁") 做了什麼

  • d(text="首頁")其實只做了一些包裝物件的工作,但如果你在這之前執行過 UI 自動化,你會發現此時有些引數怪怪的,即便你之後執行了uiautomator2 purge把東西都解除安裝乾淨了,接下來就一步步去看
  • 首先是d的初始化,實際上就是包裝了一個 UIObject,而傳進去的 Selector 其實也只是一層引數包裝:
  • Selector構造
  • UIObject 就是對 session、selector、jsonrpc 包裝,咋看之下好像沒啥問題,但當你檢視session.address屬性時,你會發現已經存在 ip 埠了:
  • 已經有agent的ip+port了
  • 而此時你在手機裡試圖尋找 atx-agent 的程序,會發現並不存在(如果存在可能是你訪問了其它屬性):
  • atx-agent不存在

上面的 session 不要在斷點時展開所有屬性,否則你會發現展開得很慢,因為有些屬性是透過請求 atx-agent 獲取的,而發現 atx-agent 程序不在時,就會自動拉起,正常的啟動邏輯不是這樣的。而只獲取 address 屬性不會有這個問題。

  • 在這裡我直接先說結論,之所以 atx-agent 不存在就有 ip+port,是因為 uiautomator 的邏輯裡面會直接複用之前轉發到手機端 7912 的埠,後續 atx-agent 是固定死 7912 埠的所以不會有問題
  • 而先前說的uiautomator2 purge只是解除安裝 APP+ 可執行檔案,並沒有刪除埠轉發,我們可以使用adb forward --list檢視已存在的埠對映,會發現正好等於上面獲取到的 port:
  • adb forward --list
  • 我們可以持續跟進 address 屬性的獲取邏輯,看看是不是這樣:
  • address
  • _get_atx_agent_url
  • forward_port
  • 在前面uiautomator2 init的流程中是先啟動 atx-agent server 再進行埠對映的,但實際上先進行埠對映也沒關係,因為 atx-agent server 的埠固定 7912,只要保證 jsonrpc 請求前對映到就行。

click(timeout=3) 做了什麼

  • 在正式 debug 程式碼前,我先說明一些環境問題,比如你剛進入 click 的斷點時,會發現控制檯的物件一直在載入(前提是你前面的步驟沒有誤啟動過 atx-agent,且之後執行uiautomator purge清理),像下面這樣這樣:
  • 載入屬性
  • 當載入完成後,會發現手機上 atx-agent 也啟動了:
  • 屬性載入完成
  • atx-agent也被啟動了
  • 那有沒有辦法不讓它成功啟動 atx-agent 呢?有,我目前只想出一個愚鈍的方法,那就是不停地刪除,在 PC 端執行如下指令碼:
while true
do
    adb shell rm -rf /data/local/tmp/atx-agent
    sleep 0.01
done
  • 直接貼進 PC 命令列視窗,然後回車就行(如果還是成功啟動就把 sleep 那行刪掉),如下圖所示:
  • 執行刪除命令
  • 這樣即便 debug 模式中因為特有的屬性訪問而導致嘗試拉起 atx-agent,也可以在下載後、啟動前刪除,不過記得在真正啟動 atx-agent 之前終止停止命令(ctrl+c 即可)

  • 我們先進入 click 中的第一個函式must_wait,這個函式預設就是在規定時間內看指定元素是否存在,程式碼如下:
  • must_wait
  • 我們繼續跟進上面的wait函式,會發現裡面其實是 jsonrpc 的呼叫:
  • wait函式
  • 但不要忘了,我們正常流程下 agx-agent 還沒啟動呢,所以繼續要繼續深入 jsonrpc 的邏輯,在除錯的過程中有一段程式碼可能會讓你產生誤解,如下圖所示:
  • 可能產生誤解的程式碼
  • 繼續看_AgentRequestSession#request的實現,終於發現初始化 atx-agent 的程式碼了:
  • image-1693812095696
  • 到這裡為止就可以停止先前執行的迴圈刪除 atx-agent 的指令碼了,至於_prepare_atx_agent的執行邏輯我想應該不用多說太多,最終還是會執行到前面uiautomator2 init提到的setup_atx_agent函式,所以啟動引數啥的都是一樣的,呼叫棧如下圖所示:
  • _prepare_atx_agent呼叫棧
  • 之後就是真正的去請求了,只不過還是會請求失敗,失敗的原因我們可以看下 golang 的程式碼(是 debug 手機的 atx-agent,非本地的),如下圖所示:
  • 確認是wait的請求
  • 實際上這段 golang 程式碼就是將所有/jsonrpc/0的請求都轉發到127.0.0.1:9008,上面程式碼遮住了可能看不清,下面看下完整的:
  • 轉發邏輯
  • 轉發失敗後控制檯也有列印:
  • 控制檯列印
  • 到這裡你可能就要問了,為啥固定轉發 9008 埠呢,實際上這段邏輯在 APP 層,這裡可以先貼出程式碼看看:
  • 9008埠來源

  • 上面的介面因為嘗試轉發到 APP 上,但是因為 APP 程序還不存在,所以返回失敗,進入如下邏輯,不難想到肯定有設定 uiautomator 的兜底邏輯:
  • 請求失敗後邏輯
  • reset_uiautomator的核心邏輯如下
    • 再次確認 atx-agent 請求返回:
    • 在此確認atx-agent請求返回
    • 因為 uiautomator 還沒啟動,所以鐵定是不通的,之後確認 atx-agent 版本號,不對則重新呼叫_prepare_atx_agent(前面說過這個函式):
    • 檢查atx-agent
    • 我們 atx-agent 沒問題,所以直接過到下一步,進入_force_reset_uiautomator_v2開始重置 ui2 環境,這段邏輯比較長,下面單獨拆分字標題說。

重置 uiautomator_v2 如何進行

  • 進入到_force_reset_uiautomator_v2,頭部邏輯如下:
  • _force_reset_uiautomator_v2頭部
  • 到這裡我先說明一個可能的新問題:你覺得上面的self.shell(...)是怎麼呼叫的?你是不是覺得是 python 直接在 pc 端執行的命令?如果你這麼想恭喜你答錯了,實際上self.shell(...)是把命令給到 atx-agent 去執行的
  • 看看這裡 shell 的轉發程式碼。依舊是使用 jsonrpc,只不過這個 path 是 atx-agent 自己處理的:
  • shell實現
  • golang 側 shell 的實現等啟動 uiautomator 的時候再看,普通命令沒太大區別,後續進入self.uiautomator.stop(),我們看看這個stop幹了啥:
  • stop邏輯
  • 我們再到 golang 中看一下,發現是在 golang 中是透過之前儲存的字典取出保活程序
  • delete請求命中
  • 字典取出程序物件然後再呼叫stop
  • 我們再跟進pkeeper.stop()看看,發現核心就是傳了個Truep.stopC
  • pkeeper.stop()
  • 前面沒講pkeeper.start()是怎麼運作的,實際上它就是執行了一個死迴圈,當p.stopC傳入 True 時就會結束,然後釋放程序;擷取了部分關鍵程式碼如下圖所示:
  • pkeeper.start()跳出邏輯

  • 保活程序釋放後,python 層會使用 kill -9 殺掉 uiautomator 程序:
  • kill -9殺掉uiautomator
  • 接下來就是安裝 uiautomator 的兩個 apk 了,安裝的邏輯前面也看過了,這裡不再贅述,安裝完成會列印兩條日誌:
  • 安裝日誌
  • 剩下的self.uiautomator.start()跟之前的stop十分有九分相似,python 層依舊是 jsonrpc 請求,只是變成了 post 方法:
  • start請求
  • 至於 golang 端的實現,之前已經看過一次了,就是使用am instrument啟動單元測試的方式,然後再加個保活鎖:
  • golang層start
  • 到此為止,uiautomator 的程序就都起來了,我們可以用 ps 命令看看(有點亂):
  • 確認uiautomator程序
  • reset_uiautomator函式也到此結束了,後面雖然還有一些兜底邏輯,但大部分都是已經見過的函式實現,所以不再贅述。

判定控制元件是否存在

  • 回到之前的_jsonrpc_retry_call處,reset_uiautomator成功後會重新發起一次請求:
  • 重新發起請求
  • 這一次就能正確打到 APP 的程式碼上了,而 APP 是使用com.googlecode.jsonrpc4j.JsonRpcServer實現了 jsonrpc 服務,並在AutomatorServiceImpl中實現了具體實現,其中waitForExists如下:
  • waitForExists
  • 之後還會繼續呼叫androidx.test.uiautomator包提供的能力,uiautomator提供的能力其實大部分來自AccessibilityService
  • androidx.test.uiautomator
  • findAccessibilityNodeInfo
  • 到此為止,從 python client -> atx-agent server -> app 層都經歷過了,其它實現基本都是這麼流程,我就不再一一展開贅述了。

預設點選實現與坑

  • 這裡我就不再從 python 層一個個過了,直接看 APP 層的點選實現,無論你是 xpath、text 還是別的點選方式,最終大機率都會來到com.github.uiautomator.stub.AutomatorServiceImpl#click(int, int, long),按下和鬆開中間有個間隔的就是長按函式了:
  • click函式
  • 跟進touchUp,因為最終的返回值是它決定的,原理大同小異:
  • touchUp
  • injectEventSync繼續深入的話需要下載原始碼,這裡就不再深入了,你只需要知道這裡使用的是一個同步的注入方法,如果注入失敗就會返回 fasle:
  • injectEventSync

  • 那麼,有哪些坑呢?
  • 第一坑:
    • 事件注入可能會和其它應用有衝突,比如我曾嘗試和 github 上的 Fastbot_Android 放在一起執行(原因是經常會誤觸一些車控開關,想用自動化識別錯誤時返回)
    • 但結果是,每次執行一段時間後,兩者之一就會報錯並且停止
  • 第二坑:
    • 就是上面injectEventSync的返回值,在某款不知道什麼遊戲引擎構建的應用上使用自動化點選時,我指令碼明明只點了一下,但 APP 上總是點兩下。
    • 後來發現是在這個 APP 上injectEventSync都返回 false,而內部框架額外處理了這個injectEventSync的返回值,如果返回 false 就額外點一下,氣死個人。
  • 解決方法?如果是點選的話自己寫 adb,如果想效率更快些就考慮 minitouch 這種(明日方舟的 MAA 掛機使用 minitouch 給我感覺就快了很多)
  • 到此為止,點選的處理流程也講完了,本來想再講講 dump 控制元件樹的介面,但想想好像都大同小異就算了,接下來基本不用再看程式碼了,來聊些稍微有趣的話題。

UI 自動化穩定性/收益問題

穩定性問題

  • 先說一個可能、應該、大概普遍的結論:如果 UI 自動化落地一段時間,且嘗試過各種手段最佳化,但穩定性提升還是不明顯,那大機率是沒救的。
  • UI 自動化不穩定/維護難的原因通常如下:
    • uiautomator2 自身穩定性問題,但透過外部測試框架排程封裝,增加一些兜底邏輯還是比較容易的避免的(內部自研的也一樣有類似的問題)
    • 網路問題,比如 21 年位元組的機房自動化還是會出現白屏,廣州百度早起極爛的網路經常導致入庫失敗等
    • 業務變更頻繁,位元組的業務尤為明顯,以至於某些團隊會放棄 UI 自動化的維護;最近面試某些大公司的時候也是因為這個原因放棄
    • 業務鏈路太長,比如滴滴使用者端和司機端,美團使用者端&騎手端&商家端,涉及多端聯動 + 鏈路過長大大提高失敗率
    • 線上 ab 實驗/UI 適配/降級等變更策略過多,估計是大廠才會出現的通病,分 uid/did/裝置型號輸出頁面/特效,UI 自動化難以持續維護
    • 非原生控制元件只能用 CV,比如百度地圖,底層是用 OpenGL ES 繪製的,開源的方案目前來看都沒啥辦法,引擎層的程式碼保密級別又高,基本就只能用 CV 了
  • 雖然後續搞出了很多 “智慧” UI 自動化方案,但在職期間看落地效果似乎都不咋地。

收益問題

  • UI 自動化打從我開始接觸的那一刻起,就一直被 diss 收益的問題
  • 特別是在位元組期間,位元組的自動化一般都是用機房的叢集迴歸的,上面說到的穩定性問題除了 CV 這一項外,基本都是天天出現
  • 於是測試報告就各種誤報,誤報還要排查,排查之後還要相容,程式碼變更頻率特別頻繁
  • 因為投入的人力與產出不太能成正比,原本一些還在瘋狂投入人力的業務也開始慢慢不投了,或者縮減維護範圍

如何規避問題

  • 如上面的結論所說,UI 自動化的問題通常是無法解決的(至少短期內)
  • 那麼思路就應該轉變成如何利用 UI 自動化做出收益,並且規避它的短板,比如:長期維護乏力,線上變更多,收益不明確
  • 實際上我之前所在的團隊早就意識到該問題,只是解決的思路可能只適用在類似位元組這樣的大廠,下面我就來說一下。

  • 結論:團隊轉型日常效能評測專項(偏基礎體驗)+ 活動業務 BP 專項
  • 你看著描述可能還有點懵,我來解釋下具體邏輯:
    • 效能評測:通常是比對公司業務 APP 與競品的差異,單場景效能的 case 通常不多,且通用性較強,維護成本比起業務 UI 自動化低非常多;之後根據人力接入各個業務,定時輸出對比報告就是穩妥的產出。
    • 活動業務 BP:位元組內部各大 APP 都有自己的活動,再加上類似中秋節、國慶節、春節這種節日活動,不愁沒活;同時活動內容一般都偏向使用新技術 + 寫新程式碼,這意味著出現功能、效能、體驗的 BUG 機率會更高;並且,活動自動化的程式碼寫完大機率就可以扔了,基本不需要考慮後續維護,後續也是持續輸出報告就可以規避 UI 自動化原本的缺點。
  • 簡而言之,UI 自動化不再像之前一樣是投入產出不明的累贅,而是成為了專項環節中的一個小小的指令碼工具,不是過程指標也不是結果指標,單純就是一個輔助工具。

其它補充

如何遠端除錯 Android 上的 Golang 程式碼

  • 核心參考:https://github.com/golang/vscode-go/blob/master/docs/debugging.md
  • 不過光有參考資料還不太夠,因為大部分是 PC 環境下的,Android 環境還要小小處理下
  • 先說一些踩過的坑:
    • golang arm64 架構的包是無法再 Android 上執行的,使用ldd檢視可執行檔案會發現少了一些 linux 的 so,目測屬於硬傷救不了
    • 上面 debug 文件中,大部分都是使用 dlv(delve)開啟 debug 的,但 dlv 有些命令是依賴 go 相關的指令的,基於上一條 Android 中無法使用 go,有些 dlv 方法是不可用的,比如dlv debug就是
    • dlv 的 github 倉庫:https://github.com/go-delve/delve 沒有提供 Android 可執行的 dlv 可執行檔案
  • 最終我自己的解決方式還是使用 dlv,對應上面參考文件中的如下部分:
  • 官方文件
  • 首先是要自己打一個 Android 上可以執行的 dlv,這樣才能開啟 debug server,主流手機一般都支援 armv7、armv8,armv8 一般就對應 arm64,所以 build 的時候設定GOARCH=arm64 GOOS=linux即可
  • 然後就是 dlv 倉庫的版本選擇問題,我本地的 golang 是 1.18.10,下載最新的 dlv 時,專案是 1.19.x 的,可以打包成功並執行,但本地 VSCode 開始遠端除錯時就會報出版本對不上的問題,後更換低版本 dlv(golang 1.18.3)遠端除錯成功,目測是不向上相容,向下大版本能相容
  • 之後參考 dlv 的 github ci 指令碼,得出完整構建命令如下:
GOOS=linux GOARCH=arm64 go build -ldflags "-extldflags -static" -ldflags= github.com/go-delve/delve/cmd/dlv
  • 構建完後直接推手機上就可以,我這裡推的跟 uiautomator2 是同一個目錄:
  • 構建dlv
  • 之後我們在手機對應目錄上就可以執行文件中的命令了(dlv./dlv是有區別的):
  • Android啟動dlv
  • 之後按照先前文件,配置 vscode 的launch.json檔案如下:
  • launch.json配置
  • 現在還不能按 F5 啟動,上面配置的 program 指的是 debug 包的路徑,我們 atx-agent 的 debug 包還沒打,打包命令如下(順手推上去):
GOOS=linux GOARCH=arm64 go build -gcflags="all = -N -l"
  • build+push
  • 之後我們就可以在 atx-agent 的工程按下 F5 啟動除錯了(有個警告不用管),確認 vscode 進入 debug 狀態:
  • 確認vscode debug狀態
  • 確認手機端的 atx-agent server 也被啟動了:
  • atx-agent server也被啟動了
  • 確認斷點是紅色的,不是紅色說明沒生效:
  • 確認斷點為紅色
  • 最後就是確認能命中斷點和看到引數了,可以訪問http://手機ip:7912/info試試看,debug 生效的話上面就會停在上面的斷點:
  • 停在斷點

CV 真的不靠譜嗎

  • 老實說,這要看你所在廠的 CV 積累如何,比如我面快手的時候,對於一些自研的渲染引擎,快手的技術中臺基本就不考慮 CV 方案,更傾向於一些深度學習的方案,比如點掉一些突然出現的浮動視窗
  • 但在位元組就不是這樣,位元組因為有比較強大的 CV Lab,所以 CV 是可以解決絕大部分問題的,比如 Android 耗時自動化如何判斷起始幀,就是開啟開發者設定的指標位置,然後用 CV 去識別螢幕左上角那個用肉眼都不一定看得清的X/X
  • 在百度車機業務,開源方案的表現也還行,因為一個車廠下不同車型通常解析度都是一致的,如果你是負責一個車廠下的不同車型,那麼複用起來基本沒有太大問題

遇到過適合 UI 自動化落地的專案嗎

  • 目前就是百度車機業務比較合適純 UI 自動化做收益,原因如下:
    • 多個專案雖然會出現 text 文案不一致的情況,但是 RD 基本保持 resource-id 是一致的
    • 業務層面的改動不多,短的專案可能 1 年就交付了,長的 2 年 +,但以大部分車廠的佛性文化來說,需求確認後變更點就不多了
    • 車機系統便利,沒有市場上各種自己都不記得密碼的密碼鎖、許可權攔截等,進一步保證執行的穩定性
    • 車機可自由 root,這點確實就比較牛逼,通常情況下自動化執行 crash 了,要想抓到要麼靠 logcat,要麼靠 adb bugreport;而前者不一定出現對應日誌,後者又匯出齊慢;而有了系統許可權後就可以直接去系統目錄取 anr、crash、coredump 了,且速度極快,這直接給 UI 自動化附加了一層深度更深的穩定性測試,實際落地中也確實抓到不少跑 Monkey 沒出現的問題。

有對 uiautomator2 擴充套件來滿足需求嗎

  • 有,就是百度的車機地圖,車機地圖有一個特殊的業務場景,即:多屏地圖,比如主控副屏、HUD 投影等等
  • uiautomator2、weditor 等工具在遇到這些場景時,預設只會顯示主屏的控制元件,除錯起來非常不方便,於是就稍微改造了一下
  • 在說具體改造之前,我先說一下多屏地圖的主要方案,如下圖所示:
  • 多屏方案
  • Android 原生的 Presentation 基本不會使用,其餘的可抽象成兩種方案:
    • 魔改 Presentation:所有螢幕屬於一臺 Android,看到的內容都是真實控制元件,且螢幕是可控的
    • 推流:適用於 C/S 架構,即螢幕和主控 Android 不是同一臺機器,螢幕是不可控的
  • 對於推流的方式,只能從流中擷取圖片做 CV、OCR 斷言
  • 對於真實存在控制元件且可控的魔改 Presentation 方案改造,我這裡就不從頭到尾扯一遍了,就只提一些線索:
    • 魔改 Presentation 是基於 Andorid Context -> window service 管理視窗的
    • 每個視窗都有對應的 display id
    • adb 的 screencap 命令官方文件沒有說全,當我們screencap --help後,會發現有如下內容:
    • screencap --help
      • 沒錯,-d引數就是可以指定 display id 截圖
    • scrcpy 可以根據 display id 來選擇遠控的螢幕,實測多屏地圖確實可以,官方文件中描述如下:
    • scrcpy
    • uiautomator2 的一些實現也是也有用到 window manager:
    • window manager
  • 剩下的就是如何拼裝了

最後

  • 上面提到在百度、位元組工作過,看起來好 diao,但實際上我目前只工作過兩年,位元組一年,百度一年;並且最近因為業務人員裁撤,處於失業的狀態
  • 不過閒著也是閒著,並且發現身為一個測試開發都沒怎麼寫過相關的內容,所以就趁熱寫一篇吧

相關文章