WorkManager 是 Android Jetpack 组件,用于管理需可靠执行的后台任务,即使应用退出或设备重启也能保证运行。它支持一次性与周期性任务,可根据网络、电量等约束条件灵活调度。通过 Worker 定义任务逻辑,WorkRequest 配置执行方式,WorkManager 统一管理任务队列与状态。2.7.0 版本起支持加急任务,适合需要尽快执行的场景。WorkManager 并非替代线程,而是为可延迟且需保证完成的任务提供标准化解决方案,兼顾功能需求与系统资源优化。

博主博客

一. WorkManager 介绍

1.1 什么是 WorkManager?

WorkManager 是 Android Jetpack 的一部分,是一个用于管理后台任务的库。它旨在解决应用在退出或设备重启后仍需可靠运行任务的问题。

核心特性:

  • 向后兼容:内部根据设备 API 级别自动选择底层调度服务(JobScheduler, AlarmManager + BroadcastReceiver 等)
  • 生命周期感知:与 Activity/Fragment 生命周期解耦,但可观察任务状态
  • 约束感知:可在满足条件(如网络连接、充电状态)时执行任务
  • 灵活调度:支持一次性任务和周期性任务
  • 加急任务:Android 5.0+ 支持需要立即执行的任务

1.2 WorkManager 与线程的区别

WorkManager 不是 用来替代线程或协程的。Google 将后台任务分为四类:

任务类型 特点 推荐方案
Immediate Tasks 用户操作时需要立即完成的任务 Kotlin 协程、Thread
Exact Tasks 需要在精确时间执行的任务 AlarmManager
Expedited Tasks 需要尽快开始执行的任务 WorkManager (setExpedited)
Deferred Tasks 不需要立即执行,可延迟的任务 WorkManager

关键理解:WorkManager 最适合处理可延迟需要保证执行的任务,即使应用被关闭或设备重启。

1.3 加急任务(Expedited Work)

从 WorkManager 2.7.0(现已成为主流版本)开始,可以使用 setExpedited() 声明加急任务:

val request = OneTimeWorkRequestBuilder<MyWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

注意

  1. Android 12+ 要求:必须在前台服务中运行,需重写 getForegroundInfo() 方法
  2. 配额限制:系统对加急任务有限制,超出配额会降级为普通任务
  3. 立即执行:会尽快执行,但不保证立即执行

二. WorkManager 核心组件

2.1 Worker & CoroutineWorker

Worker:定义要执行的任务(同步执行)

class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        // 在主线程之外的线程执行
        try {
            // 执行任务
            return Result.success()
        } catch (e: Exception) {
            return Result.failure()
        }
    }
}

CoroutineWorker:支持协程的 Worker(推荐使用)

class MyCoroutineWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        // 在协程上下文中执行
        return try {
            // 可调用 suspend 函数
            performTask()
            Result.success(createOutputData())
        } catch (throwable: Throwable) {
            if (throwable is IOException && shouldRetry()) {
                Result.retry()  // 重试
            } else {
                Result.failure(createOutputData())
            }
        }
    }
    
    private suspend fun performTask() {
        delay(1000) // 可安全使用协程操作
    }
}

doWork() 返回值:

  • Result.success() - 任务成功完成
  • Result.failure() - 任务失败
  • Result.retry() - 任务需要重试(WorkManager 会根据退避策略重新调度)

2.2 WorkRequest

定义任务如何执行,有两种类型:
1. OneTimeWorkRequest(一次性任务)

// 创建输入数据
val inputData = workDataOf(
    "KEY_URL" to "https://example.com/file",
    "KEY_FILENAME" to "movie.mp4"
)

// 创建约束条件
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresCharging(true)
    .setRequiresBatteryNotLow(true)
    .build()

// 创建 WorkRequest
val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
    .setInputData(inputData)
    .setConstraints(constraints)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .addTag("download")
    .build()

2. PeriodicWorkRequest(周期性任务)

// 注意:最短间隔为 15 分钟
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    1, TimeUnit.HOURS, // 重复间隔
    15, TimeUnit.MINUTES // 弹性时间
).build()

2.3 WorkManager

管理 WorkRequest 的生命周期:

class MyViewModel : ViewModel() {
    private val workManager = WorkManager.getInstance(application)
    
