9. Binder 线程管理:Binder 线程池的创建与管理,主线程与子线程的交互
好,我们继续往下聊。前面几章我们把 Binder 的驱动层、数据传递、内存映射都过了一遍。今天要聊的,是真正让 Binder 跑起来的「发动机」——线程管理。
说白了,Binder 本身不干活。它就是个信使。真正干活的是线程。你想想看,一个 Service 要同时处理十几个客户端的请求,谁来执行?线程。谁在等待回复?线程。谁在排队?还是线程。
所以,理解 Binder 的线程管理,你就理解了 Android 系统里「谁在什么时候做什么事」这个核心问题。
9.1 Binder 线程池的创建
先问一个问题:一个普通的 Android 应用,启动时有多少个 Binder 线程?
答案是:默认 1 个,但可以动态增长。
这个机制藏在 ProcessState 类里。每个进程只有一个 ProcessState 实例,它负责管理当前进程的 Binder 线程池。
// frameworks/native/libs/binder/ProcessState.cpp
ProcessState::ProcessState(const char* driver)
: mDriverName(String8(driver))
, mDriverFD(-1)
, mVMStart(MAP_FAILED)
, mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
, mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
, mExecutingThreadsCount(0)
, mMaxThreads(DEFAULT_MAX_BINDER_THREADS) // 默认 15
, mManuallySetMaxThreads(false)
, mCurrentThreads(0)
, mSpawnedThreadsCount(0)
, mKernelStartedThreads(0)
{
// ...
}
注意看 mMaxThreads,默认值是 15。这是 Android 给每个进程设定的 Binder 线程上限。为什么是 15?嗯,这是个经验值。我早期做系统优化时试过调大到 32,结果发现线程切换开销反而上去了,得不偿失。
线程池的创建时机很有意思。它不是在进程启动时就一股脑全建好,而是「按需创建」。第一次调用 IPCThreadState::self() 时,才会触发线程池的初始化。
void ProcessState::spawnPooledThread(bool isMain)
{
if (mThreadPoolStarted) {
String8 name = makeBinderThreadName();
sp<Thread> t = new PoolThread(isMain);
t->run(name.string());
}
}
这里有个关键参数 isMain。主线程的 isMain = true,子线程的 isMain = false。这个标记会影响线程在 Binder 驱动里的行为,我们后面会细说。
核心要点:Binder 线程池是懒加载的。只有真正需要处理 Binder 请求时,线程才会被创建。最大数量由 mMaxThreads 控制,默认 15 个。
9.2 主线程与子线程的交互
好,线程池有了,那线程之间怎么配合?
我个人习惯把 Binder 线程分为两类:
- 主线程(Main Thread):进程启动时创建的第一个 Binder 线程,负责处理核心事务。
- 子线程(Sub Thread):后续按需创建的 Binder 线程,处理并发请求。
它们的交互,说白了就是「谁有空谁上」。但这里有个坑——Binder 驱动会尽量把同一个客户端的请求发给同一个线程。为什么?为了利用 CPU 缓存。我记得有一次排查性能问题,发现某个 Service 的响应时间忽高忽低,最后定位到就是因为线程切换太频繁,缓存老是失效。
9.3 线程池的扩容与收缩
线程池不是一成不变的。它会根据负载动态调整。
扩容的触发条件很简单:当 Binder 驱动发现当前所有线程都在忙,而还有新的请求进来时,它会通知用户空间创建新线程。
这个通知是通过 BC_REGISTER_LOOP 和 BC_ENTER_LOOPER 这两个命令完成的。驱动在 binder_thread_read 里会检查是否需要创建新线程:
// kernel/drivers/android/binder.c
static int binder_thread_read(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer,
size_t size,
binder_size_t *consumed,
int non_block)
{
// ...
if (proc->requested_threads < proc->max_threads
&& proc->ready_threads == 0
&& proc->requested_threads + proc->ready_threads == 0) {
proc->requested_threads++;
// 通知用户空间创建线程
put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer);
}
// ...
}
这段代码的逻辑很清晰:
- 如果当前请求的线程数还没达到上限
- 并且没有空闲线程
- 并且没有正在创建的线程
那就发一个 BR_SPAWN_LOOPER 回去,让用户空间赶紧建个新线程。
避坑指南:我曾经遇到过一个问题:某个 Service 的 max_threads 被设置成了 1,结果所有客户端请求都串行处理,性能惨不忍睹。后来发现是某个第三方库偷偷改了配置。所以,如果你发现 Binder 调用特别慢,先查一下线程池上限有没有被误改。
收缩呢?Binder 不会主动销毁线程。线程只有在无事可做且超时后,才会自己退出。这个超时时间由 IPCThreadState::joinThreadPool() 控制:
void IPCThreadState::joinThreadPool(bool isMain)
{
// ...
do {
// 处理命令
result = getAndExecuteCommand();
if (result == TIMED_OUT && !isMain) {
// 非主线程超时,退出
break;
}
} while (result != -ECONNREFUSED);
// 退出前通知驱动
talkWithDriver(false);
}
注意,主线程永远不会因为超时而退出。它必须一直活着,因为它是进程的「根线程」。
9.4 线程状态流转
为了让你更直观地理解,我画了一张图:
这张图把线程的五个状态和流转路径都画出来了。你重点关注两点:
- 主线程永远在「空闲」和「忙碌」之间循环,不会进入「退出」状态
- 子线程如果长时间空闲,会超时退出,释放资源
9.5 主线程的特殊地位
为什么主线程这么特殊?因为它承担着「线程池守护者」的角色。
主线程是第一个进入 joinThreadPool(true) 的线程。它负责:
- 接收
BR_SPAWN_LOOPER通知,创建子线程 - 处理那些不能失败的关键事务
- 在子线程全部退出后,依然保持进程的 Binder 通信能力
我记得有一次调试一个系统服务,发现所有子线程都因为超时退出了,但服务还能正常工作。就是因为主线程还在。它就像个「光杆司令」,虽然人手不够,但至少还能接电话。
注意事项:千万不要在主线程里做耗时操作。Binder 主线程和 UI 主线程不是一回事,但道理一样——你堵住了主线程,整个进程的 Binder 通信就卡死了。我曾经见过一个案例,开发者在 Binder 回调里做了数据库查询,结果导致所有跨进程调用都超时。嗯,血的教训。
9.6 线程池的配置与调优
最后聊聊调优。虽然默认 15 个线程够用,但有些场景确实需要调整。
比如,一个文件共享服务,可能要同时处理几十个客户端的读写请求。这时候 15 个线程就不够用了。
调整方法很简单:
// Java 层
Process.setThreadPoolMaxThreadCount(32);
// Native 层
ProcessState::self()->setThreadPoolMaxThreadCount(32);
但别贪心。线程不是越多越好。我做过压测,当线程数超过 32 时,上下文切换的开销开始明显增加。超过 64 时,性能反而下降。
| 线程数 | 适用场景 | 建议 |
|---|---|---|
| 1-4 | 轻量级 Service,低频调用 | 保持默认 |
| 8-15 | 普通应用,中等并发 | 默认值即可 |
| 16-32 | 高并发 Service,文件服务 | 适当调大 |
| >32 | 极端场景 | 谨慎,需压测验证 |
我的建议:除非你明确知道瓶颈在 Binder 线程数上,否则别动默认值。我见过太多「我觉得线程不够用」然后盲目调大,结果性能更差的案例。先 profiling,再优化。
好了,Binder 线程管理这块就聊到这。线程池的创建、主线程与子线程的分工、状态的流转,这些搞明白了,你就能理解 Binder 在「人」的层面是怎么运作的了。