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 listdumpsys。比如:

# 查看所有已注册的系统服务
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,我总结了三条铁律:

  1. 主线程只做 UI 和生命周期操作——Service 的 onCreate()onStartCommand()onBind() 都运行在主线程,任何耗时操作必须扔到子线程。
  2. Binder 调用要设置超时——跨进程调用默认没有超时,如果服务端卡死,客户端会一直等。我习惯在客户端用 FutureTaskCountDownLatch 加一个超时保护。
  3. 前台服务通知要提前准备——不要在 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 注册与查询 service list dumpsys servicemanager 服务名长度限制 绑定服务 Binder 通信 连接回调 线程池耗尽 跨进程超时 前台服务 通知与优先级 5秒超时限制 onTaskRemoved 通知丢失 ANR 预防 20秒超时红线 主线程阻塞 Binder 超时保护 子线程执行 Service 调试核心 调试原则 理解 Binder 边界 → 监控线程状态 → 设置超时保护 → 避免主线程阻塞

这张图把 Service 调试的四个维度串起来了。你调试的时候,可以对照着检查:ServiceManager 查到了吗?Binder 线程池够用吗?前台通知显示了吗?主线程有没有阻塞?

说实话,Service 调试没有太多花哨的技巧。核心就是搞清楚 Binder 的通信模型,然后盯住主线程和超时这两条红线。做到这两点,大部分 Service 问题都能提前预防。


专注资料整理