上一篇讲了 Systrace/Perfetto 性能分析,这篇切到 Android 后台治理——前台服务(Foreground Service)、后台限制、WorkManager 选型。Android 14/15 在这一块越来越严格,是 App 上线被驳回的高频原因之一。
Android 后台策略演变
前台服务原理
FGS 和普通 Service 的区别:
普通 Service:
用户看不到,资源紧张时被系统杀
前台服务(Foreground Service):
1. 系统显示通知(用户可感知)
2. 优先级 = 重要性高,被杀概率低
3. 但必须声明 type
Android 14 的类型强制声明
Android 14 之前,FGS 没有 type 也能跑。Android 14 开始编译时强制:
<!-- AndroidManifest.xml -->
<service
android:name=".MusicPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
所有 type 对应权限:
混合类型用 |:
android:foregroundServiceType="mediaPlayback|dataSync"
启动 FGS 的代码
// 启动前台服务
val intent = Intent(context, MusicService::class.java).apply {
putExtra("track_id", trackId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
// Service 内部
class MusicService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val trackId = intent?.getStringExtra("track_id")
// Android 14+ 必须立即调用 startForeground(5 秒内)
val notification = createNotification(
title = "正在播放",
text = "歌曲标题",
channelId = "media_playback"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
1,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
} else {
startForeground(1, notification)
}
// 继续工作
// playMusic(trackId)
return START_STICKY
}
}
5 秒不调用 startForeground 直接 ForegroundServiceDidNotStartInTimeException。
Android 12:后台启动限制
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+:从后台启动 FGS 受限制
val canStart = context.packageManager.isForegroundServiceAllowed()
if (!canStart) {
// 后台启动被禁
}
}
只有以下情况能从后台启动 FGS:
App 还在前台 / 刚从前台退到后台
屏幕关 / 但 App 是 active 时
后台白名单(OEM 设置)
用户操作触发(点击通知)
否则系统抛异常。
WorkManager 替代 FGS
WorkManager 是 Google 推荐的"后台工作"框架,特点:
不需要 FGS(不需要通知)
能保证最终执行
受系统约束,重启后继续
// 一次性同步
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.build()
WorkManager.getInstance(context).enqueue(syncRequest)
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
// 同步逻辑
return if (success) Result.success()
else Result.retry()
}
}
常用 task 类型:
Expedited Work(Android 12+)
如果工作量紧急但又不想弹 FGS 通知:
val urgentRequest = OneTimeWorkRequestBuilder<UrgentWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context).enqueue(urgentRequest)
不用 FGS 通知,不受 FGS 限制。但配额有限,多次触发会被系统降级。
不同场景的选型
实战中的常见反模式
1. 用 FGS 保活
// 反模式
class KeepAliveService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(1, Notification())
// 启动一个 alarm,每 1 分钟拉一次自己
Handler(Looper.getMainLooper()).postDelayed({
startService(Intent(this, KeepAliveService::class.java))
}, 60_000)
return START_STICKY
}
}
这种代码以前能跑,Android 12+ 大多数 case 直接 ANR 或异常。
合理替代:
2. 不必要的 FGS
很多场景不需要 FGS:
// 错误:用 FGS 跑下载
class DownloadService : Service() {
override fun onStartCommand(...): Int {
startForeground(1, notification())
downloadFile() // 500MB 文件下载
return START_STICKY
}
}
// 正确:用 WorkManager
class DownloadWorker(...) : CoroutineWorker(...) {
override suspend fun doWork(): Result {
downloadFile()
return Result.success()
}
}
下载只是 WorkManager 任务,不需要在前台跑。
3. 后台场景启动 Service
// 错误:App 在后台,启动 FGS
val intent = Intent(this, MyFGS::class.java)
startForegroundService(intent) // Android 12+ 抛异常
判断自己是否在前台:
if (ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
startForegroundService(intent)
}
或者用 WorkManager 一劳永逸。
Android 13+ 的权限变化
通知权限(Android 13+)
// Android 13+ 必须声明通知权限
val permission = android.Manifest.permission.POST_NOTIFICATIONS
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(permission), 100)
}
FGS 通知显示在状态栏,必须申请通知权限。
健康数据权限(Android 14+)
<uses-permission android:name="android.permission.HEALTH_DATA" /> 是健康数据权限,包括运动、身体数据。需要 USER_FITNESS 申请:
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.HEALTH_DATA" />
实战案例:定位打卡
App 用户打卡有"位置"和"后台"两个要求。怎么做?
class LocationCheckinWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(applicationContext)
val location: Location? = try {
suspendCancellableCoroutine { continuation ->
fusedLocationClient.lastLocation
.addOnSuccessListener { loc ->
continuation.resume(loc)
}
.addOnFailureListener { e ->
continuation.resume(null)
}
}
} catch (e: SecurityException) {
null
}
if (location == null) return Result.retry()
// 验证位置是否在打卡范围内
return if (isInCheckinRange(location)) {
api.checkin(location.latitude, location.longitude)
Result.success()
} else {
Result.failure()
}
}
}
// 调度
val checkinWork = PeriodicWorkRequestBuilder<LocationCheckinWorker>(
30, TimeUnit.MINUTES
)
.setConstraints(
Constraints.Builder()
.setRequiresLocationAccuracy(LocationAccuracy.HIGH)
.build()
)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"location_checkin",
ExistingPeriodicWorkPolicy.KEEP,
checkinWork
)
这套做法:
不弹 FGS 通知:用户不需要被打扰
不依赖前台应用:App 杀掉也会按周期执行
权限要求低:只需要 ACCESS_BACKGROUND_LOCATION
监控 ANR 和 90 死亡
ANR 在 Android 14 提升到 5 秒,背景下 FGS ANR 直接崩溃:
ForegroundServiceDidNotStartInTimeException
避免办法:
不做超过 5 秒的工作在 FGS 里,超过就用 WorkManager
onStartCommand 立即 startForeground,复杂工作异步处理
不要在 FGS onStartCommand 里网络/IO
小结
Android 后台治理经历了 8 年演变,变得非常严格。FGS 类型、通知权限、后台启动限制、ANR 触发,组合起来能把"想保活"的 App 全部干掉。正确思路:用 WorkManager 做不需要实时的工作;用 FGS 仅做必须的用户可见工作(音乐、定位、相机)。不要被旧文档误导,Android 14+ 已经把所有"流氓后台"的路堵了。
下一篇聊聊 WebView 性能优化与 JSBridge,这是 H5 嵌入原生场景的硬技能。