https://github.com/openatx/uiautomator2
平時(shí)用的最多也最順手的一個(gè)Android系統(tǒng)APP控制Python庫,它不僅可以幫我在工作上實(shí)現(xiàn)基于Android車載娛樂系統(tǒng)的自動化操控和測試,也可以在雙十一為你自動開搶心儀的商品。以下對常用功能進(jìn)行了整理和總結(jié)。
連接ADB設(shè)備:
可以通過USB或Wifi與ADB設(shè)備進(jìn)行連接,進(jìn)而調(diào)用Uiautomator2框架,支持同時(shí)連接單個(gè)或多個(gè)ADB設(shè)備。
- USB連接:只有一個(gè)設(shè)備也可以省略參數(shù),多個(gè)設(shè)備則需要序列號來區(qū)分
import uiautomator2 as u2
d = u2.connect("--serial-here--")
- USB連接:一個(gè)設(shè)備時(shí),可簡寫
d = u2.connect()
- 無線連接:通過設(shè)備的IP連接(需要在同一局域網(wǎng)且設(shè)備上的atx-agent已經(jīng)安裝并啟動)
d = u2.connect("10.1.2.3")
- 無線連接:通過ABD wifi 等同于下面的代碼
d = u2.connect_adb_wifi("10.0.0.1:5555")
#等同于
+ Shell: adb connect 10.0.0.1:5555
+ Python: u2.connect_usb("10.0.0.1:5555")
APP操作:
用于啟動或停止某個(gè)APP
- 獲取前臺應(yīng)用 packageName, activity
d.app_current()
2.1 啟動應(yīng)用( 默認(rèn)的這種方法是先通過atx-agent解析apk包的mainActivity,然后調(diào)用am start -n $package/$activity啟動)
d.app_start("com.example.app")
2.2. 通過指定main activity的方式啟動應(yīng)用,等價(jià)于調(diào)用am start -n com.example.hello_world/.MainActivity
d.app_start("com.example.hello_world", ".MainActivity")
2.3. 使用 monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1 啟動,這種方法有個(gè)副作用,它自動會將手機(jī)的旋轉(zhuǎn)鎖定給關(guān)掉
d.app_start("com.example.hello_world", use_monkey=True)
- 啟動應(yīng)用前停止此應(yīng)用
d.app_start("com.example.app", stop=True)
4.1 停止應(yīng)用, 等價(jià)于am force-stop,此方法會丟失應(yīng)用數(shù)據(jù)
d.app_stop("com.example.app")
4.2 停止應(yīng)用, 等價(jià)于pm clear
d.app_clear('com.example.hello_world')
4.3 停止所有應(yīng)用
d.app_stop_all()
4.4 停止所有應(yīng)用,除了某個(gè)應(yīng)用
d.app_stop_all(excludes=['com.examples.demo'])
- 得到APP圖標(biāo)
img = d.app_icon("com.examples.demo")
img.save("icon.png")
- 列出所有運(yùn)行中的應(yīng)用
d.app_list_running()
- 確定APP是否啟動,也可以通過Session來判斷
pid = d.app_wait("com.example.android") # 等待應(yīng)用運(yùn)行, return pid(int)
if not pid:
print("com.example.android is not running")
else:
print("com.example.android pid is %d" % pid)
d.app_wait("com.example.android", front=True) # 等待應(yīng)用前臺運(yùn)行
d.app_wait("com.example.android", timeout=20.0) # 最長等待時(shí)間20s(默認(rèn))
or
d.wait_activity(".ApiDemos", timeout=10) # default timeout 10.0 seconds
Session操作
一般用于測試某個(gè)特定的APP,首先將某個(gè)APP設(shè)定為一個(gè)Session,所有的操作都基于此Session,當(dāng)Session退出時(shí),代表APP退出。
- 啟動應(yīng)用并獲取session
session的用途是操作的同時(shí)監(jiān)控應(yīng)用是否閃退,當(dāng)閃退時(shí)操作,會拋出SessionBrokenError
sess = d.session("com.example.app") # start app
- 停止或重啟session,即app
sess.close() # 停止app
sess.restart() # 冷啟app
- python with 功能,開啟某個(gè)APP執(zhí)行某個(gè)操作后,自動退出某個(gè)session
with d.session("com.netease.cloudmusic") as sess:
sess(text="Play").click()
4.1 當(dāng)APP已運(yùn)行時(shí)自動跳過啟動
# launch app if not running, skip launch if already running
sess = d.session("com.netease.cloudmusic", attach=True)
4.2 當(dāng)某個(gè)APP沒有啟動時(shí),報(bào)錯
# raise SessionBrokenError if not running
sess = d.session("com.netease.cloudmusic", attach=True, strict=True)
5.1 確定session對應(yīng)的APP是否運(yùn)行
# check if session is ok.
# Warning: function name may change in the future
sess.running() # True or False
5.2 確定session對應(yīng)的APP是否運(yùn)行,當(dāng)不在運(yùn)行將報(bào)錯
# When app is still running
sess(text="Music").click() # operation goes normal
# If app crash or quit
sess(text="Music").click() # raise SessionBrokenError
# other function calls under session will raise SessionBrokenError too
截圖與hierarchy提?。?/h2>
用于獲取Android當(dāng)前的截圖和界面元素。
- 截圖
# take screenshot and save to a file on the computer, require Android>=4.2.
d.screenshot("home.jpg")
# get PIL.Image formatted images. Naturally, you need pillow installed first
image = d.screenshot() # default format="pillow"
image.save("home.jpg") # or home.png. Currently, only png and jpg are supported
# get opencv formatted images. Naturally, you need numpy and cv2 installed first
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)
# get raw jpeg data
imagebin = d.screenshot(format='raw')
open("some.jpg", "wb").write(imagebin)
- 獲取hierarchy
# get the UI hierarchy dump content (unicoded).
xml = d.dump_hierarchy()
模擬觸控操作:
用于模擬用戶對手機(jī)的點(diǎn)擊或滑動等操作
1.1 XY坐標(biāo)點(diǎn)擊
d.click(10, 20)
1.2 XY坐標(biāo)雙擊
d.double_click(x, y)
d.double_click(x, y, 0.1) # default duration between two click is 0.1s
1.3 長按某個(gè)坐標(biāo)
d.long_click(x, y)
d.long_click(x, y, 0.5) # long click 0.5s (default)
- 通過元素中的Text信息來點(diǎn)擊,程序會點(diǎn)擊Text所在layout的中心位置,
# click on the center of the specific ui object
d(text="Settings").click()
d(Text="Settings").double_click()
d(Text="Settings").long_click()
# wait element to appear for at most 10 seconds and then click
d(text="Settings").click(timeout=10)
# click with offset(x_offset, y_offset)
# click_x = x_offset * width + x_left_top
# click_y = y_offset * height + y_left_top
d(text="Settings").click(offset=(0.5, 0.5)) # Default center
d(text="Settings").click(offset=(0, 0)) # click left-top
d(text="Settings").click(offset=(1, 1)) # click right-bottom
# click when exists in 10s, default timeout 0s
clicked = d(text='Skip').click_exists(timeout=10.0)
# click until element gone, return bool
is_gone = d(text="Skip").click_gone(maxretry=10, interval=1.0) # maxretry default 10, interval default 1.0
- 滑動操作,從(10, 20)滑動到(80, 90)
d.swipe(10, 20, 80, 90)
d.swipe(sx, sy, ex, ey, 0.5)
d(text="Settings").swipe("right")
d(text="Settings").swipe("left", steps=10)
d(text="Settings").swipe("up", steps=20) # 1 steps is about 5ms, so 20 steps is about 0.1s
d(text="Settings").swipe("down", steps=20)
# swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)
# time will speed 0.2s bwtween two points
d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2))
- 整個(gè)屏幕右滑動
d.swipe_ext("right")
- 屏幕右滑,滑動距離為屏幕寬度的90%
d.swipe_ext("right", scale=0.9)
- 從一個(gè)坐標(biāo)拖拽到另一個(gè)坐標(biāo)
d.drag(sx, sy, ex, ey)
d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
- 模擬按下后的連續(xù)操作,如九宮格解鎖
d.touch.down(10, 10) # 模擬按下
time.sleep(.01) # down 和 move 之間的延遲,自己控制
d.touch.move(15, 15) # 模擬移動
d.touch.up() # 模擬抬起
- 模擬兩指縮放操作
# notes : pinch can not be set until Android 4.3.
# from edge to center. here is "In" not "in"
d(text="Settings").pinch_in(percent=100, steps=10)
# from center to edge
d(text="Settings").pinch_out()
or
d().pinch_in(percent=100, steps=10)
d().pinch_out()
硬按鍵操作
用于模擬用戶對手機(jī)硬按鍵或系統(tǒng)按鍵的操作。
- 模擬按 Home 或 Back 鍵
目前支持以下關(guān)鍵字,但并非所有設(shè)備都支持:
home
back
left
right
up
down
center
menu
search
enter
delete ( or del)
recent (recent apps)
volume_up
volume_down
volume_mute
camera
power
d.press("back")
d.press("home")
- 模擬按Android定義的硬鍵值
d.press(0x07, 0x02)
# press keycode 0x07('0') with META ALT(0x02)
#具體可查詢:
#https://developer.android.com/reference/android/view/KeyEvent.html
- 解鎖屏幕
d.unlock()
# This is equivalent to
# 1. launch activity: com.github.uiautomator.ACTION_IDENTIFY
# 2. press the "home" key
- 模擬輸入,需要光標(biāo)已經(jīng)在輸入框中才可以
d.set_fastinput_ime(True) # 切換成FastInputIME輸入法
d.send_keys("你好123abcEFG") # adb廣播輸入
d.clear_text() # 清除輸入框所有內(nèi)容(Require android-uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切換成正常的輸入法
d.send_action("search") # 模擬輸入法的搜索
- 清空輸入框
d.clear_text()
執(zhí)行ADB shell命令
直接通過Python來執(zhí)行ADB shell中的指令,并得到反饋。
- 執(zhí)行shell命令,獲取輸出和exitCode
output, exit_code = d.shell("ps -A", timeout=60)
- 僅得到輸出
output = d.shell("pwd").output
- 僅得到Exitcode
exit_code = d.shell("pwd").exit_code
- 推送文件到ADB設(shè)備中
# push to a folder
d.push("foo.txt", "/sdcard/")
# push and rename
d.push("foo.txt", "/sdcard/bar.txt")
# push fileobj
with open("foo.txt", 'rb') as f:
d.push(f, "/sdcard/")
# push and change file access mode
d.push("foo.sh", "/data/local/tmp/", mode=0o755)
- 獲取文件到本地
d.pull("/sdcard/tmp.txt", "tmp.txt")
# FileNotFoundError will raise if the file is not found on the device
d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")
元素操作或Selector
這是Uiautomator2最為關(guān)鍵的核心功能,測試者可以根據(jù)界面中的元素來判斷當(dāng)前畫面是否符合預(yù)期或基于界面元素進(jìn)行點(diǎn)按滑動等操作。
目前Uiautomator2支持以下種類的關(guān)鍵字參數(shù):
text, textContains, textMatches, textStartsWith
className, classNameMatches
description, descriptionContains, descriptionMatches, descriptionStartsWith
checkable, checked, clickable, longClickable
scrollable, enabled,focusable, focused, selected
packageName, packageNameMatches
resourceId, resourceIdMatches
index, instance
舉個(gè)例子,測試者可以通過以上關(guān)鍵字的組合,來實(shí)現(xiàn)特定界面元素的定位,如下面這段代碼是要求UT2去點(diǎn)擊界面中,元素text信息為clock,className為'android.widget.TextView'的元素:
# Select the object with text 'Clock' and its className is 'android.widget.TextView'
d(text='Clock', className='android.widget.TextView').click()
除了,可以使用關(guān)鍵字的組合來限定特定UI元素,UT2也支持通過子節(jié)點(diǎn)或兄弟節(jié)點(diǎn)來限定特定UI元素。如下面這幾段代碼分別是通過某個(gè)元素,獲取其子元素或同胞元素中的信息或進(jìn)行后續(xù)操作。
# children
# get the children or grandchildren
d(className="android.widget.ListView").child(text="Bluetooth")
# get the children or grandchildren
d(className="android.widget.ListView").child(text="Bluetooth")
# siblings
# get siblings
d(text="Google").sibling(className="android.widget.ImageView")
也可以根據(jù)子節(jié)點(diǎn)的Text或 Description或Instance來定位元素, 特別提下下面代碼中的這個(gè)allow_scroll_search功能,它調(diào)用UT2自動滾動直到找到對應(yīng)元素:
# get the child matching the condition className="android.widget.LinearLayout"
# and also its children or grandchildren with text "Bluetooth"
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text("Bluetooth", className="android.widget.LinearLayout")
# get children by allowing scroll search
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text(
"Bluetooth",
allow_scroll_search=True,
className="android.widget.LinearLayout"
)
下面有另一個(gè)實(shí)例來展示UT2的定位,以下為Android的系統(tǒng)設(shè)置界面及它的hierarchy:
系統(tǒng)設(shè)置界面:

