在 Android 中使用 UIAutomator 執行自動化任務
用代碼來替代任何重復性的工作,一直是我的追求。這周,我又寫了一段腳本,讓我從無盡的重復工作中解脫了出來。
問題 & 需求
最近在修 bug 的時候,為了達到一個合適的測試環境,我需要一直要重復執行這些操作:
- 完全關閉 App
- 打開應用設置頁清空 App 數據
- 在權限界面打開「存儲空間」權限
- 啟動 App,登錄帳號
主要過程就是不斷地點擊屏幕,等待界面切換,輸入內容。我就想讓代碼來幫忙點擊這些固定的位置,然后輸入預設的內容,自動化完成這些無聊的操作。
整理一下,需要實現的功能大概就是:
- 根據文本信息確定點擊位置
- 執行點擊操作
- 執行輸入操作
- 啟動一些界面
解決方案
要用命令來控制 Android 設備,那肯定是選用 adb 了。GitHub 上有個叫 awesome-adb 的項目,列舉了 adb 的各種用法,其中有提到 調起 Activity 和 模擬按鍵輸入 的操作。另外查閱資料得知 uiautomator 命令可以獲取屏幕中的控件信息,從中可以提取到控件的位置,用于模擬點擊。下面用 Python 來實現整個流程。
這里先提一下兩個工具函數,方便后續的代碼展示。一個是用來執行 adb 的,另外一個是裝飾器,在目標函數執行完之后休眠一會,等待 UI 的響應。
def run(cmd):
"""執行 adb 命令"""
# adb <CMD>
return subprocess.check_output(('adb %s' % cmd).split(' '))
def sleep_later(duration=0.5):
"""裝飾器:在函數執行完成之后休眠等待一段時間"""
def wrapper(func):
def do(*args, **kwargs):
func(*args, **kwargs)
if 'duration' in kwargs.keys():
time.sleep(kwargs['duration'])
else:
time.sleep(duration)
return do
return wrapper
根據文本信息點擊屏幕
需要先用 uiautomator 命令來獲取屏幕信息。
dump_file = '/sdcard/window_dump.xml'
def dump_layout():
print 'Dump window layouts'
# adb shell uiautomator dump <FILE>
run('shell uiautomator dump %s' % dump_file)
得到的 XML 文件是由 node 節點組成的,其中的 text 和 bounds 屬性是我們需要的。可以根據文本去匹配到相應的 node 節點,然后解析出控件的邊界信息,后續只要在這個邊界內點擊就可以模擬真實的操作了。
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node
index="0"
text=""
resource-id=""
class="android.widget.FrameLayout"
package="com.teslacoilsw.launcher"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[0,0][1080,1920]">
<!-- many nodes -->
</node>
</hierarchy>
這里先用了 cat 命令直接讀出 XML 的內容,然后用 lxml 解析匹配目標節點。后面用正則表達式提取出邊界的坐標點,然后直接計算出邊界矩形的中心點。
def parse_bounds(text):
# adb shell cat /sdcard/window_dump.xml
dumps = run('shell cat %s' % dump_file)
nodes = etree.XML(dumps)
return nodes.xpath(u'//node[@text="%s"]/@bounds' % (text))[0]
bounds_pattern = re.compile(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]')
def point_in_bounds(bounds):
"""
'[42,1023][126,1080]'
"""
points = bounds_pattern.match(bounds).groups()
points = map(int, points)
return (points[0] + points[2]) / 2, (points[1] + points[3]) / 2
再用 input 命令,結合上面的幾個函數,可以完成這個需求了。
@sleep_later()
def click_with_keyword(keyword, dump=True, **kwargs):
# 有的屏幕需要多次點擊時,dump 可以設置為 False,使用上一次的屏幕數據
if dump:
dump_layout()
bounds = parse_bounds(keyword)
point = point_in_bounds(bounds)
print 'Click "%s" (%d, %d)' % (keyword, point[0], point[1])
# adb shell input tap <x> <y>
run('shell input tap %d %d' % point)
模擬輸入
這個比較簡單,直接使用 input text 命令。另外還實現了模擬按返回鍵。
@sleep_later()
def keyboard_input(text):
# adb shell input text <string>
run('shell input text %s' % text)
@sleep_later()
def keyboard_back():
# adb shell input keyevent 4
run('shell input keyevent 4')
停止應用、清除數據、啟動 Activity
這一些命令操作,按照 awesome-adb 的文檔執行就好。
@sleep_later()
def force_stop(package):
print 'Force stop %s' % package
# adb shell am force-stop <package>
run('shell am force-stop %s' % package)
@sleep_later(0.5)
def start_activity(activity):
print 'Start activity %s' % activity
# adb shell am start -n <activity>
run('shell am start -n %s' % activity)
@sleep_later(0.5)
def clear_data(package):
print 'Clear app data: %s' % package
# adb shell pm clear <package>
run('shell pm clear %s' % package)
另外,打開指定應用的設置界面,需要指定 ACTION 和 DATA。
@sleep_later()
def open_app_detail(package):
print 'Open application detail setting: %s' % package
# adb shell am start -a ACTION -d DATA
intent_action = 'android.settings.APPLICATION_DETAILS_SETTINGS'
intent_data = 'package:%s' % package
run('shell am start -a %s -d %s' % (intent_action, intent_data))
拼裝整個流程
target_package = 'com.mingdao'
launcher_activity = 'com.mingdao/.presentation.ui.login.WelcomeActivity'
def main():
username, password = sys.argv[1:3]
# 停止應用
force_stop(target_package)
# 清除數據
clear_data(target_package)
# 啟動應用設置頁
open_app_detail(target_package)
# 進入權限頁
click_with_keyword(u'權限')
# 打開「存儲控件權限」
click_with_keyword(u'存儲空間')
# 按一下返回
keyboard_back()
# 啟動 app
start_activity(launcher_activity)
# 歡迎頁跳過
click_with_keyword(u'跳過')
# 選中「帳號」輸入框
click_with_keyword(u'手機或郵箱', duration=0)
# 輸入帳號
keyboard_input(username)
# 選中「密碼」輸入框
click_with_keyword(u'密碼', dump=False, duration=0)
# 輸入密碼
keyboard_input(password)
# 點一下登錄按鈕
click_with_keyword(u'登錄', dump=False)
前面把各種操作寫好,主流程就很清晰啦,照著手動操作的過程,一步一步調函數就好了。
效果圖

Reference
來自:http://brucezz.itscoder.com/articles/2016/12/05/use-uiautomator-in-android/