深入 Android Sensor 框架全链路:从 SensorManager API 到 Sensor HAL
做计步器 SDK 时遇到过一个棘手的问题:步数传感器在前台工作正常,App 切入后台 5 分钟后数据就停了。排查发现有两个系统服务在抢同一个物理传感器,而我们对 Android Sensor 框架的理解只停留在 registerListener 那一层。
从 API 调起到硬件中断触发,传感器数据要经历一条比想象中更长的链路。这篇文章顺着调用栈从上往下走一遍。
SensorManager 封装了什么
大多数开发者接触到的第一行传感器代码是这样的:
val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
getSystemService 返回的实际上是 SystemSensorManager,它在构造时获取了 SensorManager 这个系统服务的 Binder 代理。registerListener 也没做什么复杂的事——把参数打包,跨进程发给 SensorService 就结束了。但有两个点踩坑的人很多。
回调线程由调用方决定。registerListener 有一个重载接受 Handler 参数,不传则默认在主线程回调。传感器高频数据(比如游戏场景 50Hz 的陀螺仪)在主线程处理会直接导致掉帧。实际项目中我会专门开一个 HandlerThread:
val sensorThread = HandlerThread("SensorThread", Process.THREAD_PRIORITY_URGENT_DISPLAY)
sensorThread.start()
val handler = Handler(sensorThread.looper)
sensorManager.registerListener(listener, gyroscope, 10000, handler)
采样周期只是建议值,不是硬性约定。SENSOR_DELAY_GAME(20000μs)和 SENSOR_DELAY_UI(66667μs)这些常量最终被转换为微秒级参数传给底层,但硬件不一定能精确支持。系统会取一个「不小于请求值且硬件支持」的速率。如果你的算法要求严格时间对齐,必须在回调中用 SensorEvent.timestamp 做插值,而不是假设数据是等间隔的。
SensorService 的中枢调度
SensorService 是运行在 system_server 进程中的一个 C++ 服务,核心职责就三个:管理客户端连接、调度传感器硬件、分发数据。
每个注册了监听器的客户端对应一个 SensorEventConnection 对象。这个对象里有一条关键的 IPC 通道——BitTube。它不是 Binder 调用,而是一对 Unix domain socket(基于 socketpair 的管道),专门用来高频传输传感器事件。Binder 只走控制指令(注册、注销、设置参数),数据通路走 BitTube,绕开了 Binder 线程池的开销。
传感器激活策略是引用计数式的:第一个客户端注册时,调用 HAL 的 activate(handle, 1) 打开传感器;最后一个客户端注销时,调用 activate(handle, 0) 关闭。这也是开篇那个问题的根因——某个三方 SDK 持有传感器引用,导致我们的注册被降级到了辅助角色。
数据分发走广播模型:SensorService::onSensorEvent 中,持有该传感器连接的客户端都会收到一份数据副本。没有优先级队列,先注册先收到,公平调度。
HAL 层:sensors.h 是硬件厂商的履约协议
Sensor HAL 的接口定义在 hardware/libhardware/include/hardware/sensors.h,核心结构体是 sensors_poll_device_t,暴露了五个关键方法:
| 方法 | 作用 |
|---|---|
activate(sensor_handle, enabled) | 开启/关闭指定传感器 |
setDelay(sensor_handle, sampling_period_ns) | 设置采样周期 |
poll(events, max_count) | 阻塞读取传感器事件 |
batch(sensor_handle, sampling_period_ns, max_report_latency_ns) | 设置批处理参数 |
flush(sensor_handle) | 立即落盘 FIFO 中的所有数据 |
HAL 层之上,SensorDevice 类负责管理这个接口。它起了一个线程,在循环中阻塞调用 poll(),拿到事件后通过 BitTube 写回 SensorService。简化后的逻辑大致如下:
// SensorDevice 中的读取线程伪代码
while (!mStop) {
sensors_event_t buffer[16];
int n = mHalDevice->poll(mHalDevice, buffer, 16);
for (int i = 0; i < n; i++) {
// 写入 SensorEventConnection 的 BitTube
sendToConnections(buffer[i]);
}
}
poll 的返回时机由硬件中断驱动。加速度计每次采样触发一次中断,温度传感器可能几秒才触发一次——poll 会阻塞等待,所以 SensorDevice 线程大部分时间在睡眠,不消耗 CPU。
传感器融合:Virtual Sensor 是怎么算出来的
Android 的 TYPE_ROTATION_VECTOR(旋转向量)没有对应的物理硬件。它是一个虚拟传感器(Virtual Sensor),由加速度计 + 陀螺仪 + 磁力计的数据融合计算得出。
融合逻辑在 SensorService 内部,不依赖 HAL。具体实现在 Fusion.cpp 中,底层是一个扩展卡尔曼滤波器(EKF),Android 做了工程简化:
- 陀螺仪积分提供姿态预测
- 加速度计提供重力方向参考(修正俯仰和横滚)
- 磁力计提供水平方向参考(修正偏航角)
三个物理传感器各有自己的采样周期,但融合输出需要统一频率。SensorService 的处理方式是:收到任一底层传感器的数据就触发一次融合计算,输出频率约等于最快的那个传感器的频率。这样做不保证三者时间戳严格对齐——如果某个传感器掉数据(比如磁力计被降频),旋转向量的精度会明显下降。实际项目中判断旋转向量是否可用,我习惯检查 SensorEvent.values[4](估计精度),超过 0.1 弧度就降级到只用陀螺仪的方案。
批处理与功耗:batch() 参数的讲究
从 Android 4.4(KitKat)开始,HAL 支持了传感器批处理,API 层面就是 registerListener 的 maxReportLatencyUs 参数,最终调用了 HAL 的 batch()。
批处理的本质是:传感器硬件有一个 FIFO 缓冲区,数据先堆积在硬件里,达到一定量或超时后再一次性上报。收益不在传感器本身,而在应用处理器(AP):AP 不用被每次采样中断唤醒,批量处理时可以把上下文切换开销合并掉。
// 典型的 batch 参数含义
batch(TYPE_ACCELEROMETER,
20000, // sampling_period_ns: 50Hz 采样
200000); // max_report_latency_ns: 200ms 上报一次
// 效果:传感器以 50Hz 采样,每 200ms 攒 10 个事件一次性上报
maxReportLatencyUs 设为 0 不等于禁用批处理,只是告诉 HAL「尽快上报」。实际行为取决于硬件,某些芯片在 latency=0 时仍然有 5-10ms 的硬件缓冲延迟。
另一个容易混淆的是 wake-up 传感器和 non-wake-up 传感器。wake-up 传感器(比如 TYPE_SIGNIFICANT_MOTION)的事件可以唤醒 AP,但功耗更高,且需要在 HAL 层额外走一条 SoC 到 AP 的中断通路。Non-wake-up 传感器数据只在 AP 清醒时才能被 Service 读到,睡眠期间的数据直接丢失。
Google 的 Activity Recognition API 底层用的就是这套复合策略:用低功耗的 significant motion 传感器做唤醒触发,唤醒后再打开高精度的加速度计做具体动作识别。
排查与优化要点
整理几个实际项目中的经验:
传感器数据没到?先查引用计数。 用 dumpsys sensorservice 可以看到每个传感器的激活状态和所有注册包名。被其他进程抢占的场景远比预想的多。
延迟不稳定?排查回调线程阻塞。 SensorService 通过 BitTube 写数据,但你那端的 HandlerThread 如果做了耗时操作,消息队列堆积会导致处理延迟越来越大。SensorService 不会等你,FIFO 满了旧数据直接被覆盖丢弃。
精度不够?换融合传感器。 单独用加速度计算倾斜角会有 ±5° 的误差,TYPE_GRAVITY(也是虚拟传感器)经由融合算法处理后能做到 ±1° 以内。除非绑定了自定义融合算法,否则直接用系统提供的结果更省心。
功耗敏感场景优先考虑硬件 FIFO。 同一套计步逻辑,配置 batch(100ms) 和 batch(20ms) 的功耗差异在手机上能达到 2-3 倍。这个参数可以动态调整——用户静止时拉大 latency,运动时缩小,兼顾精度和省电。