28、结合WorkManager:后台定时采集传感器数据

霍尔传感器数据采集,如果只在App在前台时做,那其实没什么挑战。真正的难点在于——用户把App切到后台,甚至锁屏了,你还能不能稳定地拿到数据?

我早年做过一个智能门磁项目,霍尔传感器检测门窗开关状态。用户反馈说“明明门开了,App却没记录”。查了半天,原来是App被系统杀进程了。嗯,从那以后,我再也不敢只用Service来做后台采集了。

今天我们就聊聊WorkManager。它是Android官方推荐的“后台任务之王”。说白了,就是帮你搞定“即使App被杀了,也能在指定时间醒来干活”这件事。

为什么选WorkManager?

你可能会问:我直接用Handler+Service不行吗?行,但坑很多。

  • 省电策略:系统对后台Service限制越来越严,Android 8.0以后后台Service几乎跑不起来
  • 生命周期:App被杀死,Service就没了,数据采集中断
  • 重试机制:网络不好、传感器临时不可用,你得自己写重试逻辑

WorkManager把这些都封装好了。它保证任务一定会执行,哪怕设备重启过。我个人习惯,凡是需要“定时、持久、可靠”的后台工作,一律用WorkManager。

核心思想:WorkManager不是“一直跑”,而是“在合适的时间醒来干活,干完就睡”。这对传感器采集来说非常友好。

整体架构

我们先看一张图,理清WorkManager采集霍尔传感器数据的完整流程:

WorkManager 定时采集霍尔传感器数据流程图 应用启动 创建 PeriodicWorkRequest 设置采集间隔(如15分钟) WorkManager.enqueue() 任务加入队列,系统调度 Worker.doWork() 执行 打开霍尔传感器 → 读取数据 → 存储/上报 → 返回Result 下次触发 系统保证:即使App被杀死,下次触发时间到仍会唤醒执行

流程其实不复杂。你只需要定义好“做什么”(Worker)和“多久做一次”(PeriodicWorkRequest),剩下的交给WorkManager调度。它会在合适的时机唤醒你的Worker,执行完就继续休眠。

实战:定时采集霍尔传感器数据

下面我们一步步实现。先加依赖:

// build.gradle (app级别)
dependencies {
    implementation "androidx.work:work-runtime-ktx:2.9.0"
}

第一步:定义Worker

Worker就是你的“干活单元”。每次WorkManager唤醒它,就会执行doWork()方法。

class HallSensorWorker(
    context: Context,
    params: WorkerParameters
) : Worker(context, params) {

    override fun doWork(): Result {
        return try {
            // 1. 获取霍尔传感器
            val sensorManager = applicationContext
                .getSystemService(Context.SENSOR_SERVICE) as SensorManager
            val hallSensor = sensorManager
                .getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)

            if (hallSensor == null) {
                Log.w("HallWorker", "设备不支持霍尔传感器")
                return Result.success()
            }

            // 2. 注册一次性监听(注意:这里用回调方式)
            val latch = CountDownLatch(1)
            var hallValue = 0f

            val listener = SensorEventListener {
                override fun onSensorChanged(event: SensorEvent) {
                    // 磁场强度,单位微特斯拉(μT)
                    hallValue = event.values[0]
                    latch.countDown()
                }
                override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
            }

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

            // 等待最多2秒获取数据
            latch.await(2, TimeUnit.SECONDS)
            sensorManager.unregisterListener(listener)

            // 3. 存储数据(这里用SharedPreferences演示)
            val prefs = applicationContext
                .getSharedPreferences("hall_data", Context.MODE_PRIVATE)
            prefs.edit()
                .putFloat("last_value", hallValue)
                .putLong("timestamp", System.currentTimeMillis())
                .apply()

            Log.d("HallWorker", "采集到霍尔值: $hallValue μT")
            Result.success()

        } catch (e: Exception) {
            Log.e("HallWorker", "采集失败", e)
            Result.retry()  // 失败后重试
        }
    }
}
💡 个人经验:这里用CountDownLatch等待传感器回调,是因为Worker的doWork()是在后台线程执行的,不能直接阻塞UI。我早期犯过一个错——在Worker里直接调Thread.sleep()等传感器数据,结果被系统判定为“卡死任务”直接杀掉。用CountDownLatch才是正道。

