16、采样率设置:通过 registerListener 的第三个参数设置采样频率

好,咱们接着聊。上一章我们把传感器监听器的注册讲完了,但有个关键参数我一直没细说——就是 registerListener 的第三个参数,采样频率。

说实话,这个参数我刚开始接触时也踩过坑。你以为设得越高越好?不一定。设得太低?数据又不够用。今天我就把这块掰开揉碎讲清楚。

registerListener 的完整签名

先看一眼完整的方法签名,心里有个底:

SensorManager.registerListener(
    SensorEventListener listener,
    Sensor sensor,
    int samplingPeriodUs,
    Handler handler
)

第三个参数 samplingPeriodUs,单位是微秒(μs)。它决定了传感器多久上报一次数据。说白了,就是采样间隔。

核心公式:

采样频率(Hz)= 1,000,000 / samplingPeriodUs(微秒)

举个例子:设 10,000μs,就是每秒 100 次采样。

系统预定义的四种采样模式

Android 贴心地给了四个常量,你不用自己算微秒数。我个人习惯先用这些常量,不够再调。

常量 对应值 采样频率 适用场景
SENSOR_DELAY_NORMAL 200,000μs 5 Hz 屏幕方向、简单计步
SENSOR_DELAY_UI 60,000μs 约 16 Hz UI 动画、游戏界面
SENSOR_DELAY_GAME 20,000μs 50 Hz 普通游戏、体感控制
SENSOR_DELAY_FASTEST 0μs 硬件最高 VR、高精度运动追踪

嗯,这里要注意——SENSOR_DELAY_FASTEST 并不是真正的 0,而是「尽可能快」。具体多快,取决于硬件能力。我在项目里见过有的手机能跑到 1000Hz,有的只能到 200Hz。

实际代码怎么写?

直接上代码,你一看就明白:

SensorManager sensorManager = 
    (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor gyroscope = 
    sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);

// 用 GAME 模式,50Hz 采样
sensorManager.registerListener(
    this,
    gyroscope,
    SensorManager.SENSOR_DELAY_GAME
);

如果你想自定义频率,比如想要 100Hz:

// 100Hz = 10,000μs
int customRate = 10000;
sensorManager.registerListener(
    this,
    gyroscope,
    customRate
);

我的小技巧:

如果你不确定设多少合适,先从 SENSOR_DELAY_GAME 开始。50Hz 对大多数体感应用都够用。跑起来发现数据太卡,再往下调。我曾经有个项目,一开始直接上 FASTEST,结果手机发烫,电池哗哗掉——得不偿失。

采样率不是越高越好

你想想看,采样率高了,数据量就大。数据量大,CPU 就得频繁处理。CPU 忙了,功耗就上去了。这是个三角关系:

采样率设置的三角平衡 高采样率 数据更细腻 低功耗 省电不发热 低CPU负载 系统不卡顿 功耗↑ CPU负载↑ 两者不可兼得 三者只能选两个,你需要根据场景做取舍

为什么会这样?因为传感器数据是硬件中断驱动的。你设了 10,000μs,硬件就每 10 毫秒产生一次中断。中断来了,CPU 就得从手头的工作切过来处理。切来切去,性能就损耗了。

我曾经踩过的坑:

有个 AR 项目,我图省事直接用了 SENSOR_DELAY_FASTEST。结果在低端机上,陀螺仪回调把 UI 线程堵死了,界面直接卡成 PPT。后来改成 SENSOR_DELAY_GAME,再配合后台线程处理数据,问题才解决。

所以我的建议是:先保守,再激进。从低频率开始,测试发现不够再往上加。

不同场景的推荐配置

根据我这些年做项目的经验,给你一个速查表:

应用场景 推荐采样率 理由
屏幕自动旋转 5-10 Hz 人眼对旋转变化不敏感,低频足够
计步器 10-20 Hz 步频最多每秒3步,20Hz绰绰有余
赛车游戏 50-100 Hz 需要及时响应方向盘转动
VR/AR 头部追踪 200-1000 Hz 延迟超过20ms就会晕,必须高频
运动姿态分析 100-200 Hz 人体动作频率一般不超过20Hz,但需要过采样做滤波

如何验证你设的采样率是否生效?

代码写完了,怎么知道实际跑的是多少?我一般用两种方法:

  1. 看时间戳差值:在 onSensorChanged 里打印 event.timestamp,相邻两次的差值就是实际采样间隔。
  2. 用 adb 命令adb shell dumpsys sensorservice,能看到每个传感器的当前采样率。

举个例子,你在回调里这么写:

long lastTimestamp = 0;

@Override
public void onSensorChanged(SensorEvent event) {
    if (lastTimestamp != 0) {
        long interval = event.timestamp - lastTimestamp;
        // 注意:timestamp 是纳秒,要转成微秒
        float actualRate = 1_000_000f / (interval / 1000);
        Log.d("Gyro", "实际采样率: " + actualRate + " Hz");
    }
    lastTimestamp = event.timestamp;
    
    // 处理陀螺仪数据...
    float x = event.values[0];
    float y = event.values[1];
    float z = event.values[2];
}

一个小细节:

event.timestamp 的单位是纳秒(ns),不是微秒。很多新手在这里翻车,算出来的频率差了 1000 倍。你记住:1 秒 = 1,000,000 微秒 = 1,000,000,000 纳秒。

采样率设了,但硬件不听话怎么办?

说实话,你设的采样率只是一个「建议值」。硬件不一定完全照做。我遇到过的情况:

  • 设了 50Hz,实际跑出 48Hz —— 正常,硬件有误差
  • 设了 100Hz,实际只有 30Hz —— 硬件能力不够,被降级了
  • 设了 200Hz,实际跑出 200Hz 但数据全是重复的 —— 传感器在「造假」

最后一种情况最坑。我记得有个项目,陀螺仪数据看起来频率很高,但仔细一看,连续 5 个数据一模一样。说白了,硬件能力跟不上,就重复上报旧数据充数。

怎么避免?我的做法是:永远不要完全相信采样率设置,要在代码里做校验。比如检查相邻数据是否完全相同,如果重复率超过 10%,就主动降低采样率。


好了,采样率这块就讲到这里。核心就一句话:够用就好,别贪高。下一章我们聊聊更实际的问题——数据来了之后,怎么处理才不卡顿。

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