告别繁琐,使用WorkManager解决后台工作的难题

告别繁琐,使用WorkManager解决后台工作的难题

前言

执行后台任务是Android开发中比较麻烦的部分,需要考虑许多事情。首先是省电续航问题,后台任务频繁执行,电量像流水一样消耗,这是用户和系统都无法忍受的。谷歌为了治理Android应用野蛮地在后台偷偷工作的恶行,推出各种后台限制机制(Doze Mode、App Standby、Background Execution Limits),新版本Android的后台管理越来越严格,给开发者带来一定的兼容性难题。最重要的是开发者难以保证应用的后台任务的可靠性,系统常常因为各种原因暂停或者杀死后台任务。

WorkManager是Android Jetpack 家族中的一位"后台任务管家",是适用于持久性工作和一般后台处理操作的官方推荐 API。本文以开发一个下载器为例子介绍WorkManager的使用方法。下载器的代码可以在下面仓库找到:github.com/Ilovecat194...

欢迎大家关注我的音视频开发项目:github.com/Ilovecat194... 。这是一个开源的Android音视频编辑器,支持ffmpeg命令行、视频编码压缩和格式转换、视频剪裁变速等多种音视频功能。

WorkManager为什么是最佳选择

1.官方背书,可靠性强:作为谷歌官方推荐的后台任务解决方案,WorkManager 会持续得到支持和优化,具有简单且一致的 API 。

2.持久可靠地执行:这是WorkManager最大的亮点。即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。对于失败的任务,WorkManager提供了灵活的重试政策,包括可配置的指数退避政策。

3.高效节能:WorkManager遵循低电耗模式等省电功能和最佳做法,具有强大的调度能力。

4.约束条件灵活:开发者可以为任务设置各种约束条件,让它在最合适的时机执行,从而节省电量和流量。

例如:

网络状态:只在Wi-Fi连接时执行。

充电状态:只在设备充电时执行。

存储空间:只在存储空间足够时执行。

5.任务类型多样:支持一次性任务、周期性任务和链式任务,满足开发者的多种需求。

核心理念与实践:WorkManager是如何工作的?

WorkManager 的设计理念非常清晰,它将后台任务分解为三个核心组件:

1.工作单元:Worker

理念:WorkManager将后台任务抽象成一个个独立的Worker。每个Worker只负责执行一个具体的任务,例如:上传日志、同步数据、下载文件等。

实现:继承Worker类并重写doWork()方法。在这个方法中放入你的后台任务逻辑。

2.任务请求:WorkRequest

理念:WorkRequest是用来描述任务如何执行的,它包含了任务的约束条件、延迟执行时间等信息。

实践:使用OneTimeWorkRequest(一次性任务)或PeriodicWorkRequest(周期性任务)来创建任务请求,并通过Constraints构建器设置约束。

3.任务调度:WorkManager

理念:WorkManager是后台任务的调度中心,它负责接收WorkRequest,并根据约束条件和系统状态来决定何时执行Worker。

实践:通过WorkManager.getInstance(context).enqueue(workRequest)来将任务提交给WorkManager。

用代码说话:使用WorkManager开发一个下载器

设计

我们在开发之前,需要先想好我们开发的应用是怎么样的。

1.使用kotlin语言开发、使用jetpack compose开发UI。

2.能够申请实现下载所需的权限,如网络权限,文件读写权限。

3.界面上支持用户输入下载链接并开启下载。

4.有下载任务列表,展示下载进度和状态。

6.支持下载进度通知。

这里需要强调一下通知的重要性。对于长时间运行且需要持续访问网络或 CPU 的任务,它需要一个"通行证"来告知系统该任务的重要性。为了解决这个问题,我们需要将下载任务升级为 "前台服务(Foreground Service)"。前台服务会向系统声明任务正在进行,并且对用户可见(通过通知),因此系统不会轻易终止它。

代码实现

我们首先需要在AndroidManifest.xml里声明所需要的权限。对于Android13以及后面的Android版本,我们需要显式地声明通知权限。

xml 复制代码
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <!-- 声明网络权限,这是下载功能所必需的 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

我们还需要运行时检查和请求权限,还有创建通知通道。

kotlin 复制代码
    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            if (isGranted) {
                // Permission granted. Continue with the app flow.
            } else {
                Toast.makeText(this, "需要通知权限才能显示下载进度", Toast.LENGTH_LONG).show()
            }
        }
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "下载通知"
            val descriptionText = "显示下载文件的进度和状态"
            val importance = NotificationManagerCompat.IMPORTANCE_LOW
            val channel = android.app.NotificationChannel(DOWNLOAD_NOTIFICATION_CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            val notificationManager: android.app.NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R){
            val permission = arrayOf(
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE
            )
            val requestPermissionLauncher =
                this.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->

                }
            permission.forEach {
                if(! (this.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED)){
                    requestPermissionLauncher.launch(it)
                }
            }
        }
        else{
            if(!Environment.isExternalStorageManager()){
                val builder = android.app.AlertDialog.Builder(this)
                    .setMessage("需要获取文件读写权限")
                    .setPositiveButton("ok") { _, _ ->
                        val packageName = this.packageName
                        val intent = Intent()
                        intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
                        intent.data = Uri.fromParts("package", packageName, null)
                        ContextCompat.startActivity( this, intent, null)
                    }
                    .setNeutralButton("稍后再问"){ _, _ ->

                    }
                builder.show()
            }
        }


        // 请求通知权限 (适用于 Android 13 及以上版本)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }

        // 创建通知渠道
        createNotificationChannel()    
    ...    
    }        

