Android自动化的一般方法

1

背景

https://mp.weixin.qq.com/s/B7o3JSkYI_9c4RivatsZKQ

Android自动化包含自动化测试和第三方App的自动化运行,这里的自动化测试主要指的是纯粹的黑盒测试,即在完全不了解代码逻辑的情况下编写的测试用例,可以代替人工完成重复性的工作,提高效率;而第三方App的自动化指的是为完成某一目标或获取指定内容而进行的自动化运行。随着技术的不断发展,App安全性的不断提升,破解App或者对App完成抓包的成本越来越高,通过自动化,能够在不破解第三方App的情况下获取App中的部分信息。除此,还可以通过自动化实现一些小工具,比如,微信自动抢红包插件(作者已将代码开源),跳过apk授权页(源码见下文)等。


自动化需要模拟用户行为,按照事先设计好的路径,完成固定的流程,用户行为可能包含启动App、点击、滚动、输入、返回等常规操作,一个完整的流程是这些事件的有序组合。为了使自动化程序更加稳定,即在程序进入异常或错误的页面时仍然能够正常响应,并逐步回到正常流程,事件也可能是基于页面当前状态的。一个基本的运行顺序应该是,获取页面状态,即页面名称、页面内容、页面结构,保存相应状态,如计数器、输出信息等,然后根据页面状态和保存状态进行相应的操作,然后触发页面变化,再获取新的状态,重复这一过程。

2

自动化工具一:辅助服务

Android系统中运行的每个App都是一个单独进程,因而除了攻破Android系统,否则不可能在运行时获取其他App的运行时内存,进而不能够直接操作其他App。然而,Android系统提供了辅助服务(AccessibilityService),辅助服务是Android系统提供的一种设计用来帮助残疾人的工具。作为Android系统提供的一种标准服务,辅助服务能够监控Android系统中其他App中发生的事件,并可以获取页面结构和操作页面,还可以进一步进行语音提示,从而帮助残疾人更好的使用Android应用。能够被监控的事件基本覆盖了App内部发生的所有事件,如页面的重绘、点击、滚动、焦点变化等。下面借助辅助服务来完成一个能够自动跳过授权页安装apk的小工具,以说明辅助服务的使用方式。

 

自动授权apk安装小工具

安装apk时,会首先弹出授权页,这个时候需要用户点击安装按钮才能够开始安装。在需要同时安装多个apk的时候,授权过程会显得略为繁琐。因此,可以通过一个在授权页自动点击安装的小工具来完成安装。首先通过以下代码来向系统提交安装apk的请求:

扫描二维码关注公众号,回复: 1931304 查看本文章

这里使用了一个test.apk,放到了SD卡的根目录下,然后在代码中获取apk路径,转换成URI,设置参数application/vnd.android.package-archive,启动安装程序。Android系统安装apk的过程是由com.android.packageinstaller这个包来管理的,所以需要监控来自这个包的所有事件。辅助服务可以指定要监控App的包名,如下为实现这个小工具的服务配置,需要放到一个单独的xml文件中:

辅助服务反馈的事件有很多种,这里只选择其中两种,获取的事件是typeWindowStateChanged和typeWindowContentChanged,当窗体状态或窗体内容变化,也就是页面发生变化的时候,当前辅助服务会收到事件。android:description指定的内容会在开启辅助服务的时候,展示在辅助服务设置页面。这个配置需要在AndroidManifest.xml中引入:

辅助服务在AndroidManifest.xml中的声明需要包含android.permission.BIND_ACCESSIBILITY_SERVICE权限、过滤器和特定的meta-data,配置的xml在meta-data中的android:resource中引入。然后定义一个服务,继承AccessibilityService,这个是所有辅助服务的基类,所有的辅助服务均由系统管理,这里只需要实现特定的接口:

自定义服务需要实现onAccessibilityEvent(AccessibilityEvent event)来处理监控的App反馈的事件,通过getRootInActiveWindow()来获取页面的根节点信息,这个根节点信息是一个AccessibilityNodeInfo的实例,可以通过其获得节点及子节点的状态信息,如展示的文字getText()、描述字段getContentDescription(),同时也可以通过这些信息定位子节点。节点的点击操作通过performAction这个函数完成。最后,启动系统的辅助服务列表页:

