setExpedited是如何工作的?它是否像前台服务一样好和可靠?

39

背景

过去,我使用一个前台IntentService来处理各种连续发生的事件。然后当Android 11发布时(Android R,API 30),它被弃用,并建议使用Worker代替,Worker使用setForegroundAsync,因此我就这样做了。

val builder = NotificationCompat.Builder(context,...)...
setForegroundAsync(ForegroundInfo(notificationId, builder.build()))

问题

随着 Android 12 的到来 (Android S, API 31),仅仅一个版本后,现在 setForegroundAsync 被标记为已弃用,我被告知要使用 setExpedited 代替:

* @deprecated Use {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)} and
* {@link ListenableWorker#getForegroundInfoAsync()} instead.

问题在于,我不确定它的工作原理。以前,我们有一个通知,应该向用户显示,因为它正在使用前台服务(至少对于“旧”的Android版本)。现在似乎使用getForegroundInfoAsync来处理这个问题,但我不认为它会用于Android 12 :

在Android S之前,WorkManager代表您管理和运行前台服务,以执行WorkRequest,并显示ForegroundInfo中提供的通知。要随后更新此通知,应用程序可以使用NotificationManager。

从Android S及以上版本开始,WorkManager使用即时作业来管理和运行此WorkRequest。

关于这个问题的另一个线索在于(这里):

从WorkManager 2.7.0开始,您的应用程序可以调用setExpedited()来声明Worker应该使用加速作业。当在Android 12上运行时,此新API将使用加速作业;而在早期的Android版本中,该API则使用前台服务以提供向后兼容性。

他们唯一提供的代码片段是调度本身:

OneTimeWorkRequestBuilder<T>().apply {
    setInputData(inputData)
    setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
}.build()

即使是OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST的文档看起来也很奇怪:

当应用程序没有任何加速作业配额时,加速工作请求将回退到常规工作请求。

我想做的就是立即、可靠地执行某些操作,而且不间断(或至少可能性最小),并使用操作系统提供的任何东西来执行它。

我不明白为什么要创建setExpedited

问题

  1. setExpedited的工作原理是什么?
  2. 什么是“加速作业配额”?在Android 12中达到这种情况会发生什么?工作程序不会立即执行吗?
  3. setExpedited与前台服务一样可靠吗?它能够始终立即启动吗?
  4. setExpedited有哪些优点、缺点和限制?为什么应该优先考虑它而不是前台服务?
  5. 这是否意味着当应用程序在Android 12上使用此API时,用户将看不到任何内容?

你找到任何解决方案了吗?我也想在Android 12中做同样的事情。 - Chirag Bhuva
@ChiragBhuva 目前还不知道如何正确使用它。 我甚至不确定我想不想使用它。 - android developer
谢谢。我想知道,你是否有一个具体的答案来回答你的问题?因为我和你一样也有类似的疑问 - https://stackoverflow.com/questions/74177577/how-to-avoid-android-app-foregroundservicestartnotallowedexception-when-using-wo - Cheok Yan Cheng
@CheokYanCheng 我不知道。 - android developer
2个回答

13

从概念上讲,“前台服务”和“加速工作”并不是同一回事。

只有 OneTimeWorkRequest 可以被加速运行,因为这些任务对时间非常敏感。任何 Worker 都可以请求在前台运行。根据应用程序的前台状态,这可能成功。

Worker 可以尝试使用 setForeground[Async]() 在前台运行其工作,从 WorkManager 2.7 开始可以这样使用:

class DownloadWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    override suspend fun getForegroundInfo(): ForegroundInfo {
        TODO()
    }

    override suspend fun doWork(): Result {
        return try {
            setForeground(getForegroundInfo())
            Result.success()
        } catch(e: ForegroundServiceStartNotAllowedException) {
            // TODO handle according to your use case or fail.
            Result.fail()
        }
    }
}


您可以在构建 WorkRequest 时使用 setExpedited 来请求尽快运行。
val request = OneTimeWorkRequestBuilder<SendMessageWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()


WorkManager.getInstance(context)
    .enqueue(request)

在 Android 12 之前的版本中,快速作业将作为前台服务运行,并显示通知。在 Android 12+ 中,可能不会显示通知。

使用前台服务和快速作业的时机图示