    fun startDownload() {
        val downloadRequest = createDownloadRequest()
        
        // 1. 简单入队
        workManager.enqueue(downloadRequest)
        
        // 2. 唯一工作链(避免重复任务)
        workManager.beginUniqueWork(
            "unique_download_work",
            ExistingWorkPolicy.KEEP, // 或 REPLACE, APPEND
            downloadRequest
        ).enqueue()
        
        // 3. 链式任务
        workManager
            .beginWith(downloadRequest)
            .then(processRequest)
            .then(uploadRequest)
            .enqueue()
    }
    
    fun cancelWork() {
        // 通过ID取消
        workManager.cancelWorkById(workId)
        
        // 通过Tag取消
        workManager.cancelAllWorkByTag("download")
        
        // 取消唯一工作
        workManager.cancelUniqueWork("unique_download_work")
        
        // 取消所有工作
        workManager.cancelAllWork()
    }
}

2.4 观察任务状态

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 通过 LiveData 观察任务状态
        workManager.getWorkInfosByTagLiveData("download")
            .observe(this) { workInfos ->
                workInfos.firstOrNull()?.let { info ->
                    when (info.state) {
                        WorkInfo.State.ENQUEUED -> {
                            // 任务已入队,等待条件满足
                        }
                        WorkInfo.State.RUNNING -> {
                            // 任务正在运行
                            val progress = info.progress.getInt("PROGRESS", 0)
                            updateProgress(progress)
                        }
                        WorkInfo.State.SUCCEEDED -> {
                            // 任务成功完成
                            val result = info.outputData.getString("RESULT_KEY")
                            showResult(result)
                        }
                        WorkInfo.State.FAILED -> {
                            // 任务失败
                            showError()
                        }
                        WorkInfo.State.CANCELLED -> {
                            // 任务被取消
                        }
                        WorkInfo.State.BLOCKED -> {
                            // 任务被阻塞(例如等待前置任务完成)
                        }
                    }
                }
            }
        
        // 也可以通过 WorkQuery 进行复杂查询
        val workQuery = WorkQuery.Builder
            .fromTags(listOf("download"))
            .addStates(listOf(WorkInfo.State.RUNNING, WorkInfo.State.SUCCEEDED))
            .addUniqueWorkNames(listOf("unique_download_work"))
            .build()
        
        workManager.getWorkInfosLiveData(workQuery)
            .observe(this) { workInfos ->
                // 处理查询结果
            }
    }
}

三. 实践:文件下载示例

3.1 创建 Worker(支持前台服务)

class DownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    companion object {
        const val TAG = "DownloadWorker"
        const val INPUT_URL = "input_url"
        const val INPUT_FILENAME = "input_filename"
        const val OUTPUT_RESULT = "output_result"
        const val OUTPUT_FILE_PATH = "output_file_path"
        const val NOTIFICATION_ID = 1001
        