所有的辅助服务都会展示在这个列表页中,在辅助服务设置页面中,找到当前应用,进入辅助服务配置页,点击开启服务。之后启动安装即可完成自动授权过程。在不同Android设备上可能表现不同,这里的逻辑是:监控系统安装页面的事件,然后在页面中找到带有安装文案的节点,执行点击操作。不同设备安装页面的页面结构可能不尽相同,要想兼容各种各样的Android设备可以使用Android SDK提供的层级查看工具uiautomatorviewer来分析App的页面结构,然后确定搜索和点击路径,如英文系统可能点击的文案是INSTALL。

 

辅助服务自动化的缺陷

辅助服务在进行自动化时,存在以下缺陷:

1. 辅助服务本身是一个Service,这个Service被系统管理,可能因为很多原因挂掉,则自动化停止。

2. 辅助服务无法自动化WebView。


3

自动化工具二:uiautomator

辅助服务作为Android系统中的一种标准服务,很难通过脚本去配置和控制,也不容易监控。另外一个进行自动化的思路是通过自动化测试工具,这要求自动化测试工具必须是完全黑盒的,也即在不了解代码逻辑、代码接口的前提下,能够顺利的编写用例,实现必要的功能。


uiautomator是Google提供的为Android编写UI测试用例的自动化工具,开发时,可以按照如下方式使用Gradle添加依赖:

也可以直接依赖uiautomator.jar,uiautomator.jar在Android SDK目录下的platforms/[android-version]/目录中,提供编写用例所需要的工具类。运行用例必须在Android系统中,可以打成Java的可执行文件jar包,也可以打包成Android标准的test apk,执行需要依赖ADB工具。除此,uiautomator还可以作为获取Android设备页面布局的工具,Android SDK目录tools/bin/下的可执行文件uiautomatorviewer是uiautomator获取首页页面布局的图形界面版本,能够方便的查看页面布局,在分析第三方App的时候,会经常用到。


使用命令行


uiautomator的测试用例可以打包运行,也可以通过adb命令运行。uiautomator主要支持三个命令,runtest用来执行测试用例,dump命令获取页面信息,events可以把发生的所有的Accessibility事件(即上文中辅助服务能够获取和操作的事件)输出。

adb的help命令会详细输出每个指令的具体功能。这里首先使用dump命令,通过脚本,实现基本的自动化功能。dump命令会把当前Android设备展示的页面结构以xml的方式输出到文件,所以需要做的是解析xml,获取各个节点信息,节点信息会包含位置,之后配合adb的input指令,完成基本的操作,实现自动化。下面利用dump命令完成简单的自动化脚本,脚本使用Python编写。首先通过adb命令获取页面布局:

页面布局的xml文件会把Android设备的页面解析成一个由node节点构成的树结构,每个node节点会固定的包含位置、点击、View的类名等信息,如下是一个根节点的信息:

节点信息包含了开发中常会用到的属性,由此可以看到,虽然限于Android内存管理模型无法获取页面上节点的实际内存对象,但是仍然可以借助uiautomator来获取节点的状态。bounds是当前节点在屏幕中的位置,为了操作节点,需要使用位置信息确定指定View的位置,然后通过adb的input命令完成具体的操作。解析xml,获取节点位置,完成点击、滚动、输入等操作:

adb的input的命令十分丰富,能够满足绝大多数的交互操作,tap模拟点击,swipe模拟手指在屏幕上移动,text模拟文字输入,除此比较常用到的还有keyevent,用于模拟按键操作,按键包含了返回键、home键、软键盘以及外接键盘的按键。如果需要支持多设备,需要为adb命令加上-s选项,指定设备序列号。通过拼接这些操作,能够完成简单的自动化程序。


利用dump方式的缺陷

dump指令耗时过长,严重影响自动化效率。


用例方式

uiautomator的主要功能是用来执行测试用例,测试用例使用Java编写,开发时依赖Android SDK。可以通过执行用例的方式来进行自动化操作第三方App,用例一般的开发方式是编写用例函数,打包成jar或者test apk。写好的用例不能在本地直接运行,如果是jar包,必须推送到Android设备上通过adb运行,如果是test apk,需安装运行。test apk是指用来执行测试的apk,会在build的时候生成,如下为SDK中uiautomator工具API中UiDevice的部分源码,可以看到源码没有实现,只是提供了接口,因此用例无法脱离设备运行。

