17、批量模式与 FIFO:使用 flush() 和 requestFlush() 处理批量数据
各位同学,今天我们来聊一个很实在的话题——批量模式与 FIFO。
说实话,刚接触 Android 传感器时,我总觉得数据是一来一回的。你注册一个监听器,它每秒给你 50 次回调,很公平对吧?但后来我在一个需要连续记录姿态的项目里,发现了一个严重问题:当系统负载高时,回调会丢失,数据出现断层。嗯,这就是我们今天要解决的痛点。
为什么需要批量模式?
先想想看,传感器硬件本身是独立于 CPU 的。它有自己的时钟,以固定频率采集数据。如果每次采集都立刻唤醒 CPU,那功耗会非常高。尤其对于陀螺仪这种高频传感器(200Hz 甚至更高),CPU 会被频繁打断。
批量模式的核心思路很简单:让传感器把数据先存到自己的 FIFO 缓冲区里,等攒够一批,再一次性交给 CPU。这样 CPU 可以更长时间处于休眠状态,省电又高效。
关键概念:
- FIFO(先进先出):传感器内部的一块硬件缓冲区,按顺序存储采样数据。
- 批量模式:数据先写入 FIFO,达到阈值或收到 flush 指令后,再批量上报。
- 非批量模式:每次采样都立即上报,实时性高但功耗大。
FIFO 的容量与水位线
每个传感器的 FIFO 大小不同。比如我常用的 BMI160 陀螺仪,FIFO 能存 1024 组数据。你可以设置一个「水位线」,比如存满 50 组就触发一次上报。这样既不会太频繁,也不会让数据积压太久。
我在项目中遇到过一个问题:水位线设得太低(比如 5 组),结果 CPU 还是频繁被唤醒,省电效果不明显。后来我改成 80 组,配合 200Hz 采样率,大约 0.4 秒上报一次,功耗直接降了 40%。
| 采样率 | FIFO 大小 | 水位线(组) | 上报间隔 | 功耗表现 |
|---|---|---|---|---|
| 200 Hz | 1024 | 50 | 0.25 s | 中等 |
| 200 Hz | 1024 | 200 | 1.0 s | 优秀 |
| 200 Hz | 1024 | 10 | 0.05 s | 较差 |
flush() 与 requestFlush() 的区别
这两个方法名字很像,但用途完全不同。我刚开始也搞混过,后来踩了坑才记住。
- flush():强制将 FIFO 中当前所有数据立即上报,并清空缓冲区。调用后,你会收到一个
onFlushCompleted()回调。 - requestFlush():请求传感器在下次上报时包含一个「flush 完成」事件。它不会立即清空 FIFO,而是标记一个同步点。
说白了,flush() 是「立刻给我全部数据」,requestFlush() 是「下次上报时告诉我数据边界在哪里」。
注意: 调用 flush() 后,FIFO 会被清空。如果你在批量模式下连续调用 flush(),可能会丢失两次 flush 之间的数据。我曾经在一个实时绘图项目里每 100ms 调用一次 flush,结果发现数据曲线出现锯齿——因为 FIFO 永远攒不够一批,每次都被强制清空了。
代码实战:使用 flush() 处理批量数据
下面是一个完整的示例。我们注册陀螺仪,开启批量模式,然后每 2 秒调用一次 flush() 获取累积数据。
// 获取传感器管理器
SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
// 创建监听器
SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
// 批量模式下,event.values 包含多组数据
// 每组数据有 3 个值:x, y, z
int batchSize = event.values.length / 3;
for (int i = 0; i < batchSize; i++) {
float x = event.values[i * 3];
float y = event.values[i * 3 + 1];
float z = event.values[i * 3 + 2];
Log.d("Gyro", "Batch[" + i + "]: " + x + ", " + y + ", " + z);
}
}
@Override
public void onFlushCompleted(SensorEvent event) {
Log.d("Gyro", "Flush completed. FIFO cleared.");
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// 精度变化处理
}
};
// 注册传感器,开启批量模式
// 第二个参数是采样延迟(微秒),第三个参数是最大报告延迟(微秒)
// 最大报告延迟 > 0 即开启批量模式
sensorManager.registerListener(listener, gyro,
SensorManager.SENSOR_DELAY_NORMAL, 500000); // 500ms 最大延迟
// 每 2 秒调用一次 flush
new Timer().schedule(new TimerTask() {
@Override
public void run() {
sensorManager.flush(listener);
}
}, 2000, 2000);
个人经验: 在 onSensorChanged() 中,event.values 的长度不是固定的。批量模式下,它可能包含多组数据。我习惯先判断 event.values.length 是否大于 3,如果是,就按批量处理。否则按单点处理。这样代码兼容性更好。
requestFlush() 的使用场景
你可能会问:既然有 flush(),为什么还要 requestFlush()?
举个例子。假设你在做手势识别,需要知道「从这一刻开始,到下一刻结束」之间的完整数据。如果用 flush(),你会丢失两次 flush 之间的数据边界。而 requestFlush() 会在 FIFO 中插入一个标记,当你收到 onFlushCompleted() 时,你就知道「之前的数据是上一段,之后的数据是下一段」。
我曾经在做一个空中写字项目时,就用 requestFlush() 来标记笔画的开始和结束。效果很好,数据分段非常干净。
// 在某个动作开始时调用
sensorManager.requestFlush(listener);
// 当 onFlushCompleted 回调时,表示之前的数据已经完整上报
// 你可以在这里重置手势状态
知识体系:批量模式与 FIFO 核心逻辑
下面这张图展示了从传感器硬件到应用层的完整数据流。我特意把 FIFO 和 flush 的位置标了出来,方便你理解。
避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
- 不要频繁调用 flush():我曾经在一个 200Hz 的项目里每 10ms 调用一次 flush,结果 FIFO 永远存不满,批量模式形同虚设。建议根据你的水位线和采样率计算合理间隔。
- 注意 onFlushCompleted 的线程:这个回调可能不在主线程。如果你要更新 UI,记得用 Handler 切回主线程。
- FIFO 溢出问题:如果 CPU 处理速度跟不上传感器写入速度,FIFO 会溢出,旧数据被覆盖。我一般会监控
event.timestamp,如果发现时间戳跳跃,就说明有丢帧。 - requestFlush 不是立即生效:它只是插入一个标记,真正的上报要等下一次批量触发。如果你需要立即获取数据,还是用
flush()更靠谱。
我的习惯: 在调试阶段,我会在 onSensorChanged() 里打印 event.values.length 和 event.timestamp。如果发现长度总是 3,说明批量模式没开启。如果时间戳间隔不均匀,说明 FIFO 水位线设置不合理。这两个小技巧帮我快速定位了很多问题。
好了,关于批量模式与 FIFO 的内容就到这里。记住,flush() 是强制清空,requestFlush() 是标记边界。根据你的场景选对方法,才能让陀螺仪数据既完整又高效。