        // 创建通知渠道(Android 8.0+ 需要)
        fun createNotificationChannel(context: Context) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel = NotificationChannel(
                    "download_channel",
                    "文件下载",
                    NotificationManager.IMPORTANCE_LOW
                ).apply {
                    description = "显示下载进度"
                }
                val manager = context.getSystemService(NotificationManager::class.java)
                manager.createNotificationChannel(channel)
            }
        }
    }
    
    private lateinit var notificationBuilder: NotificationCompat.Builder
    
    // 重写此方法以支持加急任务(Android 12+ 必须)
    override suspend fun getForegroundInfo(): ForegroundInfo {
        val notification = createNotification("准备下载...", 0)
        return ForegroundInfo(NOTIFICATION_ID, notification)
    }
    
    override suspend fun doWork(): Result {
        createNotificationChannel(applicationContext)
        
        val url = inputData.getString(INPUT_URL)
        val filename = inputData.getString(INPUT_FILENAME)
        
        if (url.isNullOrEmpty() || filename.isNullOrEmpty()) {
            return Result.failure(workDataOf(OUTPUT_RESULT to "输入参数错误"))
        }
        
        return try {
            // 模拟下载进度
            for (progress in 0..100 step 10) {
                if (isStopped) {
                    return Result.failure()
                }
                
                // 更新进度(可通过观察 WorkInfo.progress 获取)
                setProgress(workDataOf("progress" to progress))
                
                // 更新通知
                updateNotification("下载中...", progress)
                
                delay(200) // 模拟下载耗时
            }
            
            // 下载完成
            val resultData = workDataOf(
                OUTPUT_RESULT to "下载完成",
                OUTPUT_FILE_PATH to "/storage/emulated/0/Download/$filename"
            )
            
            updateNotification("下载完成", 100, true)
            
            Result.success(resultData)
            
        } catch (e: Exception) {
            Log.e(TAG, "下载失败", e)
            updateNotification("下载失败", 0, true)
            Result.failure(workDataOf(OUTPUT_RESULT to "下载失败: ${e.message}"))
        }
    }
    
    private fun createNotification(contentText: String, progress: Int): Notification {
        // 创建取消下载的 PendingIntent
        val cancelIntent = WorkManager.getInstance(applicationContext)
            .createCancelPendingIntent(id)
        
        notificationBuilder = NotificationCompat.Builder(
            applicationContext,
            "download_channel"
        ).apply {
            setContentTitle("文件下载")
            setContentText(contentText)
            setSmallIcon(R.drawable.ic_download)
            priority = NotificationCompat.PRIORITY_LOW
            setOngoing(true)
            setOnlyAlertOnce(true)
            
            // 添加取消按钮
            addAction(
                R.drawable.ic_cancel,
                "取消",
                cancelIntent
            )
            
            // 设置进度条
            if (progress < 100) {
                setProgress(100, progress, false)
            } else {
                setProgress(0, 0, false)
            }
        }
        
        return notificationBuilder.build()
    }
    
    private fun updateNotification(
        contentText: String,
        progress: Int,
        isComplete: Boolean = false
    ) {
        if (::notificationBuilder.isInitialized) {
            notificationBuilder.apply {
                setContentText(contentText)
                
                if (isComplete) {
                    setOngoing(false)
                    setAutoCancel(true)
                    setProgress(0, 0, false)
                    
                    // 添加打开文件的 Action
                    val intent = Intent(Intent.ACTION_VIEW).apply {
                        // 这里应该设置实际的文件 URI
                        flags = Intent.FLAG_ACTIVITY_NEW_TASK
                    }
                    val pendingIntent = PendingIntent.getActivity(
                        applicationContext,
                        0,
                        intent,
                        PendingIntent.FLAG_IMMUTABLE
                    )
                    addAction(
                        R.drawable.ic_open,
                        "打开文件",
                        pendingIntent
                    )
                } else {
                    setProgress(100, progress, false)
                }
            }
            
            // 更新通知
            notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
        }
    }
    
    private val notificationManager: NotificationManager
        get() = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}

3.2 在 ViewModel 中管理工作

class DownloadViewModel(application: Application) : AndroidViewModel(application) {
    
    private val workManager = WorkManager.getInstance(application)
    
    private val _workInfo = MutableLiveData<WorkInfo?>()
    val workInfo: LiveData<WorkInfo?> = _workInfo
    
    private val _progress = MutableLiveData<Int>()
    val progress: LiveData<Int> = _progress
    
    fun startDownload(url: String, filename: String) {
        val inputData = workDataOf(
            DownloadWorker.INPUT_URL to url,
            DownloadWorker.INPUT_FILENAME to filename
        )
        
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
        
        val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
            .setInputData(inputData)
            .setConstraints(constraints)
            .addTag(DownloadWorker.TAG)
            .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
            .build()
        
        // 观察任务状态
        workManager.getWorkInfoByIdLiveData(downloadRequest.id)
            .observeForever { info ->
                _workInfo.value = info
                
                // 获取进度
                info?.progress?.getInt("progress", 0)?.let { progressValue ->
                    _progress.value = progressValue
                }
            }
        
        // 入队任务
        workManager.enqueue(downloadRequest)
    }
    
    fun cancelDownload() {
        workManager.cancelAllWorkByTag(DownloadWorker.TAG)
    }
    
    fun getWorkStatus(): LiveData<List<WorkInfo>> {
        return workManager.getWorkInfosByTagLiveData(DownloadWorker.TAG)
    }
}

