2、回调机制基础:回调函数概念、同步回调 vs 异步回调、回调线程模型

好,咱们正式开始聊回调机制。说实话,回调这个概念在Camera HAL里太常见了,几乎每个数据流、每个事件处理都离不开它。我刚开始接触Android Camera时,就被各种回调搞得晕头转向——什么process_capture_result、notify、signal_stream_flush……后来才慢慢理清楚,其实核心就三件事:回调是什么、怎么调、在哪个线程调。

2.1 回调函数概念

回调函数,说白了就是「你调用我提供的函数,我反过来调用你注册的函数」。嗯,有点绕是吧?我换个说法:你给系统留个电话号码,等有结果了系统打给你。这个「电话号码」就是回调函数。

在Camera HAL里,HAL层和Framework层之间就是通过回调来通信的。HAL层完成一帧图像处理后,不会傻等着Framework来取,而是直接调用Framework注册的回调函数,把数据「推」过去。

核心要点:回调是一种「反向控制」机制。调用者(HAL)在特定事件发生时,执行被调用者(Framework)预先注册的函数。

举个例子,HAL3的process_capture_request接口:

// Framework注册的回调函数指针
typedef void (*process_capture_result_t)(
    const camera3_callback_ops_t *ops,
    const camera3_capture_result_t *result);

// HAL层在完成拍照后调用
ops->process_capture_result(ops, &result);

你看,HAL层根本不需要知道Framework内部怎么处理这个result,它只管调用回调函数就行。这就是解耦——我经常跟团队说,回调的本质就是「我不关心你怎么做,我只关心你什么时候做完通知我」。

2.2 同步回调 vs 异步回调

这里有个坑,很多人一开始分不清。我当年在调试一个预览卡顿问题时,就栽在同步回调上。

同步回调

同步回调,就是调用者发起回调后,必须等回调函数执行完才能继续往下走。说白了,就是「你等着,我处理完再说」。

// 同步回调示例
void onCaptureResult(camera3_capture_result_t *result) {
    // 处理结果,可能耗时
    process_result(result);
    // 处理完才返回
}

// HAL层调用
ops->process_capture_result(ops, &result);
// 必须等上面的回调返回才能继续
prepare_next_request();

同步回调的好处是逻辑简单,数据一致性容易保证。但坏处也很明显——如果回调里干了重活,整个调用链就被卡住了。我在项目中遇到过,某个厂商在回调里做了JPEG编码,结果预览帧率直接掉到15fps。

避坑指南:我曾经在调试一个HAL实现时,发现同步回调里做了内存拷贝和格式转换,导致HAL的请求处理线程被阻塞,后续的capture request无法及时处理。最终表现就是拍照后要等很久才能拍下一张。

异步回调

异步回调就不一样了。调用者发起回调后,不等回调执行完,直接返回继续干自己的事。回调函数在另一个线程或上下文中执行。

// 异步回调示例
void onCaptureResultAsync(camera3_capture_result_t *result) {
    // 把结果丢到工作队列
    post_to_worker_thread(result);
    // 立即返回
}

// HAL层调用
ops->process_capture_result(ops, &result);
// 不等回调返回,直接继续
prepare_next_request();  // 这里可以立即执行

异步回调的核心是「非阻塞」。HAL层把结果扔给回调后,马上就能处理下一个请求。这在高速连拍场景下特别重要——你想想看,如果每帧都要等回调处理完才能发下一帧,那连拍速度就上不去了。

对比项 同步回调 异步回调
执行方式 调用者阻塞等待 调用者立即返回
线程模型 同一线程 不同线程
数据一致性 天然保证 需要加锁
适用场景 轻量操作、状态通知 耗时操作、高吞吐

我的建议:在Camera HAL里,回调函数本身应该尽量轻量。如果确实需要做耗时操作,就在回调里把任务派发到工作线程,而不是在回调里直接处理。这样既利用了异步回调的非阻塞优势,又避免了阻塞HAL的调用线程。

2.3 回调线程模型

嗯,线程模型这块,说实话是回调机制里最容易出问题的地方。我见过太多因为线程问题导致的死锁、数据竞争、ANR。

在Camera HAL3中,回调的线程模型大致是这样的:

// HAL3 回调接口定义
typedef struct camera3_callback_ops {
    void (*notify)(const camera3_callback_ops_t *ops,
                   const camera3_notify_msg_t *msg);
    
    void (*process_capture_result)(const camera3_callback_ops_t *ops,
                                   const camera3_capture_result_t *result);
    
    void (*signal_stream_flush)(const camera3_callback_ops_t *ops,
                                uint32_t num_streams,
                                const camera3_stream_t *const *streams);
} camera3_callback_ops_t;

这些回调函数在哪个线程执行?答案是:HAL层决定的。Framework只是注册了回调函数指针,具体在哪个线程调用,完全看HAL的实现。

常见的线程模型有几种:

  • 单线程模型:所有回调都在同一个线程里执行。简单,但容易阻塞。
  • 多线程模型:不同的回调在不同线程执行。比如notify在一个线程,process_capture_result在另一个线程。
  • 线程池模型:回调任务提交到线程池,由空闲线程执行。

我个人习惯用多线程模型。为什么?因为notify通常是事件通知,比如快门事件、错误事件,这些需要快速响应。而process_capture_result可能涉及大量数据处理,放在不同线程可以避免互相影响。

关键点:不管用哪种线程模型,HAL层必须保证回调函数的线程安全性。也就是说,同一个回调函数不能同时被多个线程调用。Framework侧通常不假设回调的线程模型,所以HAL层自己要做好同步。

举个例子,如果HAL用两个线程分别处理不同stream的capture result,那这两个线程可能同时调用process_capture_result。Framework内部如果没做好线程保护,就会出现数据竞争。所以,HAL层要么保证串行调用,要么在回调内部加锁。

// HAL层保证串行调用的示例
pthread_mutex_t callback_mutex = PTHREAD_MUTEX_INITIALIZER;

void hal_process_capture_result(camera3_capture_result_t *result) {
    // 加锁保证串行
    pthread_mutex_lock(&callback_mutex);
    ops->process_capture_result(ops, result);
    pthread_mutex_unlock(&callback_mutex);
}

避坑指南:我曾经遇到过一个case,HAL层在回调里又调用了Framework的另一个接口,而这个接口内部又触发了HAL的另一个回调。结果就是回调嵌套回调,最后栈溢出。所以,回调函数里尽量不要调用可能触发其他回调的接口,避免递归调用。

最后总结一下回调线程模型的核心原则:

  • 回调函数要尽快返回,不要在回调里做耗时操作
  • HAL层要保证回调的线程安全性
  • 避免回调嵌套和递归调用
  • 如果需要在回调里做复杂操作,派发到工作线程

嗯,回调机制基础就聊这么多。下一节我们会深入HAL3的具体回调接口,看看每个回调函数到底该怎么实现。到时候我会拿实际项目中的代码来拆解,保证让你看得明明白白。