29、Android系统服务架构:ServiceManager、系统服务注册与获取、Binder线程池
各位同学,今天我们来聊聊Android系统里最核心的“大管家”——ServiceManager。说白了,它就是系统服务的“黄页”。你想想看,一个App想用摄像头、想发通知、想访问网络,这些功能谁提供的?都是系统服务。那问题来了,App怎么找到这些服务?谁在管理这些服务?嗯,这就是ServiceManager的活儿。
29.1 ServiceManager:系统服务的“电话总机”
我个人习惯把ServiceManager比作一个电话总机。每个系统服务在启动时,都会给总机打个电话:“喂,我是‘ActivityManagerService’,我的分机号是XXX,以后有人找我,你帮我转接。” 然后App想用某个服务时,也先打给总机:“你好,请帮我转接‘ActivityManagerService’。” 总机查一下号码本,就把电话接过去了。
在Android里,这个“电话总机”是一个叫servicemanager的进程。它其实是一个特殊的Binder服务,但它的特殊之处在于——它是所有Binder服务的“服务”。它的句柄永远是0。为什么是0?因为Binder驱动在初始化时,第一个注册的就是它。这个设计很巧妙,相当于系统里有一个“默认知道”的入口。
核心要点:ServiceManager本身也是一个Binder服务,它的引用句柄固定为0。所有其他系统服务在启动时,必须向它注册;所有客户端在获取服务时,必须先向它查询。
29.2 系统服务注册流程
我记得在项目中调试一个系统服务时,发现服务死活注册不上。后来排查了半天,原来是服务名称写错了。嗯,这里要注意,注册流程其实很清晰,但细节容易踩坑。
服务注册的核心调用链是这样的:
// 服务端:注册服务
// 以MediaPlayerService为例
defaultServiceManager()->addService(
String16("media.player"), // 服务名称
new MediaPlayerService(), // 服务实例
false // 是否允许isolated进程调用
);
这段代码背后发生了什么?我带你走一遍:
- 获取ServiceManager代理:
defaultServiceManager()内部通过Binder驱动,拿到句柄为0的BpBinder,封装成BpServiceManager。 - 发起Binder调用:调用
addService()时,BpServiceManager把服务名称、服务引用打包成Parcel,通过Binder驱动发送给servicemanager进程。 - ServiceManager处理:servicemanager收到请求后,把服务名称和Binder引用存入一个内部链表(
svclist)。 - 通知Binder驱动:ServiceManager还会告诉Binder驱动:“这个服务我接管了,以后有人查它,你直接告诉我。”
避坑指南:我曾经在自定义系统服务时,忘了设置服务的权限检查。结果任何App都能调用我的服务接口,差点酿成安全漏洞。记住:注册服务时,一定要在服务接口里做好权限校验,别指望ServiceManager帮你做这件事。
29.3 系统服务获取流程
获取服务的过程,其实就是注册的逆过程。App端调用getService()时,流程是这样的:
// 客户端:获取服务
sp<IBinder> binder = defaultServiceManager()->getService(String16("media.player"));
// 然后通过interface_cast转换成具体接口
sp<IMediaPlayerService> service = interface_cast<IMediaPlayerService>(binder);
这里有个性能优化点,你想想看:每次调用getService()都要跨进程去ServiceManager查一次,如果频繁调用,性能会很差。所以Android在ServiceManager的客户端做了缓存机制。第一次获取后,Binder引用会缓存在进程内部,下次直接拿缓存。
但缓存也有坑。我记得有一次,系统服务因为异常重启了,但客户端还拿着旧的Binder引用。这时候调用服务接口,就会收到DEAD_OBJECT异常。嗯,正确的做法是监听Binder的死亡通知:
// 注册死亡通知
binder->linkToDeath(
death_recipient,
cookie,
flags
);
// 死亡回调中重新获取服务
void binderDied(const wp<IBinder>& who) {
// 重新获取服务
sp<IBinder> newBinder = defaultServiceManager()->getService(service_name);
// 更新缓存
}
29.4 Binder线程池:服务的“工人团队”
好,服务注册好了,客户端也拿到了Binder引用。那客户端发起调用时,服务端谁来处理?这就是Binder线程池的职责了。
每个使用Binder的进程,在初始化时都会创建一个Binder线程池。这个线程池的大小不是固定的,它会根据负载动态调整。但有一个上限,默认是16个线程。为什么是16?嗯,这是Google工程师权衡后的结果——既要保证并发处理能力,又不能占用太多线程资源。
Binder线程池的工作模式是这样的:
- Binder驱动收到客户端的调用请求后,把请求数据放到目标进程的Binder内核缓冲区。
- 内核通知目标进程的Binder线程池:“有活干了!”
- 线程池从空闲线程中挑一个,去处理这个请求。
- 处理完成后,线程把结果写回内核缓冲区,然后回到线程池等待下一个任务。
重要概念:Binder线程池中的线程是“同步”工作的。也就是说,一个线程在处理一个Binder调用时,不能同时处理另一个。如果所有线程都在忙,新的请求就得排队等待。这就是为什么有时候系统会卡顿——线程池满了,请求在排队。
我画了一张图,帮你理清ServiceManager、系统服务注册/获取、Binder线程池之间的关系:
29.5 线程池的扩容与收缩
Binder线程池不是一成不变的。当系统检测到所有线程都在忙,而且还有请求在排队时,它会尝试创建新的线程。但不会无限创建,上限就是16个。为什么?因为每个线程都要占用栈空间(默认是16KB或更大),线程多了内存开销大,而且线程切换也有成本。
反过来,如果线程池里的线程长时间空闲,系统会回收一部分,保持一个最小线程数(通常是1个)。这个机制有点像线程池的“弹性伸缩”。
注意:如果你在系统服务里做了耗时操作(比如网络请求、数据库写入),而这个操作是在Binder线程里执行的,那这个线程就会被长时间占用。如果同时有16个这样的请求,线程池就满了,其他所有Binder调用都得排队。这就是为什么我经常说:“不要在Binder线程里做耗时操作!”
29.6 实战经验总结
最后,我分享几个实战中总结的经验:
- 服务名称要唯一:系统服务的命名空间是全局的,别跟系统服务重名。我见过有人把自定义服务命名为“activity”,结果把系统服务覆盖了,系统直接崩溃。
- 获取服务要加重试:系统服务可能还没启动完,你调用
getService()可能返回空。建议加一个循环重试机制,每次等100ms,最多等5秒。 - 监控线程池状态:通过
/sys/kernel/debug/binder/proc/[pid]可以查看每个进程的Binder线程使用情况。我在排查性能问题时,经常看这个文件。 - 死亡通知一定要处理:系统服务可能因为各种原因重启(比如crash),你的App如果不处理死亡通知,就会一直拿着失效的Binder引用,调用时必崩。
好了,关于ServiceManager、系统服务注册与获取、Binder线程池的内容,我就讲到这里。这些是Android系统架构的基石,理解了它们,你就能明白系统服务是怎么运转的,也能在遇到问题时快速定位。