uiautomator2簡介
項目Git地址: https://github.com/openatx/uiautomator2
安裝
#1. 安裝 uiautomator2
使用pip進行安裝, 注意因為uiaotumator2還在開發中需要加上--pre來安裝最新的版本
pip install --upgrade --pre uiautomator2
也可以使用源碼來進行安裝
git clone https://github.com/openatx/uiautomator2 pip install -e uiautomator2
安裝pillow庫, 截屏功能會需要用到這個庫
pip install pillow
#2.安裝守護程序到設備上(對設備進行初始化)
電腦連接上一個手機或多個手機, 確保adb已經添加到環境變量中,執行下面的命令會自動安裝本庫所需要的設備端程序:uiautomator-server 、atx-agent、openstf/minicap、openstf/minitouch
執行之前需要確保adb service開啟了
adb devices
執行上面的指令能看到連接的設備id就可以
# init 所有的已經連接到電腦的設備
python -m uiautomator2 init
如果沒有開啟adb service會提示以下錯誤, 開啟adb service服務就好了
socket.error: [Errno 61] Connection refused
安裝提示success即可, 注意安裝的過程中手機上會有提示是否安裝, 要手動確認下
#3. 安裝weditor (UI Inspector)
基於瀏覽器技術開發的weditor UI查看器, 安裝方法
pip install -U weditor
啟動方式:
python -m weditor
會自動打開瀏覽器,輸入設備的ip或者序列號,點擊Connect成功后,點擊刷新。
鼠標選擇控件,可以在右邊看到對應的屬性值
使用
與設備建立連接
#1. 通過WiFi連接設備
如果手機和電腦處於同一個局域網, 可以使用如下方式通過手機ip建立連接
import uiautomator2 as u2 d = u2.connect('10.234.12.104') print d.info
其中'10.234.12.104'是手機的IP地址, 可以通過adb指令獲得: adb shell ifconfig | grep Mask
運行結果如下:
{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}
#2.通過USB連接設備
如果手機與電腦有通過USB連接, 可以使用如下方式建立連接
import uiautomator2 as u2 d = u2.connect('62ab58430211') print d.info
其中'62ab58430211'是手機的SN, 可以使用adb指令獲得: adb devices
運行結果如下:
{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}
#3.通過adb WiFi連接
如果配置了手機指定端口監聽TCP/IP連接, 比如
adb tcpip 5555
可以通過指定端口建立連接
import uiautomator2 as u2 d = u2.connect('10.234.12.104:5555') print d.info
這個方法在我的機器上python2.7會報錯, 可能要在3.0以上
命令行指令
注: 下面的$device_ip代表手機IP
#1. init 為設備安裝所需要的程序
python -m uiautomator2 init
#2. install: 安裝apk, apk通過URL給出
python -m uiautomator2.cli install $device_ip https://example.org/some.apk
#3. clear-cache: 清空緩存
python -m uiautomator2 clear-cache
#4. app-stop-all: 停止所有應用
python -m uiautomator2 app-stop-all $device_ip
#5. screenshot: 截圖
python -m uiautomator2 screenshot $device_ip screenshot.jpg
#6. healthcheck: 健康檢查
python -m uiautomator2 healthcheck $device_ip
常用API
全局設置
#1. Debug HTTP Requests
import uiautomator2 as u2 d = u2.connect('10.234.12.104') d.debug=True print d.info
運行結果:
23:17:15.628 $ curl -X POST -d '{"params": {}, "jsonrpc": "2.0", "id": "eaacec696e5911b38ea35b652f5a0d54", "method": "deviceInfo"}' 'http://10.234.12.104:7912/jsonrpc/0' 23:17:15.856 Response (228 ms) >>> {"jsonrpc":"2.0","id":"eaacec696e5911b38ea35b652f5a0d54","result":{"currentPackageName":"com.miui.home","displayHeight":2150,"displayRotation":0,"displaySizeDpX":393,"displaySizeDpY":829,"displayWidth":1080,"productName":"lotus","screenOn":true,"sdkInt":27,"naturalOrientation":true}} <<< END {u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True}
#2.Implicit wait
設置元素操作等待時間, 單位: 秒
d.implicitly_wait(10.0) d(text="小米體檢").click() print("wait timeout", d.implicitly_wait())
第一步為設置全局元素操作等待時間, 第二步點擊文本"小米體檢", 如果10秒內"小米體檢還沒有出現則會 raise UiObjectNotFoundError
這是設置會影響的操作有: click, long_click, drag_to, get_text, set_text, clear_text等
APP管理
#1. 安裝APP
只支持從網絡鏈接安裝
d.app_install('http://some-domain.com/some.apk')
#2. 運行APP
d.app_start("com.example.hello_world") # start with package name
#3. 停止運行
# equivalent to `am force-stop`, thus you could lose data d.app_stop("com.example.hello_world") # equivalent to `pm clear` d.app_clear('com.example.hello_world')
#4. 停止所有運行的app
# stop all d.app_stop_all() # stop all app except for com.examples.demo d.app_stop_all(excludes=['com.examples.demo'])
#5. 獲取APP信息
print d.app_info("com.examples.demo")
#6. 保存app圖標
# save app icon img = d.app_icon("com.examples.demo") img.save("icon.png")
#7. Push文件到設備
# 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)
#8. 從設備pull文件
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")
#9. 檢查並維持設備端守護進程處於運行狀態
d.healthcheck()
基本API使用
Shell Command
adb_shell已經廢棄,現在使用shell.
short-lived shell command
默認的超時為60s, 我們試試用shell命令發送pwd指令
output, exit_code = d.shell("pwd", timeout=60) print output print exit_code
輸出:
/ 0
也可以這樣寫
output = d.shell('pwd').output exit_code = d.shell('pwd').exit_code
參數可以以list的形式使用,我們試試
output, exit_code = d.shell(['ls', '-l']) print output print exit_code
long-running shell command
r = d.shell("logcat", stream=True) # r: requests.models.Response deadline = time.time() + 10 # run maxium 10s try: for line in r.iter_lines(): # r.iter_lines(chunk_size=512, decode_unicode=None, delimiter=None) if time.time() > deadline: break print("Read:", line.decode('utf-8')) finally: r.close() # this method must be called
Session
#1. 使用session來操作APP的開啟和關閉
sess = d.session("com.netease.cloudmusic") # start 網易雲音樂 sess.close() # 停止網易雲音樂
#2. 使用with來開啟和關閉
with d.session("com.netease.cloudmusic") as sess: sess(text="Play").click()
#3. 創建已經打開的APP的session
sess = d.session('com.ganji.android.haoche_c', attach=True) time.sleep(5) sess.close()
#4. 偵測APP Crash
# 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
#5. 檢查session是否正常
# 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
檢索設備信息
#1. base information
print d.info
結果:
{u'displayRotation': 0, u'displaySizeDpY': 829, u'displaySizeDpX': 393, u'screenOn': True, u'displayWidth': 1080, u'productName': u'lotus', u'currentPackageName': u'com.miui.home', u'sdkInt': 27, u'displayHeight': 2150, u'naturalOrientation': True} Process finished with exit code 0
#2. windows size
print(d.window_size()) # device upright output example: (1080, 1920) # device horizontal output example: (1920, 1080)
#3. Get current app info
print d.current_app()
運行結果:
{'activity': u'com.ganji.android.haoche_c.ui.main.MainActivity', 'package': u'com.ganji.android.haoche_c'} Process finished with exit code 0
#4. Wait activity
sess = d.session('com.ganji.android.haoche_c') time.sleep(5) print d.wait_activity('com.ganji.android.haoche_c.ui.main.MainActivity')
#5. Get device serial number
print d.serial
#6. Get WLAN ip
print d.wlan_ip
#7. Get detailed device info
print d.device_info
Key Events
#1. Turn on/off screen
d.screen_on() # turn on the screen d.screen_off() # turn off the screen
#2. Get current screen status
print d.info.get('screenOn')
#3. Press hard/soft key
d.press("home") # press the home key, with key name d.press("back") # press the back key, with key name d.press(0x07, 0x02) # press keycode 0x07('0') with META ALT(0x02)
之前支持的key有以下這些:
home back left right up down center menu search enter delete ( or del) recent (recent apps) volume_up volume_down volume_mute camera power
更多key可以查看: Android KeyEvnet
#4. Unlock screen
d.unlock() # This is equivalent to # 1. launch activity: com.github.uiautomator.ACTION_IDENTIFY # 2. press the "home" key
Gesture interaction with the device
#1. Click on the screen
d.click(x, y)
#2. Double click
d.double_click(x, y) d.double_click(x, y, 0.1) # default duration between two click is 0.1s
#3. Long click on the screen
d.long_click(x, y) d.long_click(x, y, 0.5) # long click 0.5s (default)
#4. Swipe
d.swipe(sx, sy, ex, ey) d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
#5. Drag
d.drag(sx, sy, ex, ey) d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
#6. Swipe points
# swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2) # time will speed 0.2s bwtween two points d.swipe((x0, y0), (x1, y1), (x2, y2), 0.2)
多用於九宮格解鎖,提前獲取到每個點的相對坐標(這里支持百分比), 更詳細的使用參考這個帖子 使用u2實現九宮圖案解鎖
#7. Touch and drap (beta)
這個接口屬於比較底層的原始接口,感覺並不完善,不過湊合能用。注:這個地方並不支持百分比
d.touch.down(10, 10) # 模擬按下 time.sleep(.01) # down 和 move 之間的延遲,自己控制 d.touch.move(15, 15) # 模擬移動 d.touch.up() # 模擬抬起
click, swipe, drag操作支持按比例操作, 比如
d.long_click(0.5, 0.5)
意思是長按屏幕中心
Screen-Related
#1. 獲取當前設備方向
print d.orientation
可能的方向有
natural or n left or l right or r upsidedown or u (can not be set)
#2. 設置設備方向
d.set_orientation('l') # or "left" d.set_orientation("l") # or "left" d.set_orientation("r") # or "right" d.set_orientation("n") # or "natural"
#3. 禁止旋轉和解除
# freeze rotation d.freeze_rotation() # un-freeze rotation d.freeze_rotation(False)
#4. 截屏
# 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)
#5. 獲取UI層級關系
# get the UI hierarchy dump content (unicoded). xml = d.dump_hierarchy()
#6. 打開通知中心
d.open_notification()
#7. 快速設置
d.open_quick_settings()
Selector
選擇當前窗口中的UI控件, 例如
# Select the object with text 'Clock' and its className is 'android.widget.TextView' d(text='Clock', className='android.widget.TextView')
支持以下這些選擇參數, 詳細可以參考UiSelector Java doc
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
#1. Children (子級控件)
# get the children or grandchildren d(className="android.widget.ListView").child(text="Bluetooth")
#2. Siblings(同級控件)
# get siblings d(text="Google").sibling(className="android.widget.ImageView")
#3. children by text or description or instance
# 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" )
-
child_by_description
is to find children whose grandchildren have the specified description, other parameters being similar tochild_by_text
. -
child_by_instance
is to find children with has a child UI element anywhere within its sub hierarchy that is at the instance specified. It is performed on visible views without scrolling.
See below links for detailed information:
- UiScrollable,
getChildByDescription
,getChildByText
,getChildByInstance
- UiCollection,
getChildByDescription
,getChildByText
,getChildByInstance
Above methods support chained invoking, e.g. for below 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>
To click the switch widget right to the TextView 'Wi‑Fi', we need to select the switch widgets first. However, according to the UI hierarchy, more than one switch widgets exist and have almost the same properties. Selecting by className will not work. Alternatively, the below selecting strategy would help:
d(className="android.widget.ListView", resourceId="android:id/list") \ .child_by_text("Wi‑Fi", className="android.widget.LinearLayout") \ .child(className="android.widget.Switch") \ .click()
#4. relative positioning
我們可以使用相對位置來輔助獲取控件: left、right、top、bottom.
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()
#5. Multiple instances
有時候我們會遇到屏幕上有多個控件的屬性一樣,這個時候就可以使用instance屬性來獲取其中一個控件
d(text="Add new", instance=0) # which means the first instance with text "Add new"
另外還提供了以下API功能
# 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 # ...
Notes: when using selectors in a code block that walk through the result list, you must ensure that the UI elements on the screen keep unchanged. Otherwise, when Element-Not-Found error could occur when iterating through the list.
Get the selected ui object status and its information
#1. 判斷UI控件是否存在
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)
#2. 獲取控件信息
print d(text="Settings").info
輸出:
{ u'contentDescription': u'', u'checked': False, u'scrollable': False, u'text': u'Settings', u'packageName': u'com.android.launcher', u'selected': False, u'enabled': True, u'bounds': {u'top': 385, u'right': 360, u'bottom': 585, u'left': 200}, u'className': u'android.widget.TextView', u'focused': False, u'focusable': True, u'clickable': True, u'chileCount': 0, u'longClickable': True, u'visibleBounds': {u'top': 385, u'right': 360, u'bottom': 585, u'left': 200}, u'checkable': False }
#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
#4. 獲取控件中心坐標/中心偏移坐標
x, y = d(text="Settings").center() # x, y = d(text="Settings").center(offset=(0, 0)) # left-top x, y
Perform the click action on the selected UI object
#1. 點擊控件
# click on the center of the specific ui object d(text="Settings").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
#2. 長按控件
# long click on the center of the specific UI object d(text="Settings").long_click()
Gesture actions for the specific UI object
#1. 拖動控件往指定坐標或控件
# notes : drag can not be used for Android<4.3. # drag the UI object to a screen point (x, y), in 0.5 second d(text="Settings").drag_to(x, y, duration=0.5) # drag the UI object to (the center position of) another UI object, in 0.25 second d(text="Settings").drag_to(text="Clock", duration=0.25)
#2. 控件上滑動
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)
#3. 雙指手勢
d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))
#4. 縮放手勢
# 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()
#5. 等待控件出現或消失
# 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)
The default timeout is 20s. see global settings for more details
#6. 慣性滾動控件
# fling forward(default) vertically(default) d(scrollable=True).fling() # fling forward horizontally d(scrollable=True).fling.horiz.forward() # fling backward vertically d(scrollable=True).fling.vert.backward() # fling to beginning horizontally d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000) # fling to end vertically d(scrollable=True).fling.toEnd()
#7. 滾動控件
# scroll forward(default) vertically(default) d(scrollable=True).scroll(steps=10) # scroll forward horizontally d(scrollable=True).scroll.horiz.forward(steps=100) # scroll backward vertically d(scrollable=True).scroll.vert.backward() # scroll to beginning horizontally d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000) # scroll to end vertically d(scrollable=True).scroll.toEnd() # scroll forward vertically until specific ui object appears d(scrollable=True).scroll.to(text="Security")
全局變量
# set delay 1.5s after each UI click and click d.click_post_delay = 1.5 # default no delay # set default element wait timeout (seconds) d.wait_timeout = 30.0 # default 20.0
超時設置
>> d.jsonrpc.getConfigurator() {'actionAcknowledgmentTimeout': 500, 'keyInjectionDelay': 0, 'scrollAcknowledgmentTimeout': 200, 'waitForIdleTimeout': 0, 'waitForSelectorTimeout': 0} >> d.jsonrpc.setConfigurator({"waitForIdleTimeout": 100}) {'actionAcknowledgmentTimeout': 500, 'keyInjectionDelay': 0, 'scrollAcknowledgmentTimeout': 200, 'waitForIdleTimeout': 100, 'waitForSelectorTimeout': 0}
為了防止客戶端程序響應超時,waitForIdleTimeout
和waitForSelectorTimeout
目前已改為0
Input Method
這種方法通常用於不知道控件的情況下的輸入。第一步需要切換輸入法,然后發送adb廣播命令,具體使用方法如下
d.set_fastinput_ime(True) # 切換成FastInputIME輸入法 d.send_keys("你好123abcEFG") # adb廣播輸入 d.clear_text() # 清除輸入框所有內容(Require android-uiautomator.apk version >= 1.0.7) d.set_fastinput_ime(False) # 切換成正常的輸入法 d.send_action("search") # 模擬輸入法的搜索
Toast
顯示Toast
d.toast.show("Hello world") d.toast.show("Hello world", 1.0) # show for 1.0s, default 1.0s
獲取Toast
# [Args] # 5.0: max wait timeout. Default 10.0 # 10.0: cache time. return cache toast if already toast already show up in recent 10 seconds. Default 10.0 (Maybe change in the furture) # "default message": return if no toast finally get. Default None d.toast.get_message(5.0, 10.0, "default message") # common usage assert "Short message" in d.toast.get_message(5.0, default="") # clear cached toast d.toast.reset() # Now d.toast.get_message(0) is None
Stop UiAutomator
停止UiAutomator守護服務
https://github.com/openatx/uiautomator2/wiki/Common-issues
因為有atx-agent
的存在,Uiautomator會被一直守護着,如果退出了就會被重新啟動起來。但是Uiautomator又是霸道的,一旦它在運行,手機上的輔助功能、電腦上的uiautomatorviewer 就都不能用了,除非關掉該框架本身的uiautomator。下面就說下兩種關閉方法
方法1:
直接打開uiautomator app(init成功后,就會安裝上的),點擊關閉UIAutomator
方法2:
d.service("uiautomator").stop() # d.service("uiautomator").start() # 啟動 # d.service("uiautomator").running() # 是否在運行