这个图表似乎是从 Android 开发者网站获取的,你能提供一下图表的链接吗?谢谢! - BobTheCat
2
这张图是来自于Droidcon London会议上关于WorkManager的演讲。我们很快会将它添加到d.android.com上。 - Ben Weiss
感谢您的出色工作!我刚刚发现您是我在YouTube视频中看到的那位工程师:D 回到正题,在Java中,SDK<Android 12,我可以问:1.当我设置为“setExpedited”时,是否有必要覆盖“getForegroundInfoAsync()”?因为如果我不这样做,它会抛出IllegalStateException。然而从d.android.com上看,他们没有提到这一点,似乎WorkManager会自动处理Android 12以上或以下的情况。2.如果必须覆盖“getForegroundInfoAsync()”,请问正确的方法是什么(用Java)?谢谢! - BobTheCat
1
感谢您的赞美之词。是的,覆盖重写是必要的,因为即使不需要,WorkManager也会为您运行前台服务。我们正在添加一个API来使它更容易,但现在您需要使用“futures”工件。https://maven.google.com/web/m_index.html#androidx.concurrent:concurrent-listenablefuture:1.0.0-beta01 - Ben Weiss
如果我设置了.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST),则会在Android 11上崩溃,但在Android 12或更高版本中运行正常。 - user924
显示剩余2条评论

12

广告1:我无法准确解释这一点(没有查看代码),但从我们(开发人员)的角度来看,它就像是一个工作请求队列,考虑到您的应用程序作业配额。因此,如果您的应用程序在电池方面表现良好(不管这意味着什么...),您可以放心依赖它。

广告2:每个应用程序都会获得一定的配额,不能超过该配额。这个配额的详细信息是(而且可能始终是)OEM内部实现细节。至于达到配额 - 这正是OutOfQuotaPolicy标志的用途。如果您的应用程序达到其配额 - 作业可以作为正常作业运行或被删除。

广告3:那是一个谜。在这里,我们可以找到模糊的声明:

Android 12中新增的Expedited jobs允许应用程序执行短期重要任务,同时使系统更好地控制对资源的访问。这些作业具有介于前台服务和常规JobScheduler作业之间的一组特征

意思是,Work Manager只能用于短时间运行的作业。因此,似乎没有任何官方方式可以从后台启动长时间运行的作业。这是我的看法,但嘿 - 情况就是这样。

我们知道的是,它应该立即启动,但它可能会被推迟。来源

广告4:您不能从后台运行前台服务。因此,如果需要在应用程序不在前台时运行“几分钟任务”,则Expedited job将是在Android 12中执行该操作的唯一方法。想运行更长时间的任务?您需要将其置于前台。这可能需要您运行作业以显示启动活动的通知。然后从活动中,您可以运行前台服务!已经兴奋了吗?

广告5:在Android 12上,他们什么也看不到。在早期版本中,Expedited job将退回到前台服务。您需要覆盖public open suspend fun getForegroundInfo(): ForegroundInfo以进行自定义通知。不过,如果您不覆盖它,我不确定会发生什么。

示例使用(日志上传工作者):

@HiltWorker
class LogReporterWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    private val loggingRepository: LoggingRepository,
) : CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        loggingRepository.uploadLogs()
    }

    override suspend fun getForegroundInfo(): ForegroundInfo {
        SystemNotificationsHandler.registerForegroundServiceChannel(applicationContext)
        val notification = NotificationCompat.Builder(
            applicationContext,
            SystemNotificationsHandler.FOREGROUND_SERVICE_CHANNEL
        )
            .setContentTitle(Res.string(R.string.uploading_logs))
            .setSmallIcon(R.drawable.ic_logo_small)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .build()
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            ForegroundInfo(
                NOTIFICATION_ID,
                notification,
                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
            )
        } else {
            ForegroundInfo(
                NOTIFICATION_ID,
                notification
            )
        }
    }

    companion object {
        private const val TAG = "LogReporterWorker"
        private const val INTERVAL_HOURS = 1L
        private const val NOTIFICATION_ID = 3562

        fun schedule(context: Context) {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.UNMETERED)
                .build()
            val worker = PeriodicWorkRequestBuilder<LogReporterWorker>(
                INTERVAL_HOURS,
                TimeUnit.HOURS
            )
                .setConstraints(constraints)
                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                .addTag(TAG)
                .build()

            WorkManager.getInstance(context)
                .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, worker)
        }
    }
}

13
如果你不覆盖那个函数,我不确定会发生什么。应用程序将崩溃,并显示getForegroundInfo未实现的消息。 - Bogdan Stolyarov
2
@BogdanStolyarov 啊,又一个很棒的文档问题,好好知道一下。 - Jakoss
1
如果您想从后台运行短操作,必须使用它。如果您想运行长时间操作,则必须在打开活动时开始运行前台服务。我会添加示例来回答。 - Jakoss
1
@androiddeveloper 不,这是如何从调度程序/后台运行长时间运行的作业。只要您有一个可见的活动,您仍然可以像以前一样运行前台服务。 - Jakoss
1
@user924 是的,这似乎是合理的,但并没有保证。现在每个后台处理都依赖于我在答案中提到的配额。如果您的应用程序“行为不当”,几秒钟可能太长了。这就是我对WorkManager的整个问题所在。没有保证和数字,只有模糊的“配额”,这基本上取决于制造商。 - Jakoss
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接