26、Sensor KTX:传感器数据监听的KTX扩展,协程化回调

传感器开发,说实话,在Android里一直是个「写起来麻烦、用起来容易忘」的模块。你想想看,每次都要注册监听器、反注册、处理onSensorChanged回调,还得小心别泄漏Context。我早期做计步器功能时,就因为忘记在onPause里反注册传感器,导致用户手机电量哗哗往下掉——嗯,被测试小姐姐骂得不轻。

Sensor KTX就是为了解决这个痛点来的。它把传感器监听变成了协程,用Flow来传递数据。说白了,就是让你用挂起函数的方式去拿传感器数据,再也不用手动管理生命周期了。

为什么需要Sensor KTX?

先看看原生写法有多烦:

// 原生传感器监听
val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

val listener = object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
        // 处理数据
        event?.let {
            val x = it.values[0]
            val y = it.values[1]
            val z = it.values[2]
            // 更新UI...
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        // 精度变化处理
    }
}

// 注册
sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)

// 别忘了反注册!我经常忘...
override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(listener)
}

这段代码有什么问题?

  • 手动管理注册/反注册,容易泄漏
  • 回调在非UI线程,需要自己切线程
  • 多个传感器要写多个Listener
  • 测试起来很麻烦

我曾经在一个项目里,因为Activity重建导致传感器监听重复注册,结果手机直接卡死。从那以后,我就在想:能不能用协程把这套流程包装一下?

Sensor KTX的核心API

Sensor KTX其实就两个核心扩展函数,都在androidx.lifecycle:lifecycle-runtime-ktx包里(注意,不是单独的sensor-ktx库)。

函数 作用 返回值
SensorManager.awaitSensorEvent() 挂起函数,等待一次传感器事件 SensorEvent
SensorManager.sensorFlow() 返回Flow,持续发射传感器数据 Flow<SensorEvent>

这两个函数都接收一个Sensor对象和延迟参数。我个人习惯用sensorFlow(),因为大多数场景需要持续监听。

实战:用协程监听加速度传感器

来看一个完整的例子。假设我们要做一个「摇一摇」功能:

class ShakeDetector(context: Context) {
    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    fun observeShake(scope: LifecycleCoroutineScope): Flow<Unit> {
        return sensorManager.sensorFlow(accelerometer, SensorManager.SENSOR_DELAY_GAME)
            .map { event ->
                val x = event.values[0]
                val y = event.values[1]
                val z = event.values[2]
                val magnitude = sqrt(x * x + y * y + z * z)
                magnitude
            }
            .filter { magnitude -> magnitude > 20f } // 超过阈值认为摇动
            .map { Unit }
    }
}

// 在Activity中使用
class MainActivity : AppCompatActivity() {
    private lateinit var shakeDetector: ShakeDetector

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        shakeDetector = ShakeDetector(this)

        lifecycleScope.launch {
            shakeDetector.observeShake(lifecycleScope)
                .collect {
                    Toast.makeText(this@MainActivity, "摇一摇触发!", Toast.LENGTH_SHORT).show()
                }
        }
    }
}

你看,代码清爽多了。不需要手动注册/反注册,lifecycleScope会自动在onDestroy时取消协程,传感器监听自然就停了。这就是协程化回调的魅力。

核心要点:Sensor KTX的本质是把SensorEventListener回调包装成了Flow。Flow的取消机制天然解决了反注册问题。

多个传感器同时监听

实际项目中,经常需要同时监听多个传感器。比如做AR应用,需要加速度计+陀螺仪+磁力计。用原生写法,你得写三个Listener。用KTX,可以这样:

fun observeMultipleSensors(scope: LifecycleCoroutineScope) {
    val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    val gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
    val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)

    scope.launch {
        combine(
            sensorManager.sensorFlow(accelerometer),
            sensorManager.sensorFlow(gyroscope),
            sensorManager.sensorFlow(magnetometer)
        ) { accel, gyro, magnet ->
            Triple(accel.values, gyro.values, magnet.values)
        }.collect { (accelValues, gyroValues, magnetValues) ->
            // 同时处理三个传感器的数据
            Log.d("SensorFusion", "accel=$accelValues, gyro=$gyroValues, magnet=$magnetValues")
        }
    }
}