第二步:调度WorkManager

在Application或者Activity启动时,把任务注册进去:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        scheduleHallSensorCollection()
    }

    private fun scheduleHallSensorCollection() {
        // 每15分钟采集一次(最小间隔15分钟,系统强制)
        val collectRequest = PeriodicWorkRequestBuilder<HallSensorWorker>(
            15, TimeUnit.MINUTES
        )
            .setConstraints(
                Constraints.Builder()
                    .setRequiresBatteryNotLow(true)  // 电量不低时执行
                    .build()
            )
            .addTag("hall_sensor_collection")
            .build()

        WorkManager.getInstance(this)
            .enqueueUniquePeriodicWork(
                "hall_sensor_work",
                ExistingPeriodicWorkPolicy.KEEP,  // 如果已存在,保留旧的
                collectRequest
            )
    }
}
⚠️ 注意:PeriodicWorkRequest的最小间隔是15分钟。你设成5分钟也没用,系统会强制拉长到15分钟。这是Android省电策略的一部分。如果你需要更高频率的采集,请考虑前台Service。

数据存储与查看

采集到的数据存在SharedPreferences里。我们可以加一个简单的读取方法:

fun getLastHallValue(context: Context): Pair<Float, Long>? {
    val prefs = context.getSharedPreferences("hall_data", Context.MODE_PRIVATE)
    val value = prefs.getFloat("last_value", Float.NaN)
    val timestamp = prefs.getLong("timestamp", 0L)
    return if (timestamp > 0) Pair(value, timestamp) else null
}

在UI上展示时,我建议用RecyclerView展示历史记录。不过那是UI层面的东西了,今天我们聚焦后台采集。

避坑指南

我踩过的坑,列出来给你参考:

  • 传感器注册/注销要成对:Worker执行完一定要unregisterListener,否则传感器一直开着,耗电极快。我曾经有个版本忘记注销,用户反馈手机半天就没电了。
  • Result.retry()有退避策略:默认是10秒后重试,指数退避。如果你不想重试太多次,可以自定义BackoffPolicy。
  • UniquePeriodicWork的KEEP vs REPLACE:如果用户重新打开App,你希望保留旧任务还是替换?我建议用KEEP,避免重复创建多个任务。
  • 调试时手动触发:可以用adb命令立即执行一次Worker,不用等15分钟:adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS"

扩展:采集频率自适应

如果你觉得固定15分钟太死板,可以结合电量做自适应:

电量状态 采集间隔 说明
电量 > 50% 15分钟 正常采集
电量 20% ~ 50% 30分钟 降低频率
电量 < 20% 60分钟 保底采集

实现方式很简单:在Worker里读取当前电量,动态决定是否跳过本次采集。比如电量低于20%时,直接return Result.success(),什么都不做。

// 在doWork()开头加判断
val batteryManager = applicationContext
    .getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val battery = batteryManager.getIntProperty(
    BatteryManager.BATTERY_PROPERTY_CAPACITY
)

if (battery < 20) {
    Log.d("HallWorker", "电量不足,跳过本次采集")
    return Result.success()
}

这样做的好处是:既保证了数据连续性,又不会在低电量时雪上加霜。

小结

WorkManager + 霍尔传感器,说白了就是“定时醒来,干活,睡觉”。你不需要关心系统什么时候杀进程,也不需要写复杂的重试逻辑。WorkManager全帮你兜底了。

我个人建议,只要不是秒级采集需求,优先用WorkManager。它省心、省电、稳定。如果你真需要高频采集,那就得考虑前台Service + 前台通知了——那是另一个话题。

嗯,今天就到这里。代码可以直接拿去用,记得把依赖版本换成最新的。

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