
1. 为什么选择无障碍服务做自动化测试第一次听说Android无障碍服务AccessibilityService能用来做自动化测试时我正盯着手机屏幕疯狂点击抢单按钮。当时就在想要是能有个机器人帮我完成这些重复操作该多好。试过Monkey脚本发现太随机研究UIAutomator又需要Root权限直到偶然发现小米手机工厂的视频——上千台手机通过系统级服务自动操作这才找到了完美解决方案。无障碍服务本质上是个超级监听器。普通App只能知道自己的界面变化而这个服务能监听全系统的操作事件。比如你打开微信对话框、点击输入法键盘、甚至收到短信通知它都能第一时间感知到。更厉害的是它还能反向操作——模拟点击、滑动、输入文字完全不需要Root权限。实测对比三种方案MonkeyRunner随机事件不可控适合压力测试UIAutomator需要adb调试权限无法商用AccessibilityService免Root支持商业应用但需要用户手动开启权限去年给某出行平台做抢单测试时我们用这套方案实现了7×24小时不间断监控订单池200ms内完成从识别到点击的全流程在20款不同机型上稳定运行2. 五分钟快速搭建基础框架先来看最简实现流程。新建一个继承AccessibilityService的类核心只需要重写两个方法public class OrderService extends AccessibilityService { Override public void onAccessibilityEvent(AccessibilityEvent event) { // 在这里处理所有监听事件 if(event.getEventType() TYPE_VIEW_CLICKED){ AccessibilityNodeInfo node event.getSource(); if(node ! null 立即抢单.equals(node.getText())){ node.performAction(ACTION_CLICK); } } } Override public void onInterrupt() { // 服务被中断时回调 } }接着在AndroidManifest.xml声明服务service android:name.OrderService android:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICE intent-filter action android:nameandroid.accessibilityservice.AccessibilityService/ /intent-filter meta-data android:nameandroid.accessibilityservice android:resourcexml/service_config/ /service关键在res/xml/service_config.xml的配置accessibility-service xmlns:androidhttp://schemas.android.com/apk/res/android android:descriptionstring/accessibility_desc android:accessibilityEventTypestypeAllMask android:accessibilityFlagsflagDefault android:canRetrieveWindowContenttrue android:canPerformGesturestrue android:packageNamescom.example.targetapp/最后别忘了让用户开启权限。用这段代码直接跳转到设置页startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));3. 控件精准定位的三大秘诀实际项目中最大的坑是控件定位。某次测试发现抢单成功率只有30%排查发现是因为不同机型下单按钮的ID居然不一样。分享几个实战技巧方法一ID定位首选ListAccessibilityNodeInfo nodes root.findAccessibilityNodeInfosByViewId(com.target.app:id/order_btn);用Android SDK自带的uiautomatorviewer工具分析布局$ cd ~/Android/Sdk/tools/bin $ ./uiautomatorviewer方法二文本匹配// 完全匹配 nodes root.findAccessibilityNodeInfosByText(立即抢单); // 模糊匹配 nodes root.findAccessibilityNodeInfosByText(.*抢单.*);方法三坐标点击终极方案当控件无法通过常规方式获取时可以计算屏幕坐标Rect bounds new Rect(); node.getBoundsInScreen(bounds); GestureDescription.Builder builder new GestureDescription.Builder(); Path path new Path(); path.moveTo(bounds.centerX(), bounds.centerY()); builder.addStroke(new GestureDescription.StrokeDescription( path, 0, 50)); dispatchGesture(builder.build(), null, null);特别提醒Android 11开始对悬浮窗权限管控更严格建议优先使用前两种方案。4. 提升稳定性的五个关键策略在连续运行测试脚本时最怕遇到突然卡死。通过分析上万次测试日志总结出这些经验策略一事件去重private long lastEventTime; Override public void onAccessibilityEvent(AccessibilityEvent event) { if(System.currentTimeMillis() - lastEventTime 100){ return; // 100ms内相同事件忽略 } lastEventTime System.currentTimeMillis(); // 处理逻辑... }策略二异常恢复private void recoverWhenCrash(){ performGlobalAction(GLOBAL_ACTION_HOME); sleep(1000); Intent launch getPackageManager().getLaunchIntentForPackage(targetPkg); startActivity(launch); }策略三多条件校验boolean isValidOrder(NodeInfo node){ return node.isClickable() node.getText().toString().contains(抢单) node.getParent().getChildCount() 3; }策略四动态超时控制int retry 0; while(retry 5){ if(findTargetNode() ! null) break; sleep(300 * retry); // 递增等待 }策略五性能监控Debug.startMethodTracing(order_trace); // 关键代码段 Debug.stopMethodTracing();记得在测试结束后用Android Studio的Profiler分析trace文件重点排查主线程阻塞问题。5. 复杂业务场景实战解析以出行平台抢单为例完整流程需要处理订单过滤boolean isGoodOrder(NodeInfo node){ String text node.getText().toString(); return !text.contains(拼车) Double.parseDouble(text.split(元)[0]) 50; }多步骤操作void handleComplexOrder(){ clickByText(接受订单); sleep(500); clickById(com.target:id/confirm); sleep(1000); clickById(com.target:id/navigation); }异常处理try{ performAction(targetNode); } catch(Exception e){ takeScreenshot(error_System.currentTimeMillis()); recoverWhenCrash(); }数据记录void logOrderInfo(NodeInfo node){ HashMapString,String data new HashMap(); data.put(price, extractPrice(node)); data.put(time, new SimpleDateFormat(HH:mm:ss).format(new Date())); Log.d(OrderData, new JSONObject(data).toString()); }特别提醒商业应用记得添加免责声明并在代码中设置速率限制避免被判定为恶意程序。6. 高级技巧与避坑指南手势模拟黑科技// 滑动屏幕 Path swipePath new Path(); swipePath.moveTo(500, 1000); swipePath.lineTo(500, 500); dispatchGesture(new GestureDescription.Builder() .addStroke(new StrokeDescription(swipePath, 0, 500)) .build(), null, null); // 多指操作Android 10 GestureDescription.Builder builder new GestureDescription.Builder(); builder.addStroke(new StrokeDescription(path1, 0, 1000)); builder.addStroke(new StrokeDescription(path2, 0, 1000));常见坑点华为EMUI会限制后台服务MIUI需要额外开启悬浮窗权限Android 10以上需要动态申请MANAGE_OVERLAY_PERMISSION性能优化建议使用缓存减少重复查找private WeakHashMapString, NodeInfo nodeCache new WeakHashMap();批量操作减少绘制AccessibilityNodeInfo root getRootInActiveWindow(); // 一次性获取所有目标节点 ListNodeInfo targets root.findAccessibilityNodeInfosByText(抢单);这套方案经过多个百万级DAU App验证最高在Redmi Note 11上实现过单日执行2000次复杂操作无卡顿。关键是要根据具体业务场景调整事件监听策略比如抢单类应用要重点监控TYPE_WINDOW_CONTENT_CHANGED事件而自动化填表则需要关注TYPE_VIEW_TEXT_CHANGED。