uiautomator提供了获取设备和页面节点信息的API,借助UiDevice来获取设备信息,借助UiObject、UiObject2获取页面上的节点信息。


通过用例方式完成自动化程序的步骤为:

1. 分析要自动化的App的页面结构,设计自动化路径

2. 编写自动化程序(实际上是一个测试用例)

打包,push到Android设备上(apk通过adb am instrument命令,jar包通过adb shell uiautomator runtest命令运行)

3. 执行自动化程序


使用这种方式执行自动化效率相比dump指令方式明显有所提高,如下为通过这种方式自动获取当前用户已关注微信公众号列表的程序,微信使用6.6.5版本:

运行uiautomator的用例需要使用AndroidJUnit4,用例的入口方法要用org.junit.Test这个标签修饰,多个用例之间执行顺序无法保证,所以不能通过多用例的方式进行自动化。这里首先启动微信的首页,然后通过文案查找的方式进入公众号列表页面,遍历公众号条目,通过uiautomatorviewer可以看到公众号名称对应的resourceId是com.tencent.mm:id/z1(不同版本、渠道的微信apk可能对应的resourceId是不一样的,因为微信使用AndResGuard进行了资源混淆),通过UiSelector获取节点,然后通过getText获取名称,之后进行滚动。遍历的终止条件是:当公众号列表页面滚动到底部之后,一定会展示“xx个公众号”这样的文案,检测到这个文案,认为列表已经滚到底部。UiObject的对象是长时间有效的,所以当执行滚动之后,使用listObj对象仍然可以获取到当前ListView的最新节点数量。

最后,打包执行。通过adb命令运行:

runtest命令通过-c指定要执行用例的主类路径。instrument命令用来执行Android的InstrumentationTest,-w选项表明等待直到用例运行结束,-e表示添加key-value形式的参数,设置用例的入口为com.example.WeixinAutomator,即用例的class路径,最后是用来执行用例的AndroidJUnitRunner的类路径。


用例方式实现自动化的缺陷

无法自动化WebView


4

集成adb

ADB是Google提供的能够和Android设备进行交互的命令行工具。如上所示的小程序均是基于顺序的,各个事件按照事先设计好的顺序,一个一个执行,意味着在执行过程中,如果不出现意外,能够正常运行,但如果出现诸如断网、网速过慢、手机卡顿、app崩溃之类的问题后,自动化就会异常停止。所以自动化想要稳定运行,应该是基于页面状态的。基于页面状态是指,任何一个页面,不论其当前展示的内容是什么,是断网还是未断网,是否加载失败,都应对应于一种状态,程序应该能够处理这种状态,从而即使偏离了原有的流程,也能够慢慢的再回到正常的流程。这就要求完整的监控机制。除此,自动化通常运行独立,不会有人工实时的监测,也即出现问题,应该能够事后发现和修复问题。这就要求要有完整的日志系统。要想实现这些,需要依赖adb。同时,不论是辅助服务还是uiautomator,虽然能一定程度的解析WebView的结构,但都无法操作WebView。通过adb的input指令,可以模拟用户的各种操作,这也使得自动化WebView成为可能。


由于大多数未root的Android设备都无法进行远端adb连接,也即无法通过远程的方式执行adb命令,所以这里考虑使用套接字。


在宿主设备建立套接字服务

使用adb调试Android设备通常需要使用PC或者服务器,这里暂且把其称作宿主设备。在宿主设备上建立套接字服务,则请求方为Android设备。宿主设备辅助完成自动化功能,并实时监控Android设备的自动化状态。如果要确保在Android设备上运行的程序能够稳定运行,应设立超时机制,如果超时未收到来自Android设备的事件信息,则应进行相应的恢复或重启操作。


Android设备本身是一个Linux系统,若想要在这个Linux系统和宿主设备操作系统之间建立套接字通信,首先要进行端口转发。在宿主设备上建立套接字服务,Android设备向宿主设备发送请求,则需要把Android设备上的某一空闲端口上的内容映射到宿主的某一空闲端口上,如把Android设备的8484接口转发到宿主设备的8484端口上,需要执行:

可以使用:

查看已经映射的端口。然后在宿主设备上建立套接字服务,这里仍然使用Python:

