在 Android 中使用 UIAutomator 执行自动化任务

63845153 7年前
   <p>用代码来替代任何重复性的工作,一直是我的追求。这周,我又写了一段脚本,让我从无尽的重复工作中解脱了出来。</p>    <h2>问题 & 需求</h2>    <p>最近在修 bug 的时候,为了达到一个合适的测试环境,我需要一直要重复执行这些操作:</p>    <ul>     <li>完全关闭 App</li>     <li>打开应用设置页清空 App 数据</li>     <li>在权限界面打开「存储空间」权限</li>     <li>启动 App,登录帐号</li>    </ul>    <p>主要过程就是不断地点击屏幕,等待界面切换,输入内容。我就想让代码来帮忙点击这些固定的位置,然后输入预设的内容,自动化完成这些无聊的操作。</p>    <p>整理一下,需要实现的功能大概就是:</p>    <ul>     <li>根据文本信息确定点击位置</li>     <li>执行点击操作</li>     <li>执行输入操作</li>     <li>启动一些界面</li>    </ul>    <h2>解决方案</h2>    <p>要用命令来控制 Android 设备,那肯定是选用 adb 了。GitHub 上有个叫 awesome-adb 的项目,列举了 adb 的各种用法,其中有提到 调起 Activity 和 模拟按键输入 的操作。另外查阅资料得知 uiautomator 命令可以获取屏幕中的控件信息,从中可以提取到控件的位置,用于模拟点击。下面用 Python 来实现整个流程。</p>    <p>这里先提一下两个工具函数,方便后续的代码展示。一个是用来执行 adb 的,另外一个是装饰器,在目标函数执行完之后休眠一会,等待 UI 的响应。</p>    <pre>  <code class="language-python">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</code></pre>    <h3>根据文本信息点击屏幕</h3>    <p>需要先用 uiautomator 命令来获取屏幕信息。</p>    <pre>  <code class="language-python">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)</code></pre>    <p>得到的 XML 文件是由 node 节点组成的,其中的 text 和 bounds 属性是我们需要的。可以根据文本去匹配到相应的 node 节点,然后解析出控件的边界信息,后续只要在这个边界内点击就可以模拟真实的操作了。</p>    <pre>  <code class="language-python"><?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></code></pre>    <p>这里先用了 cat 命令直接读出 XML 的内容,然后用 lxml 解析匹配目标节点。后面用正则表达式提取出边界的坐标点,然后直接计算出边界矩形的中心点。</p>    <pre>  <code class="language-python">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</code></pre>    <p>再用 input 命令,结合上面的几个函数,可以完成这个需求了。</p>    <pre>  <code class="language-python">@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)</code></pre>    <h3>模拟输入</h3>    <p>这个比较简单,直接使用 input text 命令。另外还实现了模拟按返回键。</p>    <pre>  <code class="language-python">@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')</code></pre>    <h3>停止应用、清除数据、启动 Activity</h3>    <p>这一些命令操作,按照 awesome-adb 的文档执行就好。</p>    <pre>  <code class="language-python">@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)</code></pre>    <p>另外,打开指定应用的设置界面,需要指定 ACTION 和 DATA。</p>    <pre>  <code class="language-python">@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))</code></pre>    <h3>拼装整个流程</h3>    <pre>  <code class="language-python">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)</code></pre>    <p>前面把各种操作写好,主流程就很清晰啦,照着手动操作的过程,一步一步调函数就好了。</p>    <h3>效果图</h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3a3698aeda2074ec00dc4e5120d14fed.gif"></p>    <h2>Reference</h2>    <ul>     <li><a href="/misc/goto?guid=4959714064322746168" rel="nofollow,noindex">awesome-adb</a></li>     <li><a href="/misc/goto?guid=4959729356464140765" rel="nofollow,noindex">通过 python 调用 adb 命令实现用元素名称、id、class 定位元素 · TesterHome</a></li>    </ul>    <p> </p>    <p>来自:http://brucezz.itscoder.com/articles/2016/12/05/use-uiautomator-in-android/</p>    <p> </p>