19、功耗管理:长时间监听传感器时的电量优化技巧

传感器一开,电量就哗哗地掉。这应该是很多刚接触Android传感器开发的兄弟最头疼的事。我个人习惯把传感器比作一个“话痨”——你一旦注册了监听器,它就不停地跟你汇报数据,哪怕你根本不需要那么频繁。今天我们就聊聊,怎么让这个“话痨”学会闭嘴,只在必要的时候开口。

为什么传感器这么耗电?

说白了,传感器本身功耗其实不高。真正吃电的是三件事:

  • CPU被频繁唤醒:每次传感器有新数据,CPU就得从休眠状态爬起来处理。你想想看,一秒钟唤醒几十次,积少成多,电量就没了。
  • 数据传输开销:数据从传感器芯片传到应用层,中间要经过驱动、框架、应用,每一层都有开销。
  • 屏幕常亮:很多场景下,为了看传感器数据,屏幕一直亮着。这反而是最大的电老虎。

核心原则:能不监听就不监听,能少监听就少监听,能延迟处理就延迟处理。

技巧一:降低采样率——别让传感器“高频输出”

Android提供了四种预设的采样延迟:SENSOR_DELAY_NORMAL(200ms)、SENSOR_DELAY_UI(60ms)、SENSOR_DELAY_GAME(20ms)、SENSOR_DELAY_FASTEST(0ms)。很多新手一上来就用FASTEST,觉得数据越密集越好。其实大部分场景根本不需要。

我在项目中做过一个计步器,最初用了GAME延迟,结果手机半天就没电了。后来改成NORMAL,步数精度几乎没变,续航却翻了一倍。为什么?因为人走路频率也就每秒1-2步,200ms采样一次完全够用。

// 不推荐:高频采样
SensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_FASTEST);

// 推荐:根据场景选择合适延迟
SensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

我的习惯:先评估业务需要的最高频率,然后选一个刚好满足的延迟等级。比如做摇一摇功能,UI延迟就够了;做体感游戏才需要GAMEFASTEST

技巧二:批量处理——攒一波再干活

每次传感器回调都去更新UI或写文件,这是最傻的做法。CPU频繁切换上下文,功耗直线上升。更好的方式是:把数据先缓存起来,等攒够一批再统一处理。

我曾经做过一个跌倒检测App,最初在onSensorChanged里直接做FFT计算,手机烫得能煎鸡蛋。后来改成每100ms收集20个样本,然后一次性计算,功耗降了60%。

// 批量处理示例
private val buffer = mutableListOf<SensorEvent>()
private val handler = Handler(Looper.getMainLooper())
private val batchRunnable = Runnable {
    processBatch(buffer.toList())
    buffer.clear()
}

override fun onSensorChanged(event: SensorEvent?) {
    event?.let {
        buffer.add(it)
        // 每收集20个样本处理一次
        if (buffer.size >= 20) {
            handler.removeCallbacks(batchRunnable)
            batchRunnable.run()
        } else {
            handler.postDelayed(batchRunnable, 100)
        }
    }
}

注意:批量处理要设置超时机制,防止数据一直攒不够导致延迟过高。我一般会设一个最大等待时间(比如200ms),时间到了不管攒了多少都先处理。

技巧三:条件触发——只在需要时监听

很多场景下,我们并不需要持续监听传感器。比如屏幕旋转检测,只需要在用户横竖屏切换时触发一次。那为什么不让传感器一直开着?因为没必要。

我建议的做法是:用其他低功耗信号先做“预触发”,再开启传感器做精确判断。比如用光线传感器判断手机是否在口袋里,用接近传感器判断是否贴近耳朵,这些传感器的功耗比加速度计低得多。

// 条件触发示例:先监听低功耗的接近传感器
// 当接近传感器检测到手机被拿起时,再开启加速度计
private fun startAccelerometerOnCondition() {
    // 先注册接近传感器
    sensorManager.registerListener(proximityListener, 
        sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY),
        SensorManager.SENSOR_DELAY_NORMAL)
}

// 在接近传感器回调中
override fun onSensorChanged(event: SensorEvent?) {
    if (event?.values?.get(0) ?: 0f < proximityThreshold) {
        // 手机被拿起,开启加速度计
        sensorManager.registerListener(accelListener,
            sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
            SensorManager.SENSOR_DELAY_UI)
        // 同时关闭接近传感器
        sensorManager.unregisterListener(proximityListener)
    }
}

技巧四:注销监听——用完就关,别客气

这听起来像废话,但我在代码评审里见过太多人忘记在onPauseonStop里注销传感器。尤其是用Fragment的时候,页面切走了传感器还在跑,电量就这么白白浪费了。

我的习惯是:onResume注册,在onPause注销。如果某个功能只在特定页面需要,就在进入页面时注册,离开时注销。千万别在Application里全局注册传感器,除非你有非常充分的理由。

override fun onResume() {
    super.onResume()
    sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
}

override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(this)
}

避坑指南:我曾经在Service里注册了传感器,结果用户退出App后Service还在后台跑,传感器一直开着。后来我改用JobIntentService配合WakeLock,只在需要时短暂唤醒处理数据,处理完立刻释放。

技巧五:利用硬件FIFO——让传感器自己干活

Android从4.4开始支持传感器FIFO(先进先出缓冲区)。简单说,就是传感器芯片自己有个小内存,可以缓存一批数据,等攒够了再一次性通知CPU。这样CPU可以长时间休眠,只在需要时醒来处理。

怎么用?在注册监听器时指定maxReportLatencyUs参数(微秒)。比如设置成1秒,传感器就会把1秒内的数据缓存起来,然后一次性上报。CPU在这1秒内可以安心睡大觉。

// 使用FIFO批量上报,延迟1秒
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    sensorManager.registerListener(listener, accelerometer,
        SensorManager.SENSOR_DELAY_NORMAL,
        1_000_000) // 最大延迟1秒
}

注意:不是所有传感器都支持FIFO。你可以通过sensor.getFifoMaxEventCount()查看最大缓存数量。如果返回0,说明不支持。我一般会先检查一下,不支持就回退到普通模式。

知识体系总览

下面这张图总结了长时间监听传感器时的电量优化策略,你可以把它当作一个检查清单:

传感器电量优化策略 电量优化核心 ① 降低采样率:选合适的延迟等级 ② 批量处理:攒够样本再干活 ③ 条件触发:用低功耗传感器预判 ④ 及时注销:onPause里必须unregister ⑤ 硬件FIFO:让传感器自己缓存数据

总结一下

传感器功耗优化,说白了就是跟数据频率做斗争。你不需要每毫秒都知道手机在干嘛,只需要在关键的时刻拿到关键的数据。我个人习惯把这五个技巧记成一句话:降频、攒批、条件、注销、FIFO。每次写传感器代码时默念一遍,基本不会踩坑。

嗯,最后提醒一句:别忘了在真机上测试功耗。模拟器上测出来的数据跟实际差很远。我一般会用BatteryHistorian抓一份电量报告,看看传感器唤醒CPU的频率是不是合理。如果发现唤醒太频繁,就回头检查一下是不是哪个技巧没用到。

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