跳到主要内容

Service 与后台任务

问题

Service 的类型和生命周期是怎样的?现代 Android 开发中如何处理后台任务?

答案

1. Service 分类

2. Started Service

class UploadService : Service() {

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val filePath = intent?.getStringExtra("file_path")

// 在后台线程处理(Service 默认运行在主线程!)
thread {
uploadFile(filePath)
stopSelf(startId) // 任务完成后停止
}

// 返回值决定系统杀死后的重启行为
return START_NOT_STICKY
}

override fun onBind(intent: Intent?): IBinder? = null

override fun onDestroy() {
super.onDestroy()
// 清理资源
}
}
返回值行为
START_NOT_STICKY被杀后不重启
START_STICKY被杀后重启,Intent 为 null
START_REDELIVER_INTENT被杀后重启,重新传递最后的 Intent

3. Bound Service

class MusicService : Service() {
private val binder = MusicBinder()

inner class MusicBinder : Binder() {
fun getService(): MusicService = this@MusicService
}

override fun onBind(intent: Intent): IBinder = binder

fun play() { /* 播放音乐 */ }
fun pause() { /* 暂停音乐 */ }
fun getCurrentPosition(): Int = 0
}

// Activity 中绑定
class MusicActivity : AppCompatActivity() {
private var musicService: MusicService? = null
private var isBound = false

private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as MusicService.MusicBinder
musicService = binder.getService()
isBound = true
}

override fun onServiceDisconnected(name: ComponentName) {
musicService = null
isBound = false
}
}

override fun onStart() {
super.onStart()
bindService(Intent(this, MusicService::class.java), connection, BIND_AUTO_CREATE)
}

override fun onStop() {
super.onStop()
if (isBound) {
unbindService(connection)
isBound = false
}
}
}

4. 前台 Service

Android 8.0+ 长时间后台任务必须使用前台 Service:

class LocationService : Service() {

override fun onCreate() {
super.onCreate()
// 创建通知渠道(Android 8.0+)
val channel = NotificationChannel(
"location", "位置服务", NotificationManager.IMPORTANCE_LOW
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, "location")
.setContentTitle("正在获取位置")
.setContentText("应用正在后台使用位置信息")
.setSmallIcon(R.drawable.ic_location)
.build()

// Android 14+ 需要指定前台服务类型
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
} else {
startForeground(1, notification)
}

return START_STICKY
}

override fun onBind(intent: Intent?): IBinder? = null
}
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

<service
android:name=".LocationService"
android:foregroundServiceType="location" />
Android 后台限制演进
  • Android 8.0 (O):后台 Service 限制,需使用前台 Service
  • Android 10 (Q):后台定位限制
  • Android 12 (S):不能从后台启动前台 Service(有例外)
  • Android 14 (U):前台 Service 必须声明类型(foregroundServiceType

5. WorkManager(推荐方案)

// 定义 Worker
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
val filePath = inputData.getString("file_path") ?: return Result.failure()

return try {
uploadFile(filePath)
Result.success(workDataOf("url" to uploadedUrl))
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry()
else Result.failure()
}
}
}

// 创建并提交任务
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(workDataOf("file_path" to "/path/to/file"))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()

WorkManager.getInstance(context).enqueueUniqueWork(
"upload",
ExistingWorkPolicy.REPLACE,
uploadWork
)

// 观察结果
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadWork.id)
.observe(this) { workInfo ->
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
val url = workInfo.outputData.getString("url")
}
WorkInfo.State.FAILED -> { /* 处理失败 */ }
WorkInfo.State.RUNNING -> { /* 更新进度 */ }
else -> {}
}
}

6. 后台任务方案选型

场景推荐方案
需要立即执行 + 短任务协程
需要立即执行 + 长任务(用户可感知)前台 Service
可延迟执行 + 需要可靠完成WorkManager
精确定时任务AlarmManager
简单定时重复WorkManager(PeriodicWork)

常见面试问题

Q1: Service 运行在哪个线程?

答案

Service 默认运行在主线程(UI 线程)!在 Service 中执行耗时操作必须手动切到子线程,否则会导致 ANR。

// ❌ 错误:在主线程执行网络请求
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = api.fetchData() // ANR!
return START_NOT_STICKY
}

// ✅ 正确:使用协程
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
CoroutineScope(Dispatchers.IO).launch {
val data = api.fetchData()
stopSelf(startId)
}
return START_NOT_STICKY
}

Q2: bindService 和 startService 可以混合使用吗?

答案

可以。混合使用时 Service 的生命周期较长:

  • startService 启动后,Service 持续运行
  • bindService 绑定后,可以与 Service 交互
  • 必须同时 stopService 所有绑定方都 unbindService 后,Service 才会 onDestroy

典型场景:音乐播放器 — startService 保持播放,bindService 控制播放。

Q3: WorkManager 的底层实现是什么?

答案

WorkManager 根据 API 级别选择不同的底层实现:

  • API 23+:JobScheduler
  • API 14-22:AlarmManager + BroadcastReceiver

WorkManager 使用 Room 数据库 持久化任务信息,确保即使应用进程被杀、设备重启,任务也能恢复执行。

Q4: IntentService 为什么被废弃?替代方案是什么?

答案

IntentService 在 API 30 被废弃,原因:

  1. 受 Android 8.0+ 后台执行限制
  2. 不支持协程
  3. 任务队列管理不灵活

替代方案:

  • 简单任务:协程 + CoroutineScope
  • 需要可靠执行:WorkManager
  • 需要前台执行:前台 Service + 协程

Q5: 如何让 Service 不被系统杀死?

答案

完全不被杀死是不可能的,但可以提高优先级:

  1. 前台 Service(最有效):显示通知,优先级仅次于前台 Activity
  2. START_STICKY:被杀后自动重启
  3. WorkManager:不依赖进程存活,系统保证执行
  4. 避免内存泄漏:减少被系统主动回收的概率
不推荐的做法
  • 1像素 Activity 保活
  • 双进程守护
  • 系统广播拉活

这些黑科技在高版本 Android 上已失效,且可能导致应用被应用商店下架。

相关链接