《Android 核心组件深度系列 · 第 2 篇 Service》
继上一篇我们彻底吃透了 Activity,
今天我们来搞懂 Android 的第二大组件 ------ Service。
它不显示界面、不与用户交互,却是许多后台逻辑的灵魂所在。
不论你是写音乐播放器、下载器、IM 聊天,还是前台保活应用,
这篇都值得你收藏。
一、什么是 Service?
一句话定义:
Service 是一个在后台长时间运行的组件,用于执行不需要界面的任务。
简单来说:
- Activity → 用户界面交互
- Service → 后台逻辑执行
举几个典型例子:
- 播放音乐(QQ 音乐、网易云)
- 文件下载(迅雷、百度网盘)
- 定位追踪(高德地图、美团外卖)
- 长连接推送(微信、钉钉)
二、Service 的分类
类型 | 说明 | 示例 |
---|---|---|
普通服务(Started Service) | 调用 startService() 启动,执行完自动结束 |
文件上传、数据同步 |
绑定服务(Bound Service) | 调用 bindService() ,与客户端双向通信 |
音乐播放、蓝牙连接 |
前台服务(Foreground Service) | 系统优先级最高,带通知栏 | 音乐播放、导航定位 |
三、Service 生命周期详解
scss
onCreate() → onStartCommand() → 运行中 → onDestroy()
方法 | 作用 |
---|---|
onCreate() |
初始化服务,只调用一次 |
onStartCommand() |
每次通过 startService() 启动都会调用 |
onDestroy() |
服务销毁时调用 |
四、普通后台服务示例
场景:上传文件到服务器
文件:UploadService.kt
kotlin
class UploadService : Service() {
override fun onCreate() {
super.onCreate()
Log.d("ServiceDemo", "Service 创建了")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("ServiceDemo", "Service 开始工作")
// ⚠️ 注意:Service 默认运行在主线程!
// 耗时操作必须放到子线程,不然会 ANR(应用无响应)
Thread {
for (i in 1..5) {
Log.d("ServiceDemo", "正在上传文件 $i/5")
Thread.sleep(1000)
}
stopSelf() // 任务完成,自己关闭自己
}.start()
// 返回值很重要!决定 Service 被系统杀死后怎么办
return START_STICKY // 被杀后系统会尝试重启,但 intent 可能为空
}
override fun onDestroy() {
super.onDestroy()
Log.d("ServiceDemo", "Service 销毁了")
}
override fun onBind(intent: Intent?): IBinder? {
return null // 普通服务不需要绑定
}
}
onStartCommand 返回值详解
返回值 | 说明 | 适用场景 |
---|---|---|
START_STICKY |
被杀后重启,但 intent 为 null | 音乐播放(不需要重传参数) |
START_NOT_STICKY |
被杀后不重启 | 一次性任务(如数据同步) |
START_REDELIVER_INTENT |
被杀后重启,并重传最后的 intent | 文件下载(需要断点续传) |
MainActivity.kt
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.startServiceBtn).setOnClickListener {
val intent = Intent(this, UploadService::class.java)
startService(intent)
}
findViewById<Button>(R.id.stopServiceBtn).setOnClickListener {
val intent = Intent(this, UploadService::class.java)
stopService(intent)
}
}
}
AndroidManifest.xml
ini
<service android:name=".UploadService" />
五、前台服务:让系统不敢杀你
💡 为什么需要前台服务?
普通 Service 系统随时可能杀掉(省电、省内存)。
前台服务会在通知栏显示一个"小卡片",告诉用户"我正在工作",系统就不敢轻易杀了。
场景:音乐播放器
kotlin
class MusicService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Android 8.0 以上必须先创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"music_channel",
"音乐播放",
NotificationManager.IMPORTANCE_LOW // 低重要性,不会发出声音
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
// 创建通知
val notification = NotificationCompat.Builder(this, "music_channel")
.setContentTitle("正在播放")
.setContentText("周杰伦 - 晴天")
.setSmallIcon(R.drawable.ic_music) // 必须有小图标
.build()
// 把自己变成"前台服务"
startForeground(1, notification)
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
}
📄 别忘了权限!
xml
<!-- Android 9 以上需要 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 以上还要指定类型 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<service
android:name=".MusicService"
android:foregroundServiceType="mediaPlayback" />
!!!!!注意!!! Android 12+ 的新限制
从 Android 12 开始,前台服务必须指定类型:
类型 | 说明 | 对应权限 |
---|---|---|
mediaPlayback |
音乐、视频播放 | FOREGROUND_SERVICE_MEDIA_PLAYBACK |
location |
定位、导航 | FOREGROUND_SERVICE_LOCATION |
dataSync |
数据同步 | FOREGROUND_SERVICE_DATA_SYNC |
六、绑定服务:Activity 和 Service 聊天
场景:控制音乐播放
有时候我们需要 Activity 和 Service "对话":
- Activity:下一首!
- Service:好的,正在切歌~
这就是「绑定服务」。
MusicService.kt
kotlin
class MusicService : Service() {
private val binder = LocalBinder()
private var currentSong = "晴天"
// 这是一个"遥控器",Activity 通过它控制 Service
inner class LocalBinder : Binder() {
fun getService(): MusicService = this@MusicService
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
// 提供给 Activity 调用的方法
fun playMusic() {
Log.d("MusicService", "开始播放: $currentSong")
}
fun nextSong() {
currentSong = "稻香"
Log.d("MusicService", "切换到: $currentSong")
}
fun getCurrentSong(): String = currentSong
}
MainActivity.kt
kotlin
class MainActivity : AppCompatActivity() {
private var musicService: MusicService? = null
private var isBound = false
// 连接监听器
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
// 连接成功,拿到"遥控器"
musicService = (binder as MusicService.LocalBinder).getService()
isBound = true
Log.d("MainActivity", "已连接到 Service")
}
override fun onServiceDisconnected(name: ComponentName?) {
musicService = null
isBound = false
Log.d("MainActivity", "与 Service 断开连接")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 绑定服务
findViewById<Button>(R.id.bindBtn).setOnClickListener {
val intent = Intent(this, MusicService::class.java)
bindService(intent, connection, BIND_AUTO_CREATE)
}
// 控制播放
findViewById<Button>(R.id.playBtn).setOnClickListener {
musicService?.playMusic()
}
findViewById<Button>(R.id.nextBtn).setOnClickListener {
musicService?.nextSong()
}
}
override fun onDestroy() {
super.onDestroy()
// ⚠️ 非常重要!不解绑会内存泄漏
if (isBound) {
unbindService(connection)
isBound = false
}
}
}
七、跨进程通信:两个 App 之间怎么聊?
场景:调用第三方 App 的服务
有时候你的 App 需要和别的 App 的 Service 通信,比如:
- 调用支付宝的支付服务
- 调用地图 App 的定位服务
这就需要「跨进程通信」,用 AIDL(听起来很吓人,其实就是定义接口的工具)。
简单理解
- 同一个 App 内:用 Binder(上面的例子)
- 不同 App 之间:用 AIDL(高级版 Binder)
因为篇幅限制,这里先埋个坑,以后专门写一篇 AIDL 的文章 😄
八、Service vs IntentService vs WorkManager
🤔 我到底该用哪个?
组件 | 特点 | 适用场景 | 现状 |
---|---|---|---|
Service | 需要手动开子线程 | 需要长期运行的任务 | 仍在用 |
IntentService | 自动开子线程,任务完成自动关闭 | 一次性后台任务 | 已废弃 |
WorkManager | 谷歌推荐,支持定时、重试、链式任务 | 延迟任务、定期任务 | 首选 |
💡 建议
- 需要实时运行(如音乐播放)→ 用 Service
- 定时任务(如每天凌晨同步数据)→ 用 WorkManager
- 下载文件 → 用 WorkManager 或 DownloadManager
九、生命周期差异对比
特性 | 普通服务 | 前台服务 | 绑定服务 |
---|---|---|---|
是否长期运行 | ✅ | ✅ | ❌(依附于调用者) |
是否能交互 | ❌ | ❌ | ✅ |
系统回收概率 | 高 | 低 | 中等 |
是否显示通知 | ❌ | ✅ | ❌ |
适用场景 | 后台任务 | 音乐、定位 | 控制与通信 |
十、常见坑点与避坑技巧
1️⃣ Android 8.0+ 后台启动限制
问题:在后台启动 Service 会报错
vbnet
IllegalStateException: Not allowed to start service Intent
解决:
scss
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent) // 用这个!
} else {
startService(intent)
}
记得在 onStartCommand()
里 5 秒内调用 startForeground()
,不然会崩溃。
2️⃣ 忘记解绑导致内存泄漏
问题:Activity 销毁了,但 Service 还持有它的引用
解决:
kotlin
override fun onDestroy() {
super.onDestroy()
if (isBound) {
unbindService(connection)
isBound = false
}
}
3️⃣ Service 里做耗时操作导致 ANR
问题:Service 默认跑在主线程!
错误示范:
kotlin
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// ❌ 主线程里下载文件,会卡死
downloadFile("https://example.com/big_file.zip")
return START_STICKY
}
正确做法:
kotlin
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// ✅ 开子线程
Thread {
downloadFile("https://example.com/big_file.zip")
stopSelf()
}.start()
return START_STICKY
}
更优雅的做法(用协程):
kotlin
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceScope.launch {
downloadFile("https://example.com/big_file.zip")
stopSelf()
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel() // 取消所有协程
}
4️⃣ 电池优化导致 Service 被杀
问题:小米、华为等手机会主动杀后台
解决:
- 引导用户把 App 加入"白名单"
- 使用前台服务(被杀概率低很多)
- 用 WorkManager 替代(系统级保障)
5️⃣ 前台服务没通知渠道导致崩溃
问题:Android 8.0+ 必须创建 NotificationChannel
解决:参考上面"前台服务"部分的代码
十一、进程保活的残酷真相
很多人问:"怎么让 Service 永远不被杀?"
坦白讲:在现代 Android 系统(8.0+)几乎不可能。
曾经的骚操作(已失效):
- ❌ 双进程守护
- ❌ 1 像素 Activity 保活
- ❌ 监听系统广播拉活
现在唯一靠谱的:
- ✅ 前台服务(带通知栏)
- ✅ WorkManager(系统级保障)
- ✅ 引导用户加白名单
记住:谷歌就是要杀后台,别和系统对着干 😅
十二、总结一句话
Service 是 Android 后台的灵魂。
它让你的 App 在用户"看不见"的时候继续活着。
学会用好 Service,你就掌握了 Android 的第二层生命。
核心要点回顾:
- Service 默认运行在主线程,耗时任务必须开子线程
- 普通服务易被杀,前台服务带通知更安全
- 绑定服务可以和 Activity 双向通信
- Android 8.0+ 后台启动要用
startForegroundService()
- 记得解绑 Service,不然会内存泄漏
- 优先考虑 WorkManager,别和系统对着干
十三、互动区
你写过最"难缠"的 Service 吗?
比如后台播放被杀、定位断开、保活不稳?
评论区聊聊你的血泪史 👇
下一篇预告 :《BroadcastReceiver:Android 的消息总线》
关注我,不错过每一篇硬核干货!