这两年做 Android,只要项目里还有后台任务,前台服务基本都躲不过去。问题是,这个东西现在越来越难用已经成了很多团队的共同感受。不是单纯那种"文档又改了,代码得跟着调一下"的难用,而是你会越来越明显地感觉到,很多以前看着顺手的写法,现在做起来总有点别扭。
一开始大家通常不会把这件事想得太复杂。无非就是起个 service,挂个通知,让系统别太快把任务停掉。可项目真往后做,味道就变了。有人会发现 targetSdk 一升,原来那套逻辑开始出问题;有人会发现服务能起,但总有一层说不出的勉强;也有人会发现自己明明只是想做个同步、做个上传、做个连接过程保活,最后却越来越像在和系统抢执行权。
很多文章喜欢把这个问题总结成"Android 对前台服务越来越严格"。这话没错,但还是太表面。真正的问题,经常不是限制多,而是我们以前太习惯把 FGS 当成一个通用兜底方案了。只要任务不能轻易停、不能随便断,第一反应就是先塞进前台服务。短期这招确实好使,时间一长,系统和你的任务模型就开始不在一个频道上了。
前台服务以前为什么这么讨人喜欢,其实也不难理解。它很直接,工程上很省心。同步怕失败,放进去。上传怕中断,放进去。某个初始化流程不想半路死掉,也放进去。你给用户挂一个常驻通知,系统就知道这件事不是完全偷偷在后台跑,于是进程能多拿到一点生存空间。这套做法在过去相当长一段时间里都很实用,所以很多项目慢慢就形成了条件反射:只要任务有持续执行的需求,就先起个前台服务再说。
问题恰恰也出在这里。前台服务本来更像一种"用户知道它正在进行,而且中断会直接影响当前体验"的机制,但后来在很多项目里,它慢慢变成了一个泛用后台容器。你真正想解决的其实是"任务别停""流程别断""进程别死",可你借用的壳却是"当前有一个用户可感知的持续动作"。这两件事一开始还能凑合重合,系统收得越来越紧之后,差别就越来越明显了。
所以你现在再看 Android 对前台服务的那些要求,会发现它并不只是想让你多写几行 manifest。比如从 Android 14 开始,foreground service type 这件事已经没法继续糊弄过去了。系统希望你从声明开始就把这件事说清楚:你这个服务到底是在做数据同步,还是在处理设备连接,还是在用麦克风、相机、位置这些更敏感的能力。
xml
<service
android:name=".SyncService"
android:exported="false"
android:foregroundServiceType="dataSync" />
很多人看到这里,第一反应是规则更多了。其实再往深一点看,系统是在逼你回答一个以前经常被跳过去的问题:你这个任务,真的像一个前台服务吗?
真实项目里最容易出问题的地方,不一定在启动代码本身,而在任务的描述方式本身就有点心虚。比如我们平时很容易说,"这个同步很重要""这个上传比较长""这个初始化一旦中断会很麻烦""这个连接过程最好一直活着"。这些话从业务角度都没毛病,但它们说的其实都是业务重要性,不是用户感知。系统越来越在意的,偏偏是后者。它更关心的是用户知不知道这件事正在发生,会不会自然预期它别被停掉,以及这个动作是不是明显属于"现在正在进行中"的任务。
这也是为什么有些逻辑虽然技术上能跑进 FGS,但总让人觉得不太顺。因为你想要的是一个"别停"的后台保证,系统给你的却是一种"用户正在看着它发生"的前台身份。模型一旦错开,后面就会越来越靠补丁。
有些任务当然很重要,甚至失败代价也不低,但它们更像"可靠后台执行",不是"持续前台存在"。比如日志上传、缓存修复、补偿同步、慢慢做完也可以的数据整理。这类工作未必轻,甚至也可能耗时不短,可它们和导航、录音、通话、大文件传输这种场景终究不是一回事。后者天然带着强烈的"正在进行中",前者更像"系统找个合适时机把事情做完"。如果硬把它们都塞进前台服务里,短期可能省事,长期往往是在拿 service 的存活来掩盖任务模型本身的不够可恢复。
说白了,很多团队最后总爱"再起一个前台服务试试",不是因为他们真的觉得这件事天生就该前台,而是因为 FGS 太像一个立刻见效的止痛药。任务做不稳,先保活;后台容易死,先挂通知;流程中间断过一次,先延长执行窗口。这个思路一旦养成习惯,FGS 在项目里就很容易失去原本的产品语义,变成一个工程上的兜底手段。系统以前还能睁一只眼闭一只眼,现在它越来越不愿意再替这种用法买单了。
真正比较站得住的前台服务场景,其实都很好辨认。用户知道它正在持续进行,中断会直接影响当前体验,而且通知存在这件事本身也不奇怪。导航时用户知道定位一直在工作,录音时知道麦克风正在占用,通话时整个任务本身就是实时过程,设备交互如果真的是用户明确发起并且正在等待结果,也能说得通。这种场景用 FGS,不太需要额外辩解,连用户自己都能理解通知为什么要一直挂着。
反过来,如果一个任务你得解释半天"虽然用户没盯着看,但它真的很重要,所以应该前台跑",那它大概率已经更像另一类问题了。你真正需要的,不是一个更顽强的 service,而是一个能被调度、能恢复、失败后能继续、进程没了也知道怎么接着跑的后台任务模型。
这也是为什么现在越来越多任务,写成 WorkManager 往往比硬撑一个 service 更顺。不是因为 WorkManager 更时髦,而是它处理的本来就是另一类问题:任务不一定要一直前台可见,但它需要有调度能力、重试语义和更清楚的生命周期。
像日志上传这种事情,就很典型。很多老项目的第一反应都是直接起个前台服务,一直传到结束。后来你会发现,它其实更像"满足条件就执行,失败后可以补一次,必要时短暂提升到前台",而不是"应该维持一个长时间活着的 service"。写成 worker 以后,代码味道就会变很多:
kotlin
class UploadLogsWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
return try {
setForeground(createForegroundInfo())
uploadPendingLogs()
Result.success()
} catch (e: IOException) {
Result.retry()
} catch (e: Exception) {
Result.failure()
}
}
private fun createForegroundInfo(): ForegroundInfo {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_upload)
.setContentTitle("正在上传日志")
.setContentText("请稍候")
.build()
return ForegroundInfo(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
}
}
这里有意思的地方,不是它也能显示通知,而是任务终于被当成任务来建模了。它可以开始,可以失败,可以重试,可以完成,不需要一直靠一个 service 把整件事硬吊着。真正该表达的也不再是"服务先活下来",而是"这项工作应该以什么条件被执行、如何避免重复、失败之后谁来接着做"。
入口处的代码也会更像在提交一项后台工作,而不是先把自己带进 service 思维里:
kotlin
val request = OneTimeWorkRequestBuilder<UploadLogsWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.addTag("upload_logs")
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"upload_logs",
ExistingWorkPolicy.KEEP,
request
)
这时候你在思考的事情是任务是否重复、执行条件是否满足、失败以后是否应该重试,而不是服务还能不能再撑几分钟。这个差别看着不大,实际对代码结构影响很大。
很多前台服务后来为什么会越写越重,原因也挺直接。你会开始不断往里面塞本来不该由 service 承担的状态:当前第几步、下次从哪里继续、用户是不是取消了、失败后要不要自动补一次、进程重启以后怎么恢复现场。等这些东西越来越多时,项目其实已经在偷偷把"任务管理器"建进 service 里了。
类似这样的代码,很多团队应该都不陌生:
kotlin
class SyncService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, createNotification())
lifecycleScope.launch {
val hasPendingSync = repository.hasPendingSync()
val userAllowed = settings.allowBackgroundSync()
val networkOk = networkMonitor.isConnected()
if (!hasPendingSync || !userAllowed || !networkOk) {
stopSelf()
return@launch
}
runCatching {
repository.syncAll()
}.onSuccess {
stopSelf()
}.onFailure {
stopSelf()
}
}
return START_NOT_STICKY
}
}
这段代码本身不算错,问题是它很容易把 service 变成一个"机会来了就顺手做一下后台任务"的入口。看起来是前台服务,实质上却越来越像补丁层。逻辑一多,职责就会越来越暧昧,最后大家对 FGS 的抱怨也会越来越强,因为它承担的早就不是一个清楚的前台动作,而是一整套后台任务治理里最不好讲清楚的那部分。
所以说,前台服务为什么越来越难用了,很多时候不是因为系统突然变坏了,而是因为系统对 FGS 的理解和我们项目里对 FGS 的期待,已经明显不一样了。系统理解的是"用户看得见、正在发生、中断代价高、需要持续存在感"的任务。很多项目期待的则是"别停、别死、最好一口气做完"的后台执行能力。这两者不是一回事。你总拿前一种机制去承接后一种诉求,越往后自然越容易卡。
Android 不是突然讨厌前台服务,它只是越来越不愿意替你兜那些本来就不该靠 FGS 兜的事。真遇到前台服务越来越别扭的项目,往往也不是先去找 manifest 少了哪一行,而是先回头看一句更根上的话:我现在试图保住的,到底是一个用户正在进行中的动作,还是一个本来就该按后台任务方式来设计的工作。
答案如果是后者,那麻烦确实不在限制多。只是以前那些"先起个 FGS 再说"的做法,现在慢慢混不过去了。