《Android 核心组件深度系列 · 第 2 篇 Service》

《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 的第二层生命。

核心要点回顾

  1. Service 默认运行在主线程,耗时任务必须开子线程
  2. 普通服务易被杀,前台服务带通知更安全
  3. 绑定服务可以和 Activity 双向通信
  4. Android 8.0+ 后台启动要用 startForegroundService()
  5. 记得解绑 Service,不然会内存泄漏
  6. 优先考虑 WorkManager,别和系统对着干

十三、互动区

你写过最"难缠"的 Service 吗?

比如后台播放被杀、定位断开、保活不稳?

评论区聊聊你的血泪史 👇


下一篇预告 :《BroadcastReceiver:Android 的消息总线》

关注我,不错过每一篇硬核干货!

相关推荐
前行的小黑炭3 小时前
Compose页面切换的几种方式:Navigation、NavigationBar+HorizontalPager,会导致LaunchedEffect执行?
android·kotlin·app
前行的小黑炭3 小时前
Android :Comnpose各种副作用的使用
android·kotlin·app
BD_Marathon17 小时前
【MySQL】函数
android·数据库·mysql
西西学代码17 小时前
安卓开发---耳机的按键设置的UI实例
android·ui
maki07721 小时前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架1 天前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid1 天前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl1 天前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
旷野说1 天前
Android Studio Narwhal 3 特性
android·ide·android studio