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 的位置标了出来,方便你理解。

批量模式与 FIFO 数据流 陀螺仪硬件 200Hz 采样 FIFO 缓冲区 容量 1024 组 连续写入 传感器驱动 中断 / 轮询 达到水位线 应用层 onSensorChanged() 批量上报 flush() 调用 强制清空 FIFO 立即读取 requestFlush() 插入同步标记 标记数据边界 图例 数据流 flush 指令 requestFlush 标记

避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

  • 不要频繁调用 flush():我曾经在一个 200Hz 的项目里每 10ms 调用一次 flush,结果 FIFO 永远存不满,批量模式形同虚设。建议根据你的水位线和采样率计算合理间隔。
  • 注意 onFlushCompleted 的线程:这个回调可能不在主线程。如果你要更新 UI,记得用 Handler 切回主线程。
  • FIFO 溢出问题:如果 CPU 处理速度跟不上传感器写入速度,FIFO 会溢出,旧数据被覆盖。我一般会监控 event.timestamp,如果发现时间戳跳跃,就说明有丢帧。
  • requestFlush 不是立即生效:它只是插入一个标记,真正的上报要等下一次批量触发。如果你需要立即获取数据,还是用 flush() 更靠谱。

我的习惯: 在调试阶段,我会在 onSensorChanged() 里打印 event.values.lengthevent.timestamp。如果发现长度总是 3,说明批量模式没开启。如果时间戳间隔不均匀,说明 FIFO 水位线设置不合理。这两个小技巧帮我快速定位了很多问题。

好了,关于批量模式与 FIFO 的内容就到这里。记住,flush() 是强制清空,requestFlush() 是标记边界。根据你的场景选对方法,才能让陀螺仪数据既完整又高效。

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