https://developer.android.com/training/best-background
后台处理指南
-
面临的挑战
后台任务消耗设备的有限资源,如RAM和电量;
为了延长电池续航时间,Android系统会在应用不可见时限制其后台工作。
- Android 6.0(API 级别 23)引入了低电耗模式和应用待机模式。低电耗模式会在屏幕处于关闭状态且设备处于静止状态时限制应用行为。应用待机模式会将未使用的应用置于一种特殊状态,进入这种状态后,应用的网络访问、作业和同步会受到限制。
- Android 7.0(API 级别 24)限制了隐式广播,并引入了随时随地使用低电耗模式。
- Android 8.0(API 级别 26)进一步限制了后台行为,例如在后台获取位置信息和释放缓存的唤醒锁定。
- Android 9(API 级别 28)引入了应用待机存储分区,通过它,系统会根据应用使用模式动态确定应用资源请求的优先级。
-
根据工作选择合适的解决方案
- 工作可以延迟,还是需要立即执行
- 工作是否依赖系统条件(如连接到电源、连接到互联网)
- 作业是否需要在确切时间执行
-
WorkManager
适用于可延迟的工作
-
前台服务
适用于立即运行且必须执行完毕的工作
-
AlarmManager
适用于需要在确切时间的工作
-
DownloadManager
适用于长时间的HTTP下载
WorkManager
主要功能
- 最高向后兼容到 API 14
- 在运行 API 23 及以上级别的设备上使用 JobScheduler
- 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager
- 添加网络可用性或充电状态等工作约束
- 调度一次性或周期性异步任务
- 监控和管理计划任务
- 将任务链接起来
- 确保任务执行,即使应用或设备重启也同样执行任务
- 遵循低电耗模式等省电功能
使用入门
自定义Worker实现类,重写doWork方法,返回Result.success() Result.failure() Result.retry()
配置运行任务的方式和时间:OneTimeWorkRequest or PeriodicWorkRequest
最后将任务提交给系统调度
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
// Do the work here--in this case, upload the images.
uploadImages()
// Indicate whether the task finished successfully with the Result
return Result.success()
}
}
val uploadWorkRequest = OneTimeWorkRequestBuilder()
.build()
WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)
方法指南
定义WorkRequest
val constraints = Constraints.Builder()
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.build()
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
val compressionWork = OneTimeWorkRequestBuilder()
.setConstraints(constraints) //工作约束
.setInitialDelay(10, TimeUnit.MINUTES) //初识延迟
.setBackoffCriteria( //重试和退避政策
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.setInputData(imageData) //输入参数 doWork()中通过getInputData()获取参数
.addTag("compression") //添加标记 WorkManager可取消标记任务,也可获取标记任务状态
.build()
工作状态
- BLOCKED
- ENQUEUED
- RUNNING
- SUCCESSDED
- FAILED
- CANCELLED
可以通过Id、标记、工作名称获取WorkInfo
WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayMessage("Work finished!")
}
})
更新、观察进度
class ProgressWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
companion object {
const val Progress = "Progress"
private const val delayDuration = 1L
}
override suspend fun doWork(): Result {
val firstUpdate = workDataOf(Progress to 0)
val lastUpdate = workDataOf(Progress to 100)
setProgress(firstUpdate)
delay(delayDuration)
setProgress(lastUpdate)
return Result.success()
}
}
WorkManager.getInstance(applicationContext)
// requestId is the WorkRequest id
.getWorkInfoByIdLiveData(requestId)
.observe(observer, Observer { workInfo: WorkInfo? ->
if (workInfo != null) {
val progress = workInfo.progress
val value = progress.getInt(Progress, 0)
// Do something with progress information
}
})
链接工作
创建工作链并为其排队
WorkManager.getInstance(myContext)
// Candidates to run in parallel
.beginWith(listOf(filter1, filter2, filter3))
// Dependent work (only runs after all previous work in chain)
.then(compress)
.then(upload)
// Don't forget to enqueue()
.enqueue()
使用InputMerger在链条中传递数据:OverwritingInputMerger & ArrayCreatingInputMerger
val compress: OneTimeWorkRequest = OneTimeWorkRequestBuilder()
.setInputMerger(ArrayCreatingInputMerger::class)
.setConstraints(constraints)
.build()
使用OneTimeWorkRequest注意:
- 从属
OneTimeWorkRequest
仅在其所有父级OneTimeWorkRequest
都成功完成(即返回Result.success()
)时才会被解除阻塞(变为ENQUEUED
状态)。- 如果有任何父级
OneTimeWorkRequest
失败(返回Result.failure()
),则所有从属OneTimeWorkRequest
也会被标记为FAILED
。- 如果有任何父级
OneTimeWorkRequest
被取消,则所有从属OneTimeWorkRequest
也会被标记为CANCELLED
。
取消和停止工作
WorkManager.cancelWorkById(workRequest.id)
重复性工作
PeriodicWorkRequest
//工作器的确切执行时间取决于使用的约束
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
//可定义的最短重复间隔是15分钟(同JobScheduler)
val saveRequest = PeriodicWorkRequestBuilder(1, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(myContext)
.enqueue(saveRequest)
唯一工作
WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)
工作政策包括:REPLACE KEEP APPEND
测试Worker实现
- 测试ListenableWorker
class SleepWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
override suspend fun doWork(): Result {
delay(1000) // milliseconds
return Result.success()
}
}
@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testSleepWorker() {
// Kotlin code can use the TestListenableWorkerBuilder extension to
// build the ListenableWorker
val worker = TestListenableWorkerBuilder(context).build()
runBlocking {
val result = worker.doWork()
assertThat(result, `is`(Result.success()))
}
}
}
- 测试Worker
class SleepWorker(context: Context, parameters: WorkerParameters) :
Worker(context, parameters) {
companion object {
const val SLEEP_DURATION = "SLEEP_DURATION"
}
override fun doWork(): Result {
// Sleep on a background thread.
val sleepDuration = inputData.getLong(SLEEP_DURATION, 1000)
Thread.sleep(sleepDuration)
return Result.success()
}
}
// Kotlin code can use the TestWorkerBuilder extension to
// build the Worker
@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
private lateinit var context: Context
private lateinit var executor: Executor
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
executor = Executors.newSingleThreadExecutor()
}
@Test
fun testSleepWorker() {
val worker = TestWorkerBuilder(
context = context,
executor = executor,
inputData = workDataOf("SLEEP_DURATION" to 10000L)
).build()
val result = worker.doWork()
assertThat(result, `is`(Result.success()))
}
}
调试WorkManager
//移除初始化程序
//加入自定义配置
class MyApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
}
高级概念
线程处理
- Worker 后台线程自动运行
- CoroutineWorker 运行默认的Dispatcher
- RxWorker 针对RxJava使用
- ListenableWorker 以上的基类
class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
//自定义doWork函数运行的协程
override val coroutineContext = Dispatchers.IO
override suspend fun doWork(): Result = coroutineScope {
val jobs = (0 until 100).map {
async {
downloadSynchronously("https://www.google.com")
}
}
// awaitAll will throw an exception if a download fails,
// which CoroutineWorker will treat as a failure
jobs.awaitAll()
Result.success()
}
}
支持长时间的工作器
对于超过10分钟的长时间任务,使用setForeground()
class DownloadWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
override suspend fun doWork(): Result {
val inputUrl = inputData.getString(KEY_INPUT_URL)
?: return Result.failure()
val outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME)
?: return Result.failure()
// Mark the Worker as important
val progress = "Starting Download"
setForeground(createForegroundInfo(progress))
download(inputUrl, outputFile)
return Result.success()
}
private fun download(inputUrl: String, outputFile: String) {
// Downloads a file and updates bytes read
// Calls setForegroundInfo() periodically when it needs to update
// the ongoing Notification
}
// Creates an instance of ForegroundInfo which can be used to update the
// ongoing notification.
private fun createForegroundInfo(progress: String): ForegroundInfo {
val id = applicationContext.getString(R.string.notification_channel_id)
val title = applicationContext.getString(R.string.notification_title)
val cancel = applicationContext.getString(R.string.cancel_download)
// This PendingIntent can be used to cancel the worker
val intent = WorkManager.getInstance(applicationContext)
.createCancelPendingIntent(getId())
// Create a Notification channel if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel()
}
val notification = NotificationCompat.Builder(applicationContext, id)
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
.setSmallIcon(R.drawable.ic_work_notification)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
return ForegroundInfo(notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
// Create a Notification channel
}
companion object {
const val KEY_INPUT_URL = "KEY_INPUT_URL"
const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"
}
}