好久没有好好写博文了,20年基本都在搞自己的事情,白天上班,晚上回去写写脚本。虽然没有学到很多东西,但还是想把自己在自动化脚本这块的一点点心得体会,写出来分享一下哈。
前言
之前有分享过关于 android 辅助服务的一些介绍文章,目前网上相关的文章肯定也是数不胜数,不再稀缺了,这里贴一下之前分享的文章的链接,还不熟悉的朋友可以看看之前的文章。如下:
基础方法
辅助服务默认提供两个核心方法,可通过 text 或 id,寻找界面节点,具体使用方法可以参考上面链接
1、findAccessibilityNodeInfosByText
2、findAccessibilityNodeInfosByViewId
但实践过的朋友,应该知道这两个方法是有局限性的。就是有时候我们 dump 一个界面的时候,会出现 text 、 id 都没有的情况,那这种情况我们要怎么去定位控件节点呢? 请继续往下看
扩展方法
在没有 text、id的时候,我们是可以通过根节点信息,循环遍历,找到我们想到的控件,例如根据属性 desc、classname、bounds等,来定位我们想要的节点。那其实有没有现成的一些代码能够供我们使用呢?
做过 android 的朋友应该知道自动化测试框架 Uiautomator,其中提供了非常多好用的方法和接口。而且 uiautomator 核心实现也是基于辅助服务搞得,这里参考 Auto.js(目前市面上比较流行的自动化脚本项目) 的源码,github 地址是 github.com/hyb1996/Aut…
站在别人的肩膀上,看看是怎么把 uiautomator 与 accessibilityService 结合起来的。打开 Auto.js 的源码,找到 UiSelector 这个类,可以看到它的构造函数传入 AccessibilityBridge,这个类可以理解为桥梁,getService() 方法联通UiSelector 和 accessibilityService。
open class UiSelector(accessibilityBridge: AccessibilityBridge?, allocator: AccessibilityNodeInfoAllocator?) {
private val mSelector = Selector()
private var mSearchAlgorithm: SearchAlgorithm = DFS
open fun id(id: String): UiSelector {
mSelector.add(IdFilter.equals(id))
return this
}
fun idContains(str: String): UiSelector {
mSelector.add(IdFilter.contains(str))
return this
}
//省略部分代码...
public abstract AccessibilityService getService();
//核心方法
protected open fun findImpl(max: Int): UiObjectCollection {
val start = System.currentTimeMillis()
val roots = mAccessibilityBridge!!.windowRoots()
if (BuildConfig.DEBUG) Log.w(TAG, "find: roots = $roots")
if (roots.isEmpty()) {
return EMPTY
}
val result: MutableList<UiObject?> = ArrayList()
for (root in roots) {
if (root == null) {
continue
}
if (root.packageName != null && mAccessibilityBridge!!.config.whiteListContains(root.packageName.toString())) {
Log.d(TAG, "package in white list, return null")
return EMPTY
}
result.addAll(findAndReturnList(createRoot(root, mAllocator), max - result.size))
if (result.size >= max) {
break
}
}
val end = System.currentTimeMillis()
return of(result)
}
//实际查找方法
fun findAndReturnList(node: UiObject, max: Int = Int.MAX_VALUE): List<UiObject> {
return mSearchAlgorithm.search(node, mSelector, max)
}
}
这里的 mSearchAlgorithm 是搜索接口,分别对应 DFS(深度优先搜索算法) 、BFS(广度优先搜索) 算法,我们知道android视图,可以理解为“树”结构,通过这样查找能够快速的遍历节点。
object DFS : SearchAlgorithm {
override fun search(root: UiObject, filter: Filter, limit: Int): ArrayList<UiObject> {
val start = System.currentTimeMillis()
val result = ArrayList<UiObject>()
val stack = LinkedList<UiObject>()
stack.push(root)
while (stack.isNotEmpty()) {
val parent = stack.pop()
for (i in parent.childCount - 1 downTo 0) {
val child = parent.child(i) ?: continue
stack.push(child)
}
if (filter.filter(parent)) {
result.add(parent)
if (result.size >= limit) {
break
}
} else {
if (parent !== root) {
parent.recycle()
}
}
}
val end = System.currentTimeMillis()
if (BuildConfig.DEBUG)
Log.w("DFS", "耗时:" + (end - start) + "ms")
return result
}
}
另外我们可以看到,在遍历过程中,压栈的时候,留意到 Filter 接口,我们继续看看 Filter 是怎么实现的,可以看到 Filter 的作用,其实就是匹配 Selector 中的 mFilters 各个选择器的条件,只有都满足,才返回对应的节点。
interface Filter {
fun filter(node: UiObject): Boolean
}
class Selector : Filter {
private val mFilters = LinkedList<Filter>()
override fun filter(node: UiObject): Boolean {
for (filter in mFilters) {
if (!filter.filter(node)) {
return false
}
}
return true
}
fun add(filter: Filter) {
mFilters.add(filter)
}
override fun toString(): String {
val str = StringBuilder()
for (filter in mFilters) {
str.append(filter.toString()).append(".")
}
if (str.isNotEmpty()) {
str.deleteCharAt(str.length - 1)
}
return str.toString()
}
}
节点窗口
在做自动化的时候,有个地方需要注意的是窗口的获取,需要区分 getRootInActiveWindow() 和 getWindows() 方法 ,往往定位不到节点的时候,需要看看你这里的 window 是用的哪个方法,一个表示当前活跃的窗口,一个表示所有窗口。如:ViewPager 搭配 Fragment的情况,会出现层叠的情况,如果用第一个方法,可能会找不到节点信息。但如果用第二个方法,因为是在所有的 windows 里面搜索,因此花费的时间比仅一个窗口的时间长。
@Override
public AccessibilityNodeInfo getActiveRoot() {
return getRootInActiveWindow();
}
@Override
public List<AccessibilityWindowInfo> getActiveWindow() {
return getWindows();
}
结尾
文章学习了 auto.js 的是怎么在 uiautomator 的基础上扩展接口,以提供丰富的 api ,给用户使用,因为单单依靠 text 、id来定位节点,确实有点缺陷。
【注:文中源码学习分析使用】