☰
Android无障碍服务保活指南:原理、避坑与断线自愈完整实现
2026/10/2 5:54:30 网站建设 项目流程

做安卓开发这么多年,“保活”两个字基本就是和系统斗智斗勇的代名词。尤其是项目里一旦用到了无障碍服务(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,拿到服务属性(事件类型、反馈类型、节点获取能力等)。
  • 构造Intent,Action为android.accessibilityservice.AccessibilityService,ComponentName指向你声明的Service。
  • 检查Service是否有android.permission.BIND_ACCESSIBILITY_SERVICE权限,没有直接拒绝绑定。
  • bind成功之后,回调onServiceConnected()。

如果你还想动态调整监听属性,可以在onServiceConnected()里调用setServiceInfo()。这个方法的优先级高于XML配置,但要注意它只允许在你已经获得用户授权之后使用。

这也是为什么市面上所有无障碍类App第一步都是引导用户到系统设置页打开开关。没有用户授权,代码写得再漂亮也白搭。

2.2 为什么后台进程会被清理

Android的进程管理核心是Low Memory Killer(LMK)。系统根据每个进程的adj值决定当内存不足时先杀谁。普通后台进程的adj值大概在10到15之间,属于“优先牺牲”的候选者。

如果你只是把AccessibilityService跑在后台,没有任何额外操作,你的进程就是一个普通的bindService宿主进程,在系统眼里和那些默默挂着不干活的第三方进程没有区别。内存一紧张,它可能就是第一个被收走的。

这里有个容易误解的地方:很多文章说“无障碍服务进程优先级高于普通后台进程”,这是不准确的。服务本身被系统绑定后,进程的adj值确实会有一定提升(因为系统正在使用它的接口),但提升幅度有限,远不如前台服务(adj=0)来得直接。

实测下来的情况是:设备空闲时,无障碍服务通常能稳定跑几天;一旦用户开始玩大型游戏、拍照、多开应用,内存压力上来,服务被断开就很常见了。

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 version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault|flagReportViewIds|flagRetrieveInteractiveWindows" android:canPerformGestures="true" android:canRetrieveWindowContent="true" android:notificationTimeout="100" android:description="@string/accessibility_service_description" />

这里重点说几个参数。canRetrieveWindowContent必须为true,否则你无法获取屏幕上的节点树,自动点击就无从谈起。accessibilityEventTypes不要配all,监听所有事件会导致功耗超标,也会增加系统断开连接的概率,按需配置就好。notificationTimeout指的是系统事件通知的批量发送间隔,单位毫秒,100左右比较常用,不要设得太小,否则回调太频繁。

然后是AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <application> <!-- 无障碍服务声明 --> <service android:name=".MyAccessibilityService" android:exported="true" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:label="@string/accessibility_service_label"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> <!-- 保活前台服务 --> <service android:name=".KeepAliveService" android:exported="false" android:foregroundServiceType="specialUse" /> </application> </manifest>

这里有一个避坑关键点:android:exported="true"。我在Android 9、10、11上分别踩到过同一种现象:设置页能看到服务,但打开开关后立刻弹回去,或者开关能打开但Service始终没回调。最后定位下来,有的是因为exported="false"导致系统在某些机型上bindService失败。虽然官方文档没强制要求exported的值,但社区实践一致认为,显式加上android.permission.BIND_ACCESSIBILITY_SERVICE权限保护之后,exported="true"是兼容性最好的写法。系统绑定服务靠显式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; } List<AccessibilityNodeInfo> 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); List<AccessibilityServiceInfo> 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 前台服务KeepAliveService

package 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。这里的类型我选了specialUse,Android 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:permission="android.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的“隐蔽杀服务”场景时非常管用。

如果你正在做无障碍相关项目,先别急着堆保活黑科技,把我上面说的前台服务加断线自愈这套基础铺好,绝大多数情况下已经够用了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询