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 被废弃,原因:
- 受 Android 8.0+ 后台执行限制
- 不支持协程
- 任务队列管理不灵活
替代方案:
- 简单任务:协程 +
CoroutineScope - 需要可靠执行:WorkManager
- 需要前台执行:前台 Service + 协程
Q5: 如何让 Service 不被系统杀死?
答案:
完全不被杀死是不可能的,但可以提高优先级:
- 前台 Service(最有效):显示通知,优先级仅次于前台 Activity
START_STICKY:被杀后自动重启- WorkManager:不依赖进程存活,系统保证执行
- 避免内存泄漏:减少被系统主动回收的概率
不推荐的做法
- 1像素 Activity 保活
- 双进程守护
- 系统广播拉活
这些黑科技在高版本 Android 上已失效,且可能导致应用被应用商店下架。