ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android无障碍服务保活指南:原理、避坑与断线自愈完整实现

Android无障碍服务保活指南:原理、避坑与断线自愈完整实现 做安卓开发这么多年“保活”两个字基本就是和系统斗智斗勇的代名词。尤其是项目里一旦用到了无障碍服务AccessibilityService自动化点击、应用辅助操作、自动填报这类功能全都挂在这条链路上——服务一旦掉线后面的任何逻辑都白搭。我见过太多开发者在“保活”这件事上走弯路有的拼命上双进程守护有的做定时器反复拉活结果Android 9之后系统限制越来越严这些老套路不但没用反而容易导致应用被厂商标记成“流氓软件”。这篇文章我就围绕AccessibilityService的后台常驻问题从原理到完整代码把配置、权限、断线自愈这些关键点一次讲透。适合正处于“服务总掉线”困扰中的App开发者也适合想把自动化脚本稳定跑起来、但不想把时间耗在系统机制上的朋友。1. 项目概述无障碍服务保活到底在保什么1.1 哪些项目真正需要无障碍服务先说清楚一个前提你为什么要用AccessibilityService一般来说正经用途集中在三个场景第一个是辅助工具类。比如自动抢票、屏幕点击器、抢单助手这类工具本质上是帮用户代替手指完成重复操作必须监听界面状态并模拟点击。第二个是自动化测试。UI自动化框架比如Appium的底层在部分场景下会借助无障碍服务来获取节点、执行操作比用坐标硬点更可靠因为节点级操作不依赖屏幕分辨率。第三个是适老化、无障碍关怀类功能。比如自动朗读屏幕内容、自动放大按钮、语音代替触摸这些是系统级辅助功能的正统使用场景。不管哪一种你对服务的稳定性都有硬性要求。这里有个现实Android系统对后台进程的调度越来越严格无障碍服务本身并不能免疫进程被回收。所以“保活”不是“让服务永远不死”而是“让服务被干掉之后能快速恢复”以及“尽量降低被干掉的概率”。目标不同技术方案就完全不同。1.2 “保活”的边界到底在哪很多新人有个误解只要我把AccessibilityService写出来并在设置里打开它就应该像守护进程一样永远在后台跑。实际情况不是这样。无障碍服务本质上是系统通过bindService连接的一个普通Service进程仍然受系统内存、功耗、后台限制三条规则约束。当内存吃紧时系统会按照进程优先级oom_adj从低到高开始杀进程。普通的后台进程是最早被清理的约等于“候补牺牲品”。而Toast、前台Service、正在被用户操作的应用优先级才高。所以你能做的“保活”合法且有效的只有以下几件事把服务进程尽量往前台优先级拉前台服务方式。减少服务被系统判定为“异常”的机会比如不ANR、不频繁唤起。提供断线自愈机制让用户能一键恢复服务。引导用户关掉厂商的省电策略不同厂商设置路径不同。我在实际项目里的结论是可靠的方案是把“减少被杀”和“快速恢复”组合起来而不是妄图靠一个技巧永不掉线。明白这个边界后续所有的代码和配置才有意义。还有一个很容易忽略的点不要在无障碍服务里做重活。很多开发者把服务当成一个普通后台Service在里面跑定时任务、做网络请求甚至做数据库批量操作。系统一旦发现这个服务响应变慢会直接断开连接并标记为异常这种“被砍”是代码层面造成的和保活方案没关系。2. 原理拆解AccessibilityService的运行机制2.1 服务是怎么被系统“拉起”的无障碍服务在清单文件里声明之后系统并不会立刻绑定它必须等用户在“设置 - 无障碍 - 已安装的服务”里手动开启。这一步非常关键因为只有用户明确授权系统才会执行bindService。开启后系统里的AccessibilityManagerService会通过如下流程连接你的服务读取你在meta-data中配置的xml拿到服务属性事件类型、反馈类型、节点获取能力等。构造IntentAction为android.accessibilityservice.AccessibilityServiceComponentName指向你声明的Service。检查Service是否有android.permission.BIND_ACCESSIBILITY_SERVICE权限没有直接拒绝绑定。bind成功之后回调onServiceConnected()。如果你还想动态调整监听属性可以在onServiceConnected()里调用setServiceInfo()。这个方法的优先级高于XML配置但要注意它只允许在你已经获得用户授权之后使用。这也是为什么市面上所有无障碍类App第一步都是引导用户到系统设置页打开开关。没有用户授权代码写得再漂亮也白搭。2.2 为什么后台进程会被清理Android的进程管理核心是Low Memory KillerLMK。系统根据每个进程的adj值决定当内存不足时先杀谁。普通后台进程的adj值大概在10到15之间属于“优先牺牲”的候选者。如果你只是把AccessibilityService跑在后台没有任何额外操作你的进程就是一个普通的bindService宿主进程在系统眼里和那些默默挂着不干活的第三方进程没有区别。内存一紧张它可能就是第一个被收走的。这里有个容易误解的地方很多文章说“无障碍服务进程优先级高于普通后台进程”这是不准确的。服务本身被系统绑定后进程的adj值确实会有一定提升因为系统正在使用它的接口但提升幅度有限远不如前台服务adj0来得直接。实测下来的情况是设备空闲时无障碍服务通常能稳定跑几天一旦用户开始玩大型游戏、拍照、多开应用内存压力上来服务被断开就很常见了。2.3 系统对“断开”的处理与恢复逻辑AccessibilityService如果因为进程被杀而断开系统并不会马上尝试重新绑定而是设置一个退避策略短时间内的重试间隔会逐步拉长如果多次绑定失败系统会彻底停掉该服务直到用户再次进入设置手动打开。这就是为什么很多用户反映“开着开着服务就没了设置里显示已关闭”。这一行为是系统层面的策略开发者无法直接控制。我们能做的只有在服务断开时立刻感知通过onUnbind或onDestroy回调。弹通知、弹引导页让用户一键回到无障碍设置页重新打开。简化用户的操作路径减少“算了不开了”的流失。理解这一点之后你就知道“保活”真正要做的是一个闭环拉高进程优先级 感知断开 引导恢复。下面我逐一拆解方案。3. 保活方案选型哪些值得做哪些是坑3.1 前台服务 START_STICKY是基础底盘把AccessibilityService所在进程挂到前台服务上是目前最稳妥、最合规的提升优先级手段。前台服务会触发一个常驻通知系统会把你进程的adj值直接拉高降低被杀概率。代码上你只需要在应用启动后调用startForegroundService()启动一个普通的Service然后在这个Service里尽可能早地调用startForeground()。同时注意Android 8.0之后必须通知渠道Android 12之后要处理通知权限Android 14之后前台服务对targetSdk 34的应用要求声明foregroundServiceType。这个前台服务的onStartCommand建议返回START_STICKY这样即使系统因为某些原因杀掉了它系统进程空闲后还会尝试重建服务算是一个弱保活手段。缺点是Android 12之后从后台启动前台服务有限制所以最好在用户点开App时启动不要尝试从后台悄悄拉起来。另外有个细节前台服务通知的文案别太随意应用商店审核时会看。老老实实写“提供辅助功能服务”比写“正在运行”这种可疑的话要安全得多。3.2 断线检测与自动引导是保活的兜底这一块很多人忽略但我在实践里认为它才是保活的真正护城河。因为不管你优先级拉得多高系统内存极限状态下照样杀厂商后台管理照样清理。真正决定用户体验的是服务掉了之后用户能不能以最快速度恢复。具体做法分两步在App主界面轮询或者注册AccessibilityManager.AccessibilityStateChangeListener监听无障碍服务开关状态。一旦发现服务没开弹出一个醒目的引导卡片点击直接跳转到系统“无障碍设置”或甚至直接定位到你的服务详情页。这里有个体验优化的技巧不要用通用的无障碍设置页Settings.ACTION_ACCESSIBILITY_SETTINGS而是跳转到Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS这个Action可以拼接URI指定包名和类名直接打开当前服务的设置详情页用户只需要点一下开关就行。实测这个入口比通用页路径短一半恢复率明显更高。3.3 电池白名单和自启动权限是厂商必修课国产ROM比如MIUI、EMUI、ColorOS、OriginOS基本都有自己的后台管理策略开发者的前台服务在他们眼里也不是免死金牌。我踩过不少坑之后总结出三件套引导引导用户把App加入“电池优化白名单”。这个有标准API可以请求通过Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS直接弹系统对话框是一步到位的操作。引导用户打开“自启动”权限。这个没有标准API只能跳对应厂商的授权管理页或者干脆让用户去“安全中心 - 应用权限”里手动开。关闭“锁屏清理”或“睡眠清理”。不同ROM入口不一样H5引导页配上截图最省心。注意不要一上来就让用户开一堆权限会被当成流氓软件。优先做电池白名单这个对进程保活效果的提升最明显。3.4 双进程守护、定时拉活不推荐网上还要很多老方案比如双进程互相守护、AlarmManager定时拉起、JobScheduler周期唤醒。它们在Android 8之前确实有效但现在的系统对后台启动限制极其严格这些方案要么失效要么把App的耗电做得很难看还容易引来应用商店的审核问题。我的建议很明确放弃这些对抗式方案。把精力放在断线快速恢复和用户引导上长远来看省心得多。4. 完整代码实现附注释4.1 工程结构和准备工作下面给出一套可以直接套用的代码结构如下app/ ├── src/main/ │ ├── java/com/example/keepalive/ │ │ ├── MyAccessibilityService.java │ │ ├── KeepAliveService.java │ │ ├── AccessibilityHelper.java │ │ └── MainActivity.java │ ├── res/ │ │ ├── layout/activity_main.xml │ │ └── xml/accessibility_service_config.xml │ └── AndroidManifest.xml在动手写代码前先在build.gradle里把compileSdk设为34或更高minSdk建议21。应用targetSdk如果是34及以上前台服务需要声明类型我用specialUse来兼容保活场景。4.2 无障碍服务的XML配置与Manifest注册先创建res/xml/accessibility_service_config.xml?xml version1.0 encodingutf-8? accessibility-service xmlns:androidhttp://schemas.android.com/apk/res/android android:accessibilityEventTypestypeWindowStateChanged|typeWindowContentChanged android:accessibilityFeedbackTypefeedbackGeneric android:accessibilityFlagsflagDefault|flagReportViewIds|flagRetrieveInteractiveWindows android:canPerformGesturestrue android:canRetrieveWindowContenttrue android:notificationTimeout100 android:descriptionstring/accessibility_service_description /这里重点说几个参数。canRetrieveWindowContent必须为true否则你无法获取屏幕上的节点树自动点击就无从谈起。accessibilityEventTypes不要配all监听所有事件会导致功耗超标也会增加系统断开连接的概率按需配置就好。notificationTimeout指的是系统事件通知的批量发送间隔单位毫秒100左右比较常用不要设得太小否则回调太频繁。然后是AndroidManifest.xmlmanifest xmlns:androidhttp://schemas.android.com/apk/res/android uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_SPECIAL_USE / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / uses-permission android:nameandroid.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS / application !-- 无障碍服务声明 -- service android:name.MyAccessibilityService android:exportedtrue android:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICE android:labelstring/accessibility_service_label intent-filter action android:nameandroid.accessibilityservice.AccessibilityService / /intent-filter meta-data android:nameandroid.accessibilityservice android:resourcexml/accessibility_service_config / /service !-- 保活前台服务 -- service android:name.KeepAliveService android:exportedfalse android:foregroundServiceTypespecialUse / /application /manifest这里有一个避坑关键点android:exportedtrue。我在Android 9、10、11上分别踩到过同一种现象设置页能看到服务但打开开关后立刻弹回去或者开关能打开但Service始终没回调。最后定位下来有的是因为exportedfalse导致系统在某些机型上bindService失败。虽然官方文档没强制要求exported的值但社区实践一致认为显式加上android.permission.BIND_ACCESSIBILITY_SERVICE权限保护之后exportedtrue是兼容性最好的写法。系统绑定服务靠显式Intent并不存在暴露风险。4.3 MyAccessibilityService.java核心实现package com.example.keepalive; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Intent; import android.os.Build; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; public class MyAccessibilityService extends AccessibilityService { public static final String ACTION_CONNECTED com.example.keepalive.ACTION_ACCESSIBILITY_CONNECTED; public static final String ACTION_DISCONNECTED com.example.keepalive.ACTION_ACCESSIBILITY_DISCONNECTED; private long lastClickTime 0L; Override protected void onServiceConnected() { super.onServiceConnected(); // 动态覆盖部分XML配置适合在代码里按需调整 AccessibilityServiceInfo info getServiceInfo(); if (info ! null) { info.eventTypes AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType AccessibilityServiceInfo.FEEDBACK_GENERIC; info.flags AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS | AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; info.notificationTimeout 100; setServiceInfo(info); } // 广播告诉UI层服务已就绪 sendBroadcast(new Intent(ACTION_CONNECTED)); } Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { // 窗口切换时可以在这里处理自动点击、状态识别等逻辑 // 注意不要在这里做耗时操作建议用Handler抛到子线程 } } Override public void onInterrupt() { // 系统要求中断服务时回调一般不需要处理 } Override public boolean onUnbind(Intent intent) { // 系统与服务断开时回调这里发广播让UI能及时看到状态 sendBroadcast(new Intent(ACTION_DISCONNECTED)); return super.onUnbind(intent); } Override public void onDestroy() { sendBroadcast(new Intent(ACTION_DISCONNECTED)); super.onDestroy(); } /** * 根据文本查找可点击节点并执行点击带防抖。 */ private boolean clickByText(String text) { AccessibilityNodeInfo root getRootInActiveWindow(); if (root null) { return false; } long now System.currentTimeMillis(); if (now - lastClickTime 1000) { return false; } ListAccessibilityNodeInfo nodes root.findAccessibilityNodeInfosByText(text); for (AccessibilityNodeInfo node : nodes) { if (node.isClickable()) { lastClickTime now; node.performAction(AccessibilityNodeInfo.ACTION_CLICK); return true; } else { AccessibilityNodeInfo parent node.getParent(); while (parent ! null) { if (parent.isClickable()) { lastClickTime now; parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); return true; } parent parent.getParent(); } } } return false; } }这里有几个实操经验值得展开。第一设置事件监听不要照搬TYPES_ALL_MASK。我上面是为了演示动态配置才这么写的实际项目里你要根据需求筛选。监听事件越少系统回调越少服务被判定为“消耗资源过多”的概率越低。建议只监听TYPE_WINDOW_STATE_CHANGED和TYPE_WINDOW_CONTENT_CHANGED。第二findAccessibilityNodeInfosByText是按文本模糊匹配的不是精确匹配。如果界面上有多个相似文本结果列表里会有多个节点需要根据业务加条件过滤。另外如果开启FLAG_INCLUDE_NOT_IMPORTANT_VIEWS很多原本不暴露的节点也会进入树里匹配效率会下降慎用。第三防抖非常重要。无障碍回调触发频率极高如果不加时间间隔判断一个页面切换可能触发十几次点击用户会直接懵掉。我上面用的时间戳防抖只能算最基础的方案生产环境建议把“是否已经执行过当前任务”的状态机做进去。4.4 状态检测与无障碍设置页跳转写一个工具类AccessibilityHelper用于检查服务状态和跳转设置页package com.example.keepalive; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.Settings; import android.view.accessibility.AccessibilityManager; import java.util.List; public class AccessibilityHelper { /** 判断指定无障碍服务是否已经开启 */ public static boolean isServiceEnabled(Context context) { AccessibilityManager am (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); ListAccessibilityServiceInfo enabledServices am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); for (AccessibilityServiceInfo info : enabledServices) { ComponentName component info.getComponentName(); if (component ! null component.getClassName().equals(MyAccessibilityService.class.getName())) { return true; } } return false; } /** 跳转到当前应用的无障碍服务详情页 */ public static void goToAccessibilitySettings(Context context) { try { Intent intent new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS); intent.setData(Uri.parse(package: context.getPackageName())); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } catch (Exception e) { // 部分老旧机型不支持详情页退回通用设置页 Intent fallback new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); fallback.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(fallback); } } }为什么优先跳详情页而不是总列表页因为用户在总列表里还要再从一堆App里找自己的应用路径长且容易找不到。详情页直接定位到你的服务页面上只有一个大开关恢复操作一步到位。我接入这个跳转后用户服务恢复率大概提升了三成。4.5 前台服务KeepAliveServicepackage com.example.keepalive; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.content.pm.ServiceInfo; import android.os.Build; import android.os.IBinder; import androidx.core.app.NotificationCompat; public class KeepAliveService extends Service { private static final String CHANNEL_ID keep_alive_channel; private static final int NOTIFICATION_ID 1001; Override public void onCreate() { super.onCreate(); createNotificationChannel(); } Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification buildNotification(); if (Build.VERSION.SDK_INT Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); } else { startForeground(NOTIFICATION_ID, notification); } // START_STICKY进程被系统杀掉后系统会尝试重建 return START_STICKY; } private Notification buildNotification() { Intent clickIntent new Intent(this, MainActivity.class); PendingIntent pendingIntent PendingIntent.getActivity( this, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); return new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(辅助服务运行中) .setContentText(保持辅助功能稳定可用) .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingIntent) .setOngoing(true) .setPriority(NotificationCompat.PRIORITY_LOW) .build(); } private void createNotificationChannel() { NotificationChannel channel new NotificationChannel( CHANNEL_ID, 保活通知, NotificationManager.IMPORTANCE_LOW); NotificationManager manager getSystemService(NotificationManager.class); if (manager ! null) { manager.createNotificationChannel(channel); } } Override public IBinder onBind(Intent intent) { return null; } }代码里我特意加了Android 14的兼容分支。如果你的targetSdk是34以上前台服务必须声明类型否则运行时会直接抛ForegroundServiceStartNotAllowedException。这里的类型我选了specialUseAndroid 14允许开发者用这个类型覆盖无法归入通用类型的场景。不过要注意个别应用商店可能对specialUse类型的审核字段有要求说明文案写清楚用途就行。启动前台服务时在MainActivity里调用if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(new Intent(this, KeepAliveService.class)); } else { startService(new Intent(this, KeepAliveService.class)); }最小化场景下前台服务通知是用户退出App后仍然存在的那条“长驻通知”。有些开发者嫌它碍眼想偷偷取消我的建议是不要这么做。收起通知会触发系统限制App会被标记为后台行为异常反而影响保活效果。4.6 MainActivity引导页MainActivity承担两件事检查状态、展示引导按钮。package com.example.keepalive; import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; public class MainActivity extends AppCompatActivity { private TextView tvStatus; private Button btnEnable; private Button btnIgnoreBattery; private final Handler handler new Handler(Looper.getMainLooper()); private boolean serviceConnected false; private final BroadcastReceiver receiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (MyAccessibilityService.ACTION_CONNECTED.equals(action)) { serviceConnected true; updateStatus(); } else if (MyAccessibilityService.ACTION_DISCONNECTED.equals(action)) { serviceConnected false; updateStatus(); } } }; private final Runnable checkRunnable new Runnable() { Override public void run() { serviceConnected AccessibilityHelper.isServiceEnabled(MainActivity.this); updateStatus(); handler.postDelayed(this, 2000); } }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvStatus findViewById(R.id.tv_status); btnEnable findViewById(R.id.btn_enable); btnIgnoreBattery findViewById(R.id.btn_battery); // 动态申请通知权限Android 13 if (Build.VERSION.SDK_INT 33) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100); } } // 启动前台服务拉高进程优先级 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(new Intent(this, KeepAliveService.class)); } else { startService(new Intent(this, KeepAliveService.class)); } btnEnable.setOnClickListener(v - AccessibilityHelper.goToAccessibilitySettings(this)); btnIgnoreBattery.setOnClickListener(v - requestIgnoreBatteryOptimizations()); } private void requestIgnoreBatteryOptimizations() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { Intent intent new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse(package: getPackageName())); try { startActivity(intent); } catch (Exception e) { // 部分系统不支持直接弹窗引导用户进入设置页手动操作 Intent settings new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); startActivity(settings); } } } private void updateStatus() { if (serviceConnected) { tvStatus.setText(服务状态已开启); btnEnable.setText(重新校准服务); } else { tvStatus.setText(服务状态未开启); btnEnable.setText(去开启无障碍服务); } } Override protected void onStart() { super.onStart(); IntentFilter filter new IntentFilter(); filter.addAction(MyAccessibilityService.ACTION_CONNECTED); filter.addAction(MyAccessibilityService.ACTION_DISCONNECTED); registerReceiver(receiver, filter); handler.post(checkRunnable); } Override protected void onStop() { super.onStop(); unregisterReceiver(receiver); handler.removeCallbacksAndMessages(null); } }MainActivity的轮询和广播双通道设计是实际项目里比较稳妥的做法。广播能第一时间收到断线事件轮询则是一种兜底防止因为进程被系统冻结导致广播延迟到达。我建议你保留这个双通道因为厂商ROM对广播的限制五花八门有时候系统把广播延后了界面状态就会不准。布局文件不复杂就放一个状态文案和两个按钮这里不展开写了。关键就一句话所有按钮的点击目标都必须明确别让用户自己去设置里翻。5. 常见问题与排查技巧实录5.1 手机设置里找不到“已安装的服务”这个问题出现最多尤其是targetSdk升到31之后。排查方向按概率排序清单里服务没有声明android:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICE。没有这个权限系统直接忽略你的服务。exported设置了false。部分系统绑定不到服务列表里就不出现。改成true再试。intent-filter漏了android.accessibilityservice.AccessibilityService这个action。meta-data配置路径写错解析xml失败服务不会出现在列表里。手机系统版本太老或ROM修改过需要开发者选项里找到“无障碍”再进一次。5.2 服务开关能打开但事件不触发如果你打开开关后一切正常但onAccessibilityEvent就是没反应先检查事件类型配置。比如你只监听了TYPE_WINDOW_STATE_CHANGED但界面上做的是一个列表项局部刷新那么触发的是TYPE_WINDOW_CONTENT_CHANGED自然收不到。先把事件类型扩大到TYPES_ALL_MASK跑一遍确认能收到事件再按需缩小范围。另一个原因是canRetrieveWindowContent配了false。没有节点抓取能力事件照样回调但getRootInActiveWindow()返回null点击逻辑全部失效。检查方法在onAccessibilityEvent里加个日志打印root是否为null。5.3 跑了一会儿服务还是被系统断开先区分是被杀还是被系统主动断开。看日志如果是Unable to start service之类说明是启动被限如果是binder died相关说明进程被回收。常规优化手段有四板斧确认前台服务是否真的在跑adb shell dumpsys activity services | grep 你的包名看一眼。引导用户把App加入电池优化白名单。引导用户关掉厂商的后台清理特别是小米和vivo。确认没有在onAccessibilityEvent里做耗时操作。比如有的开发者直接在回调里做网络请求系统回调线程卡顿超过阈值服务就会被中断。5.4 点击模拟失败root节点拿不到getRootInActiveWindow()拿不到节点常见原因是当前窗口不是应用窗口或者是在系统设置页里。比如无障碍服务弹出的系统对话框、输入法窗口普通应用拿不到根节点。这时候需要FLAG_RETRIEVE_INTERACTIVE_WINDOWS标志并且要针对系统界面做特殊处理。如果项目主要跑在自家App上直接用getWindows()按窗口类型过滤更可靠。还有一点Android 12之后部分系统窗口对第三方App的节点访问更严格拿不到就换思路通过坐标模拟点击兜底。5.5 厂商ROM差异怎么处理这份代码在原生Android和AOSP系ROM上表现都不错但国产ROM的一致性要差很多。我建了一个表格快速对照问题现象可能原因处理建议小米机型锁屏后服务失效MIUI的锁屏清理策略引导加入“锁定”任务允许后台弹出界面华为机型重启后服务开关自动关闭开机自管理拦截引导开启“自启动”权限vivo/iQOO长时间后台后断开省电策略激进引导加入“后台高功耗”白名单三星/索尼等海外机型掉线系统限制后台活动前台服务基本能覆盖优先检查通知权限5.6 权限申请的一步到位技巧最后分享一个我一直在用的流程。用户点击“开启服务”时不直接跳设置页而是先弹一个半透明引导页上面解释“为什么要开无障碍服务”放一张操作截图再放一个“立即开启”按钮。这样做的原因是很多用户到了系统设置页看到一堆专业开关会犹豫引导页把预期说清楚开启率能高不少。整个流程里用户只需要一次跳转、一次开关路径越短恢复率越高。这是我在项目里反复优化出来的结论。6. 最后说点实话做了这么多保活方案我的体会是无障碍服务的“保活”更像是一场“慢性管理”而不是一次性的技术攻坚。你没法让系统永远不杀你的进程但你可以做到被杀了之后让用户用最快的速度把服务拉回来。后台常驻的稳定性一半靠代码一半靠用户配合。最后再分享一个小技巧调试无障碍服务时别只盯着Logcat。用adb shell dumpsys accessibility | grep -i excite这类命令直接看系统视角里服务的连接状态能帮你快速判断到底是代码问题还是系统调度问题。这个命令在排查厂商ROM的“隐蔽杀服务”场景时非常管用。如果你正在做无障碍相关项目先别急着堆保活黑科技把我上面说的前台服务加断线自愈这套基础铺好绝大多数情况下已经够用了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进