29、Flow与传感器:传感器数据的Flow化、实时数据流处理、功耗优化

传感器,是Android设备和物理世界之间的桥梁。但说实话,原生的传感器API用起来挺别扭的——你得注册监听器、手动管理生命周期、还得小心别漏掉取消注册导致内存泄漏。我个人习惯是把传感器数据包装成Flow,这样整个数据流就变得优雅多了。

传感器数据的Flow化

先看一个最基础的例子。把加速度传感器包装成Flow:

class SensorFlowManager(private val sensorManager: SensorManager) {

    fun accelerometerFlow(): Flow<SensorEvent> = callbackFlow {
        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        if (sensor == null) {
            close(SensorException("设备不支持加速度传感器"))
            return@callbackFlow
        }

        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                event?.let { trySend(it) }
            }

            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
                // 精度变化,一般忽略
            }
        }

        sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)

        // 协程取消时自动清理
        awaitClose {
            sensorManager.unregisterListener(listener)
        }
    }
}

这里用了callbackFlow,它是专门为回调式API设计的。我在项目中遇到过一个问题:如果传感器返回数据太快,trySend可能会丢数据。解决办法是改用send配合缓冲:

fun accelerometerFlow(): Flow<SensorEvent> = callbackFlow {
    // ... 同上
    val listener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) {
            event?.let {
                // 使用send而不是trySend,配合缓冲
                launch {
                    send(it)
                }
            }
        }
    }
    // ...
}.buffer(Channel.CONFLATED) // 只保留最新值

CONFLATED缓冲策略很关键——它只保留最后一个值。对于传感器数据来说,旧数据没有意义,我们只关心最新的状态。

实时数据流处理

拿到传感器Flow之后,真正的乐趣才开始。你可以用Flow的操作符做各种实时处理。

采样降频:传感器通常每秒产生几十到几百次数据,但UI更新不需要那么快。

accelerometerFlow()
    .sample(100) // 每100毫秒取一个样本
    .collect { event ->
        // 更新UI
    }

过滤抖动:传感器数据有噪声,小幅度变化可能是干扰。

accelerometerFlow()
    .debounce(50) // 50毫秒内没有新数据才发射
    .collect { event ->
        // 处理稳定后的数据
    }

组合多个传感器:比如同时用加速度和陀螺仪做姿态检测。

val accelFlow = sensorManager.accelerometerFlow()
val gyroFlow = sensorManager.gyroscopeFlow()

accelFlow.combine(gyroFlow) { accel, gyro ->
    // 融合计算姿态
    val roll = calculateRoll(accel.values)
    val pitch = calculatePitch(accel.values)
    val yaw = integrateGyro(gyro.values)
    OrientationData(roll, pitch, yaw)
}.collect { orientation ->
    // 更新3D场景
}

我曾经做过一个AR导航项目,就是靠这种组合方式实现手机姿态跟踪的。一开始直接用原始数据,结果画面抖得厉害。后来加了debouncesample,效果好了很多。

核心思路:传感器数据流处理的关键是「降噪」和「降频」。降噪靠debounce和filter,降频靠sample。两者配合使用效果最佳。

功耗优化

传感器是耗电大户。你想想看,如果GPS、加速度计、陀螺仪全开,手机电量撑不了多久。我见过不少App因为传感器没管理好,被用户投诉耗电。

策略一:选择合适的采样率

采样率常量 延迟(微秒) 适用场景
SENSOR_DELAY_NORMAL 200,000 屏幕方向、简单手势
SENSOR_DELAY_UI 60,000 UI动画、游戏
SENSOR_DELAY_GAME 20,000 高帧率游戏
SENSOR_DELAY_FASTEST 0 专业测量、VR

别一上来就用FASTEST。我习惯先评估需求:如果只是检测手机是否翻转,NORMAL就够了。

策略二:按需注册和注销

Flow的awaitClose帮我们自动处理了注销。但要注意,如果页面不可见时还在收集传感器数据,那就是浪费。配合lifecycleScoperepeatOnLifecycle

class SensorViewModel : ViewModel() {
    private val sensorManager = SensorFlowManager(
        getSystemService(Context.SENSOR_SERVICE) as SensorManager
    )

    fun startCollecting() {
        viewModelScope.launch {
            sensorManager.accelerometerFlow()
                .collect { event ->
                    // 处理数据
                }
        }
    }
}

// 在Activity或Fragment中
override fun onResume() {
    super.onResume()
    viewModel.startCollecting()
}

override fun onPause() {
    super.onPause()
    viewModel.stopCollecting() // 取消协程
}

策略三:使用批处理

Android 13+支持传感器批处理,可以把多个事件打包一起上报,减少CPU唤醒次数。

val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
// 设置最大报告延迟,允许批处理
sensorManager.registerListener(
    listener,
    sensor,
    SENSOR_DELAY_NORMAL,
    1000000 // 最大延迟1秒,允许批处理
)

注意:批处理会增加延迟。如果你的应用需要实时响应(比如游戏),不要设置太大的最大延迟值。

知识体系总览

下面这张图概括了传感器Flow化的完整链路:

传感器数据Flow化处理链路 物理传感器 加速度/陀螺仪/磁力 callbackFlow 包装 SensorEvent → Flow Flow操作符处理 sample / debounce / combine 功耗优化 采样率/批处理/生命周期 实时数据流 collect / stateIn / shareIn 响应式UI更新

我的经验:传感器开发最容易踩的坑是「忘记注销监听器」。用Flow+callbackFlow之后,这个问题基本解决了。但要注意,如果多个地方同时收集同一个传感器Flow,建议用shareIn共享数据流,避免重复注册传感器。

嗯,传感器Flow化其实就三件事:包装成Flow、用操作符处理、管好功耗。把这三步走稳了,你的传感器应用就能既流畅又省电。

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