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升级就可能出问题。保持距离,各自安好。

知识体系总览

下面这张图,帮你理清WorkManager持久化相关的核心脉络:

WorkManager 持久化知识体系 WorkManager 持久化 内部数据库 (workdb) WorkSpec / WorkTag / Dependency SystemIdInfo / WorkProgress ⚠ 不要直接修改 自定义日志表 (task_log) TaskLog Entity + DAO 按时间/状态查询 ✅ 灵活可控 查询与清理 API WorkQuery / pruneWork() 自定义清理策略

这张图展示了三个核心方向:内部数据库(WorkManager自带的)、自定义日志表(我们手动建的)、查询与清理API(官方提供的操作入口)。三者配合使用,才能构建一个健壮的任务追踪体系。

好了,这一章的内容就到这里。记住一句话:持久化不是可选项,而是生产环境的必需品。别等到线上出问题了,才后悔没早点做。


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