当前位置: 代码迷 >> 综合 >> Android自动化之AccessibilityService模拟操作(经验总结)
  详细解决方案

Android自动化之AccessibilityService模拟操作(经验总结)

热度:87   发布时间:2023-12-12 07:38:59.0

有两种能实现后台运行并模拟操作安卓手机的方式,一种是使用adb命令模拟操作(参考:Android自动化之adb模拟操作(可实现按键精灵和手机输入法)),一种是AccessibilityService(参考:Android自动化之AccessibilityService模拟操作(快速集成))。
这篇笔记总结下使用AccessibilityService的一些经验。

经验总结

1、使用eclipse或android studio自带的Dump View Hierarchy for UI Automator来分析目标界面的结构,此方法看到的结构信息不完全准确,但很有参考价值。
2、每次完全退出应用后,该AccessibilityService服务都会被系统关闭,再次使用需要重新手动打开。已Root的手机,可以通过修改Setting的数据库来跳过授权操作。
3、触发监听后,部分业务逻辑是需要间隔一定时间再去处理的(eg:监听到包含listview的某一界面打开后,如果这时候立即去获取该listview,那么拿到的listview则有可能不能翻页,因为界面元素和属性的渲染是需要时间的,尤其是listview的数据加载更需要时间,间隔一段时间,等它们渲染完毕后,再去执行逻辑,则会避免很多奇怪问题)。
4、AccessibilityService方式无法点击listview中的item(界面上的listview本身的clickable属性可能就是false的,但却不影响item是否能被点击)。
5、模拟用户操作可以AccessibilityService方式和adb方式混用,通常做法是先AccessibilityService方式监听事件触发和获取目标控件位置,然后adb方式执行点击、长按等操作。
6、AccessibilityNodeInfo对象使用完后记得recycle,但要注意recycle的时机,因为java是对象引用,不要对象还没使用完就在被调用函数里给recycle了。
7、不能使用单例,在启动的时候,系统已经自动创建一个对象了,该服务完全由系统管理。
8、AccessibilityService提供了findAccessibilityNodeInfosByViewId和findAccessibilityNodeInfosByText两个api来直接查找控件,所以许多应用为了防止外挂和辅助的泛滥,采用了根据一定算法来动态生成界面id的方式和尽量用图片代替文字的方式来防止通用外挂的流通。eg:安卓版微信的界面id就是动态生成的。
9、应用使用动态界面id,并不是说就不能使用AccessibilityService来写自动化辅助了。动态id在应用安装并使用之后,就固定下来了,只要应用不卸载,每次打开同一界面得到的id都一样,只要你针对每台机子上的目标应用都在辅助里配好相对应的id,辅助仍然能够运行,而且只要目标应用没有重新安装,你就不需要重新配置。当然这种方式只适合特殊用户群体,你能接触到每一台手机且数量比较少的情况下,比如公司内部研发自用的微信群控辅助。
10、其实还可以用根据目标控件的类名来遍历界面的方式实现辅助的通用。
打个比方,一个界面有好几个输入框,且每个输入框的id都是动态的,那么我们如何保证在每一台机子上都能拿到指定的输入框呢。
我们可以使用循环遍历的方式来实现!
首先,输入框的类名是固定的,叫“android.widget.EditText”,这个是android系统定义好的,谁也没法改变(开发者自定义的输入框基本也都是继承的该类,用Dump View Hierarchy for UI Automator查看,如果是其他类,就用其他类的类名来判断);
其次,界面的布局结构是固定的,目标输入框在界面结构上的位置是固定的(如果是变化的,那也是有迹可循的,我们是可以写代码根据它的变化来搞事情的)。

具体操作是:手动随意在目标输入框中输入一些内容;用AccessibilityNodeInfo的getClassName();函数来得到目标界面元素的类名,拿此类名与输入框类名相比较;循环遍历出所有的输入框,得到一个AccessibilityNodeInfo对象的list;然后在该list中重复遍历调用findAccessibilityNodeInfosByText来查到之前在目标输入框中输入的那些内容,并输出它在这个输入框在list中的index,然后在代码里写死使用list的该index的AccessibilityNodeInfo对象,每次获取到的它就是我们要获取到的目标输入框,且在每一台机子上获取到的都是正确的、统一的。

附录几个遍历查找目标控件的代码:

遍历查找输入框

private static final String editTextClassName = "android.widget.EditText";
/**根据固定的类名循环查找到输入框,查找到目标输入框后即退出循环,适用于界面只有一个输入框的情况* @param node* @return*/
public AccessibilityNodeInfo recycleFindEditText(AccessibilityNodeInfo node) {//edittext下面必定没有子元素,所以放在此时判断if (node.getChildCount() == 0) {if (editTextClassName.equals(node.getClassName())) {Log.d(TAG, "查找到输入框了。。。");return node;}} else {for (int i = 0; i < node.getChildCount(); i++) {if (node.getChild(i) != null) {AccessibilityNodeInfo result = recycleFindEditText(node.getChild(i));if (result == null) {continue;} else {return result;}}}}return null;
}

遍历查找到所有的输入框

/**根据固定的类名循环查找到输入框,查找目标界面的所有输入框,适用于界面有好多输入框的情况* @param editTexts 传入一个size为0的list。* @param node*/
public void recycleFindEditTexts(List<AccessibilityNodeInfo> editTexts, AccessibilityNodeInfo node) {//edittext下面必定没有子元素,所以放在此时判断if (node.getChildCount() == 0) {if (editTextClassName.equals(node.getClassName())) {Log.d(TAG, "查找到一个输入框。。。");editTexts.add(node);}} else {for (int i = 0; i < node.getChildCount(); i++) {recycleFindEditTexts(editTexts,node.getChild(i));}}
}

遍历查找到listview

private static final String listViewClassName = "android.widget.ListView";
/**根据固定的类名循环查找到listView,查找到目标listView后即退出循环* @param node* @return*/
public AccessibilityNodeInfo recycleFindListView(AccessibilityNodeInfo node) {if (node.getChildCount() == 0) {return null;} else {
   //listview下面必定有子元素,所以放在此时判断for (int i = 0; i < node.getChildCount(); i++) {if (listViewClassName.equals(node.getClassName())) {Log.d(TAG, "查找到listview了。。。");return node;} else if (node.getChild(i) != null) {AccessibilityNodeInfo result = recycleFindListView(node.getChild(i));if (result == null) {continue;} else {return result;}}}}return null;
}

先写到这里,后续使用有新的感悟再来记录。

  相关解决方案