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_LOOPBC_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 线程状态流转

为了让你更直观地理解,我画了一张图:

Binder 线程状态流转图 空闲 等待命令 忙碌 执行事务 等待回复 同步等待 创建中 BR_SPAWN_LOOPER 退出 超时/异常 收到请求 处理完成 同步调用 收到回复 驱动通知 创建成功 超时(非主线程) 图例: 正常流转 创建流程 退出流程 主线程永不退出,子线程超时后自动退出

这张图把线程的五个状态和流转路径都画出来了。你重点关注两点:

  • 主线程永远在「空闲」和「忙碌」之间循环,不会进入「退出」状态
  • 子线程如果长时间空闲,会超时退出,释放资源

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 在「人」的层面是怎么运作的了。


公众号:蓝海资料掘金营,微信deep3321