3.3 在 Activity/Fragment 中使用

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    private val viewModel: DownloadViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupUI()
        setupObservers()
    }
    
    private fun setupUI() {
        binding.btnDownload.setOnClickListener {
            val url = binding.etUrl.text.toString()
            val filename = binding.etFilename.text.toString()
            
            if (url.isNotEmpty() && filename.isNotEmpty()) {
                viewModel.startDownload(url, filename)
                binding.btnDownload.text = "取消下载"
            } else {
                Toast.makeText(this, "请输入URL和文件名", Toast.LENGTH_SHORT).show()
            }
        }
    }
    
    private fun setupObservers() {
        viewModel.workInfo.observe(this) { info ->
            info?.let {
                when (it.state) {
                    WorkInfo.State.ENQUEUED -> {
                        binding.tvStatus.text = "等待下载..."
                    }
                    WorkInfo.State.RUNNING -> {
                        binding.tvStatus.text = "下载中..."
                        binding.btnDownload.text = "取消下载"
                        binding.btnDownload.setOnClickListener {
                            viewModel.cancelDownload()
                        }
                    }
                    WorkInfo.State.SUCCEEDED -> {
                        val result = it.outputData.getString(DownloadWorker.OUTPUT_RESULT)
                        binding.tvStatus.text = result ?: "下载成功"
                        binding.btnDownload.text = "开始下载"
                        resetDownloadButton()
                    }
                    WorkInfo.State.FAILED -> {
                        val error = it.outputData.getString(DownloadWorker.OUTPUT_RESULT)
                        binding.tvStatus.text = error ?: "下载失败"
                        binding.btnDownload.text = "重试下载"
                        resetDownloadButton()
                    }
                    WorkInfo.State.CANCELLED -> {
                        binding.tvStatus.text = "下载已取消"
                        binding.btnDownload.text = "开始下载"
                        resetDownloadButton()
                    }
                    else -> {}
                }
            }
        }
        
        viewModel.progress.observe(this) { progress ->
            binding.progressBar.progress = progress
            binding.tvProgress.text = "$progress%"
        }
    }
    
    private fun resetDownloadButton() {
        binding.btnDownload.setOnClickListener {
            val url = binding.etUrl.text.toString()
            val filename = binding.etFilename.text.toString()
            
            if (url.isNotEmpty() && filename.isNotEmpty()) {
                viewModel.startDownload(url, filename)
            }
        }
    }
}

四. 最佳实践与常见问题

4.1 最佳实践

  1. 使用 CoroutineWorker:除非必须用 Java 线程,否则优先使用 CoroutineWorker
  2. 合理设置约束:避免不必要的唤醒,节省电量
  3. 处理任务取消:定期检查 isStopped 状态,及时释放资源
  4. 使用唯一工作链:避免重复执行相同任务
  5. 合理设置退避策略:对于可能失败的任务,设置合适的重试策略
  6. 最小化数据传递:输入/输出数据应尽量小,避免传递大对象

4.2 常见问题

Q1: WorkManager 任务没有立即执行?
A: 这是正常行为。WorkManager 不是为即时任务设计的,即使加急任务也可能有延迟。如需立即执行,请使用前台服务。

Q2: 如何传递复杂对象给 Worker?
A: 推荐使用数据库或文件存储,只传递 ID 或路径给 Worker。

Q3: 周期性任务的最小间隔?
A: 15 分钟(PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS

Q4: Worker 中的异常处理?
A: Worker 抛出的未捕获异常会导致任务失败,返回 Result.failure()

Q5: 如何测试 WorkManager?
A: 使用 WorkManagerTestInitHelper 进行测试:

@RunWith(AndroidJUnit4::class)
class DownloadWorkerTest {
    
    @get:Rule
    val rule = InstantTaskExecutorRule()
    
    private lateinit var context: Context
    private lateinit var workManager: WorkManager
    
    @Before
    fun setup() {
        context = ApplicationProvider.getApplicationContext()
        val config = Configuration.Builder()
            .setExecutor(SynchronousExecutor())
            .build()
        
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
        workManager = WorkManager.getInstance(context)
    }
    
    @Test
    fun testDownloadWorker() = runBlocking {
        val inputData = workDataOf("url" to "test_url")
        val request = OneTimeWorkRequestBuilder<DownloadWorker>()
            .setInputData(inputData)
            .build()
        
        workManager.enqueue(request).result.get()
        
        val workInfo = workManager.getWorkInfoById(request.id).get()
        
        assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    }
}

五. 总结

WorkManager 是 Android 后台任务管理的推荐解决方案,特别适合需要保证执行的可延迟任务。关键点:

  1. 选择合适的任务类型:根据需求选择 Immediate/Exact/Expedited/Deferred 任务
  2. 合理使用约束:平衡功能需求和电量消耗
  3. 处理好生命周期:WorkManager 自动处理应用生命周期变化
  4. 支持加急任务:Android 5.0+ 可使用加急任务获得更高优先级
  5. 易于测试:提供专门的测试工具

通过合理使用 WorkManager,可以创建既功能强大又对系统友好的后台任务,提升应用体验和用户满意度。