原文链接:https://www.jianshu.com/p/7025bf0f7a2a
https://github.com/openatx/uiautomator2
平时用的最多也最顺手的一个Android系统APP控制Python库,它不仅可以帮我在工作上实现基于Android车载娱乐系统的自动化操控和测试,也可以在双十一为你自动开抢心仪的商品。以下对常用功能进行了整理和总结。
连接ADB设备:
可以通过USB或Wifi与ADB设备进行连接,进而调用Uiautomator2框架,支持同时连接单个或多个ADB设备。
- USB连接:只有一个设备也可以省略参数,多个设备则需要序列号来区分
import uiautomator2 as u2 d = u2.connect("--serial-here--")
- USB连接:一个设备时,可简写
d = u2.connect()
- 无线连接:通过设备的IP连接(需要在同一局域网且设备上的atx-agent已经安装并启动)
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操作:
用于启动或停止某个APP
- 获取前台应用 packageName, activity
d.app_current()
2.1 启动应用( 默认的这种方法是先通过atx-agent解析apk包的mainActivity,然后调用am start -n $package/$activity
启动)
d.app_start("com.example.app")
2.2. 通过指定main activity的方式启动应用,等价于调用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
启动,这种方法有个副作用,它自动会将手机的旋转锁定给关掉
d.app_start("com.example.hello_world", use_monkey=True)
- 启动应用前停止此应用
d.app_start("com.example.app", stop=True)
4.1 停止应用, 等价于am force-stop
,此方法会丢失应用数据
d.app_stop("com.example.app")
4.2 停止应用, 等价于pm clear
d.app_clear('com.example.hello_world')
4.3 停止所有应用
d.app_stop_all()
4.4 停止所有应用,除了某个应用
d.app_stop_all(excludes=['com.examples.demo'])
- 得到APP图标
img = d.app_icon("com.examples.demo") img.save("icon.png")
- 列出所有运行中的应用
d.app_list_running()
- 确定APP是否启动,也可以通过Session来判断
pid = d.app_wait("com.example.android") # 等待应用运行, 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) # 等待应用前台运行d.app_wait("com.example.android", timeout=20.0) # 最长等待时间20s(默认)ord.wait_activity(".ApiDemos", timeout=10) # default timeout 10.0 seconds
Session操作
一般用于测试某个特定的APP,首先将某个APP设定为一个Session,所有的操作都基于此Session,当Session退出时,代表APP退出。
- 启动应用并获取session
session的用途是操作的同时监控应用是否闪退,当闪退时操作,会抛出SessionBrokenError
sess = d.session("com.example.app") # start app
- 停止或重启session,即app
sess.close() # 停止app sess.restart() # 冷启app
- python with 功能,开启某个APP执行某个操作后,自动退出某个session
with d.session("com.netease.cloudmusic") as sess: sess(text="Play").click()
4.1 当APP已运行时自动跳过启动
# launch app if not running, skip launch if already runningsess = d.session("com.netease.cloudmusic", attach=True)
4.2 当某个APP没有启动时,报错
# raise SessionBrokenError if not runningsess = d.session("com.netease.cloudmusic", attach=True, strict=True)
5.1 确定session对应的APP是否运行
# check if session is ok. # Warning: function name may change in the future sess.running() # True or False
5.2 确定session对应的APP是否运行,当不在运行将报错
# 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提取:
用于获取Android当前的截图和界面元素。
- 截图
# 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 firstimage = 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 firstimport cv2 image = d.screenshot(format='opencv')cv2.imwrite('home.jpg', image)# get raw jpeg dataimagebin = d.screenshot(format='raw')open("some.jpg", "wb").write(imagebin)
- 获取hierarchy
# get the UI hierarchy dump content (unicoded). xml = d.dump_hierarchy()
模拟触控操作:
用于模拟用户对手机的点击或滑动等操作
1.1 XY坐标点击
d.click(10, 20)
1.2 XY坐标双击
d.double_click(x, y)d.double_click(x, y, 0.1) # default duration between two click is 0.1s
1.3 长按某个坐标
d.long_click(x, y)d.long_click(x, y, 0.5) # long click 0.5s (default)
- 通过元素中的Text信息来点击,程序会点击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))
- 整个屏幕右滑动
d.swipe_ext("right")
- 屏幕右滑,滑动距离为屏幕宽度的90%
d.swipe_ext("right", scale=0.9)
- 从一个坐标拖拽到另一个坐标
d.drag(sx, sy, ex, ey)d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
- 模拟按下后的连续操作,如九宫格解锁
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 edged(text="Settings").pinch_out()ord().pinch_in(percent=100, steps=10)d().pinch_out()
硬按键操作
用于模拟用户对手机硬按键或系统按键的操作。
- 模拟按 Home 或 Back 键
目前支持以下关键字,但并非所有设备都支持:
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
- 模拟输入,需要光标已经在输入框中才可以
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") # 模拟输入法的搜索
- 清空输入框
d.clear_text()
执行ADB shell命令
直接通过Python来执行ADB shell中的指令,并得到反馈。
- 执行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设备中
# push to a folderd.push("foo.txt", "/sdcard/")# push and renamed.push("foo.txt", "/sdcard/bar.txt")# push fileobjwith open("foo.txt", 'rb') as f: d.push(f, "/sdcard/")# push and change file access moded.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最为关键的核心功能,测试者可以根据界面中的元素来判断当前画面是否符合预期或基于界面元素进行点按滑动等操作。
目前Uiautomator2支持以下种类的关键字参数:
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
举个例子,测试者可以通过以上关键字的组合,来实现特定界面元素的定位,如下面这段代码是要求UT2去点击界面中,元素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()
除了,可以使用关键字的组合来限定特定UI元素,UT2也支持通过子节点或兄弟节点来限定特定UI元素。如下面这几段代码分别是通过某个元素,获取其子元素或同胞元素中的信息或进行后续操作。
# 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")
也可以根据子节点的Text或 Description或Instance来定位元素, 特别提下下面代码中的这个allow_scroll_search功能,它调用UT2自动滚动直到找到对应元素:
# 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 searchd(className="android.widget.ListView", resourceId="android:id/list") \.child_by_text( "Bluetooth", allow_scroll_search=True, className="android.widget.LinearLayout" )
下面有另一个实例来展示UT2的定位,以下为Android的系统设置界面及它的hierarchy:
系统设置界面:
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的开关。
d(className="android.widget.ListView", resourceId="android:id/list") \ .child_by_text("Wi‑Fi", className="android.widget.LinearLayout") \ .child(className="android.widget.Switch") \ .click()
也可以通过,位置关系来定位元素:
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来定位,比如一个界面中有多个switch,我们可以通过下面的形式来定位是第一个还是第二个
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 # ...
- 等待某个元素出现
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(默认) d.xpath("立即开户").wait(timeout=10) # 修改默认等待时间
# xpath操作 d.xpath("立即开户").click() # 包含查找等待+点击操作,匹配text或者description等于立即开户的按钮 d.xpath("//*[@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
- 等待某个元素出现或消失
# 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
- 默认控件等待时间(原生操作,xpath插件的等待时间)
d.settings['wait_timeout'] = 20.0 or d.implicitly_wait(20.0)
- 点击的等待延时
d.click_post_delay = 1.5 # default no delay
- 配置accessibility服务的最大空闲时间,超时将自动释放。默认3分钟。(如果两个步骤需要等待较长时间,且不希望下一次发送指令时重启UT2,则可以将此时间加大)
d.set_new_command_timeout(300)
守护
用于处理非预期的弹出框,如崩溃窗口,一些确定或取消弹出框。
- 监控弹窗(在线程中监控)
# 常用写法,注册匿名监控d.watcher.when("安装").click()# 注册名为ANR的监控,当出现ANR和Force Close时,点击Force Closed.watcher("ANR").when(xpath="ANR").when("Force Close").click()# 其他回调例子d.watcher.when("抢红包").press("back")d.watcher.when("//*[@text = 'Out of memory']").call(lambda d: d.shell('am force-stop com.im.qq'))# 移除ANR的监控d.watcher.remove("ANR")# 移除所有的监控d.watcher.remove()# 开始后台监控d.watcher.start()d.watcher.start(2.0) # 默认监控间隔2.0s# 强制运行所有监控d.watcher.run()# 停止监控d.watcher.stop()# 停止并移除所有的监控,常用于初始化d.watcher.reset()
插件
Performance 性能采集(记录CPU,RAM等数据)
https://github.com/openatx/uiautomator2/blob/6e0d75d778a86c626df778e0432c8e339e3d9be4/uiautomator2/ext/perf/README.md
Aircv 图像比对插件(有较多限制,推荐自己单独截图后调用Aircv来实现图像比对或点击等功能,或用ATX来实现)
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