《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 的消息总线》

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

相关推荐
2501_9159214334 分钟前
iOS 26 CPU 使用率监控策略 多工具协同构建性能探索体系
android·ios·小程序·https·uni-app·iphone·webview
狂团商城小师妹34 分钟前
JAVA国际版同城打车源码同城服务线下结账系统源码适配PAD支持Android+IOS+H5
android·java·ios·小程序·交友
游戏开发爱好者839 分钟前
iOS 应用逆向对抗手段,多工具组合实战(iOS 逆向防护/IPA 混淆/无源码加固/Ipa Guard CLI 实操)
android·ios·小程序·https·uni-app·iphone·webview
虚伪的空想家1 小时前
ip网段扫描机器shell脚本
android·linux·网络协议·tcp/ip·shell·脚本·network
generallizhong1 小时前
android TAB切换
android·gitee
00后程序员张1 小时前
iOS 文件管理与导出实战,多工具协同打造高效数据访问与调试体系
android·macos·ios·小程序·uni-app·cocoa·iphone
Boop_wu1 小时前
[MySQL] JDBC
android
qq_717410013 小时前
FAQ09075:6572平台相机拍照,拍下来的照片无法查看,图库查看时提示“无缩略图”
android
Jerry12 小时前
Compose 的阶段
android
Zhangzy@12 小时前
Rust 编译优化选项
android·开发语言·rust