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_NORMAL或SENSOR_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不是什么黑科技,它只是把Android官方API用Kotlin协程重新包装了一下。但就是这层包装,让传感器开发从「小心翼翼」变成了「随心所欲」。
我个人觉得,最实用的三个点:
- 自动反注册:协程取消时自动停止监听,再也不用担心泄漏
- Flow操作符:map、filter、combine这些操作符让数据处理变得优雅
- 生命周期感知:和lifecycleScope配合,天然支持前后台切换
如果你还在用原生SensorEventListener写传感器代码,不妨试试Sensor KTX。相信我,一旦用上就回不去了。
注意:Sensor KTX目前还在alpha阶段,API可能会有变动。建议在正式项目中使用时,锁定版本号,避免升级带来的兼容性问题。
公众号:蓝海资料掘金营,微信deep3321