这里用了combine操作符,三个Flow会合并成一个。每次任何一个传感器有新数据,都会触发一次合并。嗯,要注意的是,三个传感器的采样率可能不同,所以合并后的数据频率取决于最快那个。

避坑指南:采样率与背压

我曾经在做一个体感游戏时,把传感器延迟设成了SENSOR_DELAY_FASTEST,结果Flow发射数据太快,collect里的处理逻辑跟不上,导致协程挂起队列越积越多。最后手机发烫,帧率暴跌。

解决方案有两个:

  • 使用conflate操作符:只保留最新的传感器数据,丢弃来不及处理的旧数据
  • 使用sample操作符:固定时间窗口内只取一次数据
// 方案一:conflate
sensorManager.sensorFlow(sensor, SensorManager.SENSOR_DELAY_FASTEST)
    .conflate() // 只保留最新值
    .collect { event ->
        // 处理逻辑
    }

// 方案二:sample(我比较推荐)
sensorManager.sensorFlow(sensor, SensorManager.SENSOR_DELAY_FASTEST)
    .sample(100) // 每100毫秒取一次
    .collect { event ->
        // 处理逻辑
    }

个人建议:大多数场景用SENSOR_DELAY_NORMALSENSOR_DELAY_UI就够了。只有游戏或AR才需要SENSOR_DELAY_GAME。别一上来就FASTEST,那是给自己找麻烦。

传感器数据与生命周期

Sensor KTX和lifecycleScope配合使用是最佳实践。但要注意一点:lifecycleScope默认在onDestroy时取消,如果你希望在onPause时就停止传感器监听(比如息屏时),需要自己控制:

class SensorAwareActivity : AppCompatActivity() {
    private var sensorJob: Job? = null

    override fun onResume() {
        super.onResume()
        sensorJob = lifecycleScope.launch {
            sensorManager.sensorFlow(accelerometer)
                .collect { event ->
                    // 处理数据
                }
        }
    }

    override fun onPause() {
        super.onPause()
        sensorJob?.cancel() // 息屏时停止监听,省电
    }
}

或者更优雅的方式,用repeatOnLifecycle

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.RESUMED) {
        sensorManager.sensorFlow(accelerometer)
            .collect { event ->
                // 只在RESUMED状态下收集
            }
    }
}

这样,Activity进入后台时自动取消,回到前台时重新开始。省电又安全。

SVG:Sensor KTX核心流程

Sensor KTX 协程化传感器监听流程 传感器硬件 SensorManager sensorFlow() / awaitSensorEvent() 将回调包装为 Flow Flow 操作符:map / filter / conflate / sample / combine 数据转换 → 过滤 → 背压处理 → 多传感器合并 原生痛点 手动注册/反注册 容易内存泄漏 线程切换麻烦 多传感器难管理 KTX优势 自动生命周期管理 协程取消即反注册 Flow操作符丰富 代码简洁易测试

总结

Sensor KTX不是什么黑科技,它只是把Android官方API用Kotlin协程重新包装了一下。但就是这层包装,让传感器开发从「小心翼翼」变成了「随心所欲」。

我个人觉得,最实用的三个点:

  1. 自动反注册:协程取消时自动停止监听,再也不用担心泄漏
  2. Flow操作符:map、filter、combine这些操作符让数据处理变得优雅
  3. 生命周期感知:和lifecycleScope配合,天然支持前后台切换

如果你还在用原生SensorEventListener写传感器代码,不妨试试Sensor KTX。相信我,一旦用上就回不去了。

注意:Sensor KTX目前还在alpha阶段,API可能会有变动。建议在正式项目中使用时,锁定版本号,避免升级带来的兼容性问题。


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