18、WorkManager的持久化与数据库:Room数据库存储、任务历史记录查询、清除旧任务记录、数据库版本迁移注意事项
说实话,很多开发者用WorkManager的时候,都把它当成一个“提交完就忘”的工具。任务丢进去,回调里处理结果,完事。但真正到了生产环境,你会发现——任务的历史记录、状态追踪、数据清理,这些才是让你睡个安稳觉的关键。
我早期带团队做的一个项目,线上出了个bug:某个后台同步任务莫名其妙失败了,但日志早就被覆盖了。我们花了整整两天才定位到问题——就是因为没有持久化任务状态。从那以后,我对WorkManager的持久化机制就格外上心。
这一章,我们就来聊聊WorkManager背后的数据库存储机制。说白了,就是Room数据库。你提交的每一个WorkRequest,最终都会被序列化后存进SQLite。WorkManager自己维护了一套表结构,但我们也可以利用它来做一些“额外的事情”。
WorkManager的默认数据库结构
WorkManager内部使用Room作为持久化层。数据库名是 androidx.work.workdb。你可以在设备的 /data/data/你的包名/databases/ 目录下找到它。
核心表包括:
| 表名 | 作用 |
|---|---|
WorkSpec |
存储每个WorkRequest的配置信息(约束、延迟、重试策略等) |
WorkTag |
存储WorkRequest的标签,用于按标签查询任务 |
SystemIdInfo |
存储系统JobScheduler或AlarmManager的ID映射 |
Dependency |
存储任务之间的依赖关系 |
WorkProgress |
存储任务的进度信息(如果你用了setProgress) |
Preference |
存储WorkManager内部的一些偏好设置 |
嗯,这里要注意:这些表是WorkManager内部维护的,你不应该直接去修改它们。但你可以通过WorkManager提供的API来查询和操作。
如何查询任务历史记录
我个人习惯用 WorkManager.getWorkInfosByTag() 或 getWorkInfosForUniqueWork() 来获取任务状态。但如果你想要更灵活的查询——比如按时间范围、按结果类型——那就得自己动手了。
WorkManager从2.7.0开始,提供了 WorkQuery 类,支持组合查询条件:
val workQuery = WorkQuery.Builder
.fromTags(listOf("sync"))
.addStates(listOf(WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED))
.build()
val workInfos: ListenableFuture<List<WorkInfo>> =
workManager.getWorkInfos(workQuery)
workInfos.addListener({
val result = workInfos.get()
// 处理查询结果
}, mainExecutor)
我在项目中遇到过一个问题:用户反馈某个任务一直失败,但不知道从什么时候开始的。后来我加了一个自定义的“任务日志表”,每次任务执行完毕,就把关键信息写进去。这样就能按时间线回溯了。
自定义任务日志表
说白了,就是自己建一个Room数据库,把WorkManager的任务ID、状态、时间戳、错误信息存下来。这样查询起来更灵活,而且不会影响WorkManager的内部逻辑。
@Entity(tableName = "task_log")
data class TaskLog(
@PrimaryKey val id: String,
val workRequestId: String,
val state: String,
val timestamp: Long,
val errorMessage: String?
)
@Dao
interface TaskLogDao {
@Query("SELECT * FROM task_log ORDER BY timestamp DESC")
fun getAllLogs(): LiveData<List<TaskLog>>
@Query("SELECT * FROM task_log WHERE workRequestId = :id")
fun getLogsByWorkId(id: String): List<TaskLog>
@Insert
fun insert(log: TaskLog)
@Query("DELETE FROM task_log WHERE timestamp < :cutoff")
fun deleteOldLogs(cutoff: Long)
}
然后在Worker的 doWork() 里,根据结果插入一条日志:
class SyncWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
return try {
// 执行同步逻辑
val db = AppDatabase.getInstance(applicationContext)
db.taskLogDao().insert(TaskLog(
id = UUID.randomUUID().toString(),
workRequestId = id.toString(),
state = "SUCCESS",
timestamp = System.currentTimeMillis(),
errorMessage = null
))
Result.success()
} catch (e: Exception) {
db.taskLogDao().insert(TaskLog(
id = UUID.randomUUID().toString(),
workRequestId = id.toString(),
state = "FAILED",
timestamp = System.currentTimeMillis(),
errorMessage = e.message
))
Result.retry()
}
}
}
你想想看,有了这个表,排查问题就方便多了。直接按时间倒序查,一目了然。
清除旧任务记录
日志表不能无限增长。我建议定期清理。可以在Application启动时,或者每次查询前,执行一次清理:
fun cleanOldLogs(daysToKeep: Int = 30) {
val cutoff = System.currentTimeMillis() - daysToKeep * 24 * 60 * 60 * 1000L
db.taskLogDao().deleteOldLogs(cutoff)
}
另外,WorkManager本身也提供了 pruneWork() 方法,用来清理已经完成且没有依赖的任务记录。不过这个方法只清理WorkManager内部的数据,不会动你的自定义表。
pruneWork(),记得在合适的时机调用。我一般放在 onDestroy() 或者应用进入后台时触发。别频繁调用,毕竟数据库操作也是有开销的。
数据库版本迁移注意事项
这是个大坑。WorkManager内部数据库版本是它自己管理的。如果你升级了WorkManager的版本,它内部会自动做迁移。但如果你自己加了自定义表,那就得小心了。
我曾经遇到过一个问题:升级WorkManager从2.6到2.8后,应用启动时崩溃了。原因是WorkManager内部数据库版本从5升到了7,而我的自定义数据库也用了同一个 Room.databaseBuilder,结果迁移策略没写对。
解决方案很简单:自定义数据库和WorkManager的数据库分开。不要混用。
// 错误做法:混在一起
@Database(entities = [TaskLog::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
// 这样会和WorkManager的数据库冲突
}
// 正确做法:分开建库
@Database(entities = [TaskLog::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
abstract fun taskLogDao(): TaskLogDao
}
// 初始化时指定不同的数据库名
val db = Room.databaseBuilder(
context.applicationContext,
TaskDatabase::class.java,
"task_log_db" // 不要用 "androidx.work.workdb"
).build()
知识体系总览
下面这张图,帮你理清WorkManager持久化相关的核心脉络:
这张图展示了三个核心方向:内部数据库(WorkManager自带的)、自定义日志表(我们手动建的)、查询与清理API(官方提供的操作入口)。三者配合使用,才能构建一个健壮的任务追踪体系。
好了,这一章的内容就到这里。记住一句话:持久化不是可选项,而是生产环境的必需品。别等到线上出问题了,才后悔没早点做。