29、Service调试:ServiceManager、绑定服务、前台服务、ANR预防
Service 调试,说实话是 Android 系统调试里比较绕的一块。它不像 Activity 那样有明确的界面生命周期,Service 在后台默默运行,出了问题你很难第一时间感知到。
我个人习惯把 Service 调试分成四个维度来看:ServiceManager 的注册与查询、绑定服务的 Binder 调用链、前台服务的通知与优先级,以及最让人头疼的 ANR 预防。今天咱们就一个一个掰开聊。
核心观点:Service 调试的本质,是理解 Binder 通信的边界条件。你搞懂了 Binder 的线程模型和超时机制,Service 的坑基本就避开了八成。
一、ServiceManager:系统服务的“电话本”
ServiceManager 是什么?说白了就是系统里所有 Binder 服务的注册中心。每个系统服务启动时,都会把自己的 Binder 实体注册到 ServiceManager 里。客户端要调用某个服务,得先通过 ServiceManager 查到它的 Binder 引用。
调试 ServiceManager 时,我常用的命令是 service list 和 dumpsys。比如:
# 查看所有已注册的系统服务
adb shell service list
# 查看某个具体服务的详细信息
adb shell dumpsys activity services
# 查看 ServiceManager 本身的 binder 状态
adb shell dumpsys servicemanager
我在项目中遇到过一个问题:某个自定义系统服务注册后,客户端总是查不到。后来用 service list 一看,发现服务名被截断了——ServiceManager 对服务名长度有限制,超过 32 字节就会静默失败。嗯,这种边界问题,日志里根本不会报错。
调试技巧:如果你怀疑 ServiceManager 查询失败,可以用 adb shell service check <服务名> 来验证服务是否存在。返回 "Service not found" 就说明注册有问题。
二、绑定服务:Binder 的死锁与超时
绑定服务(Bound Service)是客户端通过 bindService() 建立的长连接。它的核心是 Binder 通信。调试绑定服务,我一般关注三个点:连接回调、Binder 线程池、跨进程调用超时。
先看一个典型的绑定服务代码:
// 客户端绑定
Intent intent = new Intent(this, MyService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
// 服务端 onBind
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
// Binder 实现
class MyBinder extends IMyService.Stub {
@Override
public void doSomething() {
// 这里运行在 Binder 线程池中
}
}
调试时,我经常用 dumpsys activity services 来查看绑定状态:
adb shell dumpsys activity services com.example.myapp
输出里会显示每个 Service 的 bindings 列表,包括连接的客户端进程、Binder 引用计数等。如果某个客户端没有正确解绑,你会看到 ConnectionLeaked 的警告。
避坑指南:我曾经在项目里遇到一个诡异的 ANR,主线程卡在 bindService() 上。查了半天发现是服务端的 onBind() 里做了耗时操作——你想想看,onBind() 默认运行在主线程,如果你在里面查询数据库或网络请求,主线程直接卡死。解决方案很简单:onBind() 里只返回 Binder 对象,别干别的。
另一个常见问题是 Binder 线程池耗尽。系统默认的 Binder 线程池大小是 16 个。如果所有线程都被某个慢调用占着,新的 Binder 请求就会排队。我习惯在调试时用这个命令查看 Binder 线程状态:
adb shell cat /proc/<pid>/task/*/comm | grep "Binder"
如果看到大量 Binder:<pid>_X 线程处于 D 状态(不可中断睡眠),那基本就是死锁了。
三、前台服务:通知与优先级的博弈
前台服务(Foreground Service)是 Android 为了限制后台行为推出的机制。它要求服务必须有一个可见的通知,并且优先级高于普通后台服务。
调试前台服务,我主要关注两点:通知是否及时显示、服务是否被系统杀死。
先看一个标准的前台服务启动流程:
@Override
public void onCreate() {
super.onCreate();
// 必须在 5 秒内调用 startForeground()
Notification notification = buildNotification();
startForeground(NOTIFICATION_ID, notification);
}
这里有个坑:startForeground() 必须在 onCreate() 或 onStartCommand() 里调用,而且要在 5 秒内完成。如果超时,系统会抛出 Context.startForegroundService() did not then call Service.startForeground() 异常。
我在项目中遇到过一个问题:App 在后台被系统杀死后,前台服务通知消失了,但服务进程还在。这是因为系统在回收资源时,会先杀掉前台服务的通知通道。解决方案是在 onTaskRemoved() 里重新启动前台服务:
@Override
public void onTaskRemoved(Intent rootIntent) {
// 重新启动前台服务
Intent restartIntent = new Intent(this, MyForegroundService.class);
startService(restartIntent);
super.onTaskRemoved(rootIntent);
}
调试技巧:用 adb shell dumpsys activity services | grep foreground 可以快速查看当前所有前台服务。如果某个前台服务没有通知,系统会标记为 FOREGROUND_SERVICE_WITHOUT_NOTIFICATION,这种服务很容易被杀死。
四、ANR 预防:Service 的超时红线
Service 的 ANR 和 Activity 不太一样。Activity 的 ANR 是 5 秒,但 Service 的 ANR 分两种情况:
| 场景 | 超时时间 | 触发条件 |
|---|---|---|
onCreate() / onStartCommand() |
20 秒 | 主线程被阻塞超过 20 秒 |
onBind() |
20 秒 | 主线程被阻塞超过 20 秒 |
前台服务 startForeground() |
5 秒 | 未在 5 秒内调用 startForeground() |
| Binder 调用 | 取决于调用方 | 客户端等待 Binder 响应超时 |
预防 ANR,我总结了三条铁律:
- 主线程只做 UI 和生命周期操作——Service 的
onCreate()、onStartCommand()、onBind()都运行在主线程,任何耗时操作必须扔到子线程。 - Binder 调用要设置超时——跨进程调用默认没有超时,如果服务端卡死,客户端会一直等。我习惯在客户端用
FutureTask或CountDownLatch加一个超时保护。 - 前台服务通知要提前准备——不要在
startForeground()之前才构建通知,应该在onCreate()里就准备好。
举个例子,一个安全的 Service 写法应该是这样的:
public class SafeService extends Service {
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void onCreate() {
super.onCreate();
// 提前构建通知
notification = buildNotification();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 耗时操作扔到子线程
executor.submit(() -> {
doHeavyWork();
stopSelf();
});
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// onBind 只返回 Binder,不做耗时操作
return new SafeBinder();
}
}
避坑指南:我曾经遇到一个 ANR,日志里显示 Service 的 onStartCommand() 执行了 30 秒。查代码发现开发者在里面调用了 Thread.sleep() 来模拟延迟——嗯,这种低级错误其实很常见。记住:Service 的生命周期方法里,任何阻塞操作都是红线。
五、知识体系总览
下面这张图总结了 Service 调试的核心逻辑。你可以把它当作一个检查清单:
这张图把 Service 调试的四个维度串起来了。你调试的时候,可以对照着检查:ServiceManager 查到了吗?Binder 线程池够用吗?前台通知显示了吗?主线程有没有阻塞?
说实话,Service 调试没有太多花哨的技巧。核心就是搞清楚 Binder 的通信模型,然后盯住主线程和超时这两条红线。做到这两点,大部分 Service 问题都能提前预防。