这里为了简便,利用Python的SocketServer.ThreadingMixIn启动一个多线程处理事件的套接字服务,AndroidCommandHandler这里的解析和处理过程会在一个单独的线程中进行,使用双换行作为通信的终止符,指令接收完毕可以按照一些实现定义好的协议去做相应的处理,比如执行adb操作、记录日志等。一种典型的应用场景是针对WebView,因为辅助服务和uiautomator都无法操作WebView,所以可以先在Android设备上通过辅助服务或uiautomator分析WebView的节点结构,然后把节点信息通过套接字发送到宿主设备上,再在宿主设备通过adb的input的命令完成具体的操作。


在Android设备上建立套接字服务

在Android设备上建立套接字服务,则宿主设备作为请求方。这种方式可以通过套接字把原本依赖设备的处理过程放到宿主设备上,也即任何一次查询和操作都是由宿主设备发起的,这样想要进行的自动化流程完全可以在宿主设备上编写程序来处理。开源自动化测试框架Appium即通过这种方式,能够在完全不需要修改源码条件下,执行自动化测试用例,并且,支持任意语言来编写自动化测试用例。因为基于这样的结构,任何开发语言都能使用套接字来和Android设备通信,从而获取页面信息、操作Android设备。


Appium使用Node.js搭建,同时支持iOS和Android的自动化测试,为Android提供底层接口的是appium-android-bootstrap,通过源码,可以看到其是依赖uiautomator实现的。不过Appium框架本质上是用来进行自动化测试的,框架功能强大,但是却不能很好的应用于第三方apk,其主要是用于debug版本的apk的。如使用Appium尝试自动化微信,在启动Appium服务的时候,会产生一条错误信息,导致自动化停止。阅读Appium源码之后,发现原因是Appium在安装apk之前会使用aapt命令来获取apk的基本信息,而这条命令在微信的apk上执行失败。即便如此,Appium的思路仍然是值得借鉴的,下面简单说明Appium这种利用套接字来开放uiautomator接口的方式。


首先需要把宿主设备上的某一端口内容,全部转发到Android设备上,使用adb命令:

由于分析的Appium的源代码,所以使用Appium的端口号,Appium在宿主设备上的服务默认是使用4724作为和Android设备进行通信的端口。查看已经转发的接口:

adb的forward命令和reverse命令用法基本一致,只是方向是相反的,forward是把宿主设备上的端口转发到Android设备上,reverse是把Android设备的端口转发到宿主设备上。然后利用一个测试用例,启动套接字服务:

Appium在Android设备上启动一个套接字服务监听来自端口4724的所有消息,套接字服务会做一个无限循环,也即是一个持续不会中断的测试用例,这个用例会在收到来自宿主设备的命令时,执行相应的操作。然后是对于消息的处理:

client是一个(java.net.Socket)对象,调用accept建立套接字连接,获取套接字内容。内容由handleClientData来处理,handleClientData只是将字符串解析成具体的指令然后调用runCommand来执行具体的命令:

来自宿主设备的指令可以关闭当前服务,也可以执行相应的Action。Appium定义的Action有很多,节点相关的节点Action会以element:开头,之后才是具体的Action内容,Appium以这种方式来确定该操作是否需要节点信息,Action的内容可能包括节点寻找、执行具体的点击、滚动、输入等等,Appium定义的所有Action如下所示:

Appium使用的基本都是uiautomator的接口。Appium是支持WebView的测试用例的,只是需要App的WebView开启Debug模式,同样这在第三方App上是无法做到的,所以针对这种情况,仍然要使用套接字通知宿主设备之后,利用adb的input方式来进行操作。

5

总结

如上介绍了自动化Android App过程中可能用到的几个工具,将这些工具搭配使用能够互补的实现原本无法实现的功能,同时配合宿主设备,搭建完整的稳定的Android自动化系统。经过一段时间的实践发现,通过辅助服务来获取事件和分析节点结构的效率是最高的,uiautomator是功能和API最丰富的,而脚本和adb命令是调试和控制最方便的。通过自动化能够做到定期监控其他App的状态,从第三方App中获取基本信息,而无需进行复杂繁琐的逆向分析。同时,自动化能够很好的解决重复性工作,节省人力,来进行更多的工作,当然,也能够通过自动化完成对自身App的基本测试,覆盖主要流程,然后可以用来作为上线之前的一次安全检查。

快,关注这个公众号,一起涨姿势~

猜你喜欢

转载自blog.csdn.net/sinat_17775997/article/details/80854902