基于自己的需求定义一个工作类,执行任务的具体逻辑都写在这个工作类里面。

kotlin 复制代码
class DownloadWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    private val notificationManager = NotificationManagerCompat.from(appContext)
    private val notificationBuilder = NotificationCompat.Builder(applicationContext, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
        .setContentTitle("下载中")
        .setSmallIcon(android.R.drawable.stat_sys_download)
        .setPriority(NotificationCompat.PRIORITY_LOW)
        .setOngoing(true)

    // 最大文件名长度
    private val MAX_FILENAME_LENGTH = 50

    // 重写 getForegroundInfo() 以提供前台服务通知信息
    override suspend fun getForegroundInfo(): ForegroundInfo {
        return ForegroundInfo(
            DOWNLOAD_NOTIFICATION_ID,
            notificationBuilder.setContentText("开始下载...").build()
        )
    }

    override suspend fun doWork(): Result {
     //文件下载的具体逻辑写在这里面 
     ...
    }

    // Helper function to set progress for the UI
    private fun setProgress(progress: Int) {
        val progressData = workDataOf("progress" to progress)
        setProgressAsync(progressData)
    }
}

我们通过上下文获得一个WorkManager的实例,这个实例可以直接帮我们完成任务数据库和调度系统的全部工作。WorkManager 使用了一个 Room 数据库(一个 SQLite 数据库的抽象层),这个数据库位于应用私有的存储空间中,从而实现任务的持久化和状态管理。我们可以直接从WorkManager的实例中获取所有任务的信息。配合LiveData,应用界面能实时观察并显示任务的状态和进度,实现后台任务与UI的同步。

scss 复制代码
    val workManager = WorkManager.getInstance(context)
    ...
    val workInfos: LiveData<List<WorkInfo>> = remember {
        workManager.getWorkInfosByTagLiveData(DOWNLOAD_WORK_TAG)
    }
    val downloadTasks by workInfos.observeAsState(initial = emptyList())
    ...
    LazyColumn(
        modifier = Modifier.fillMaxSize()
    ) {
        items(downloadTasks) { workInfo ->
            DownloadTaskItem(workInfo)
        }
    }    

定义一个下载启动方法,封装提交任务给WorkManager的流程。

kotlin 复制代码
fun startDownload(context: Context, url: String) {
    // 1. 创建 WorkRequest,指定要执行的 Worker
    val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
        .setInputData(workDataOf(DOWNLOAD_WORK_URL to url)) // 2. 传递输入数据
        .addTag(DOWNLOAD_WORK_TAG) // 3. 添加标签以便跟踪
        .build()

    // 4. 将任务加入队列
    WorkManager.getInstance(context).enqueue(downloadRequest)
}

以上就是使用使用WorkManager开发一个下载器的思路。

展望未来:Android后台任务的新趋势与谷歌政策

谷歌一直在收紧后台任务的权限,以保护用户隐私和设备电量。未来的Android版本可能会有更严格的后台执行限制,这意味着传统的Service和BroadcastReceiver等方式会越来越难用。 在这样的背景下,WorkManager 作为官方推荐的方案,其重要性将日益凸显。它能帮助我们遵循谷歌的政策,同时保证任务的可靠执行。

总结

回顾 WorkManager 的优势:可靠、兼容、持久、灵活。它解放了开发者,让我们可以专注于业务逻辑,而不用再为复杂的后台任务适配和兼容性问题而焦头烂额。

如果你还在为后台任务而烦恼,是时候拥抱 WorkManager 了。它不仅能让你的应用更稳定,也能让你的开发工作更高效。

下载器的代码可以在下面仓库找到:github.com/Ilovecat194...

欢迎大家关注我的音视频开发项目:ilovecat1949.github.io/AudioAndVid... 。这是一个开源的Android音视频编辑器,支持ffmpeg命令行、视频编码压缩和格式转换、视频剪裁变速等多种音视频功能。

参考

1.Android后台工作官方指南

2.WorkManager官方课程

相关推荐
特立独行的猫a5 小时前
开源OpenHarmony润开鸿HH-SCDAYU800A开发板开箱体验
开源·harmonyos·openharmony·hh-scdayu800a
时序数据说6 小时前
时序数据库IoTDB的核心优势
大数据·数据库·物联网·开源·时序数据库·iotdb
三十_A6 小时前
【工具】41K star!网页一键变桌面应用
前端·开源
万岳软件开发小城8 小时前
开源与定制化对比:哪种在线教育系统源码更适合教育培训APP开发?
开源·php·软件开发·在线教育系统源码·教育小程序·教育app开发
专业软件系统开发14 小时前
52DH Pro网址导航系统开源版
开源·导航系统源码
说私域14 小时前
“互联网 +”时代商业生态变革:以开源 AI 智能名片链动 2+1 模式 S2B2C 商城小程序为例
人工智能·小程序·开源
love530love18 小时前
【保姆级教程】阿里 Wan2.1-T2V-14B 模型本地部署全流程:从环境配置到视频生成(附避坑指南)
人工智能·windows·python·开源·大模型·github·音视频
HashData酷克数据21 小时前
官宣:Apache Cloudberry (Incubating) 2.0.0 发布!
数据库·开源·apache·cloudberry
心一信息21 小时前
开源网络流量分析利器:tproxy
开源