hierarchy:
<node index="0" text="" resource-id="android:id/list" class="android.widget.ListView" ...>
<node index="0" text="WIRELESS & NETWORKS" resource-id="" class="android.widget.TextView" .../>
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" ...>
<node index="1" text="" resource-id="" class="android.widget.RelativeLayout" ...>
<node index="0" text="Wi?Fi" resource-id="android:id/title" class="android.widget.TextView" .../>
</node>
<node index="2" text="ON" resource-id="com.android.settings:id/switchWidget" class="android.widget.Switch" .../>
</node>
...
</node>
通過child_by_text + child組合后可以定位到WIFI的開關(guān)。
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text("Wi?Fi", className="android.widget.LinearLayout") \
.child(className="android.widget.Switch") \
.click()
也可以通過,位置關(guān)系來定位元素:
d(A).left(B), selects B on the left side of A.
d(A).right(B), selects B on the right side of A.
d(A).up(B), selects B above A.
d(A).down(B), selects B under A.
## select "switch" on the right side of "Wi?Fi"
d(text="Wi?Fi").right(className="android.widget.Switch").click()
還有可以通過元素的instances來定位,比如一個(gè)界面中有多個(gè)switch,我們可以通過下面的形式來定位是第一個(gè)還是第二個(gè)
d(className="android.widget.Switch", instance=0)
d(className="android.widget.Switch")[0]
or
# get the count of views with text "Add new" on current screen
d(text="Add new").count
# same as count property
len(d(text="Add new"))
# get the instance via index
d(text="Add new")[0]
d(text="Add new")[1]
...
# iterator
for view in d(text="Add new"):
view.info # ...
- 等待某個(gè)元素出現(xiàn)
d(text="Settings").exists # True if exists, else False
d.exists(text="Settings") # alias of above property.
# advanced usage
d(text="Settings").exists(timeout=3) # wait Settings appear in 3s, same as .wait(3)
d.xpath("立即開戶").wait() # 等待元素,最長等10s(默認(rèn))
d.xpath("立即開戶").wait(timeout=10) # 修改默認(rèn)等待時(shí)間
- xpath操作
具體可以參考:https://github.com/openatx/uiautomator2/blob/master/XPATH.md
# xpath操作
d.xpath("立即開戶").click() # 包含查找等待+點(diǎn)擊操作,匹配text或者description等于立即開戶的按鈕
d.xpath("http://*[@text='私人FM']/../android.widget.ImageView").click()
d.xpath('//*[@text="私人FM"]').get().info # 獲取控件信息
for el in d.xpath('//android.widget.EditText').all():
print("rect:", el.rect) # output tuple: (left_x, top_y, width, height)
print("bounds:", el.bounds) # output tuple: (left, top, right, bottom)
print("center:", el.center())
el.click() # click operation
print(el.elem) # 輸出lxml解析出來的Node
3.輸入框的操作
d(text="Settings").get_text() # get widget text
d(text="Settings").set_text("My text...") # set the text
d(text="Settings").clear_text() # clear the text
- 等待某個(gè)元素出現(xiàn)或消失
# wait until the ui object appears
d(text="Settings").wait(timeout=3.0) # return bool
# wait until the ui object gone
d(text="Settings").wait_gone(timeout=1.0)
Setting
- 默認(rèn)控件等待時(shí)間(原生操作,xpath插件的等待時(shí)間)
d.settings['wait_timeout'] = 20.0
or
d.implicitly_wait(20.0)
- 點(diǎn)擊的等待延時(shí)
d.click_post_delay = 1.5 # default no delay
- 配置accessibility服務(wù)的最大空閑時(shí)間,超時(shí)將自動釋放。默認(rèn)3分鐘。(如果兩個(gè)步驟需要等待較長時(shí)間,且不希望下一次發(fā)送指令時(shí)重啟UT2,則可以將此時(shí)間加大)
d.set_new_command_timeout(300)
守護(hù)
用于處理非預(yù)期的彈出框,如崩潰窗口,一些確定或取消彈出框。
- 監(jiān)控彈窗(在線程中監(jiān)控)
# 常用寫法,注冊匿名監(jiān)控
d.watcher.when("安裝").click()
# 注冊名為ANR的監(jiān)控,當(dāng)出現(xiàn)ANR和Force Close時(shí),點(diǎn)擊Force Close
d.watcher("ANR").when(xpath="ANR").when("Force Close").click()
# 其他回調(diào)例子
d.watcher.when("搶紅包").press("back")
d.watcher.when("http://*[@text = 'Out of memory']").call(lambda d: d.shell('am force-stop com.im.qq'))
# 移除ANR的監(jiān)控
d.watcher.remove("ANR")
# 移除所有的監(jiān)控
d.watcher.remove()
# 開始后臺監(jiān)控
d.watcher.start()
d.watcher.start(2.0) # 默認(rèn)監(jiān)控間隔2.0s
# 強(qiáng)制運(yùn)行所有監(jiān)控
d.watcher.run()
# 停止監(jiān)控
d.watcher.stop()
# 停止并移除所有的監(jiān)控,常用于初始化
d.watcher.reset()
插件
Performance 性能采集(記錄CPU,RAM等數(shù)據(jù))
https://github.com/openatx/uiautomator2/blob/6e0d75d778a86c626df778e0432c8e339e3d9be4/uiautomator2/ext/perf/README.md
Aircv 圖像比對插件(有較多限制,推薦自己單獨(dú)截圖后調(diào)用Aircv來實(shí)現(xiàn)圖像比對或點(diǎn)擊等功能,或用ATX來實(shí)現(xiàn))
https://github.com/openatx/uiautomator2/blob/436404119fafce303ad8f3a07811c044d101b9eb/uiautomator2/ext-archived/aircv/README.md
Htmlreport插件(將操作生成HTML文件)
https://github.com/openatx/uiautomator2/tree/6e0d75d778a86c626df778e0432c8e339e3d9be4/uiautomator2/ext/htmlreport