第 5 篇|Service 与 BroadcastReceiver 实战 —— 后台任务与全局广播

15 阅读10分钟

第 5 篇|Service 与 BroadcastReceiver 实战 —— 后台任务与全局广播

掌握后台服务与全局广播,实现无界面后台任务与跨组件通信


哈喽,各位坚持学习的小伙伴们!上一篇我们彻底搞定了 Activity 的生命周期与页面跳转,现在你已经有能力写出多页面的 App 了。但一个完整的应用,光有看得见的界面还不够 —— 很多场景需要在后台悄悄干活,比如播放音乐、下载文件、定时提醒、监听网络变化等。

今天,我们将解锁 Android 四大组件的另外两个:Service(服务) 和 BroadcastReceiver(广播接收器)。学会了它们,你的 App 就拥有了「后台执行」与「全局监听」的能力,真正像一个专业的 Android 应用了。而且这两个组件经常联动,堪称后台开发的“黄金搭档”。

全程附可运行代码 + 避坑指南 + 高版本适配方案,我们直接开始!


一、Service:后台无界面的“隐形工作者”

Service 是 Android 四大组件之一,无界面、专注后台任务运行,生命周期独立于 Activity,App 退到后台甚至页面销毁,Service 依然可以持续运行。

1.1 Service 典型应用场景

  • 后台持续任务:音乐播放、视频缓存、文件下载
  • 定时任务:倒计时提醒、定时上传日志、数据同步
  • 跨组件数据支撑:为 Activity、广播提供后台计算或网络结果

1.2 两种启动模式:startService 与 bindService

Service 有两种用法,它们的生命周期和适用场景截然不同。

① startService(启动式服务)

通过 startService() 启动,组件与 Service 是弱关联。即便启动者(Activity)销毁了,Service 仍可独立运行。

生命周期:onCreate() → onStartCommand() → 运行中 → onDestroy()

// 启动服务
val intent = Intent(this, MyService::class.java)
startService(intent)

// 停止服务(可在 Activity 中调用,也可在 Service 内调用 stopSelf())
stopService(intent)

特点:

无法直接获取 Service 实例,不能调用其内部方法

适合独立后台任务,如音乐播放、倒计时

② bindService(绑定式服务)

通过 bindService() 绑定,组件与 Service 是强关联。绑定者可以拿到 Service 的 Binder 对象,直接调用其内部方法。当所有绑定者解绑后,Service 自动销毁。

生命周期:onCreate() → onBind() → 绑定中 → onUnbind() → onDestroy()

// 绑定服务
val conn = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        val myBinder = binder as MyService.LocalBinder
        val service = myBinder.getService()
        // 现在可以调用 service 的公开方法了
    }
    override fun onServiceDisconnected(name: ComponentName?) {}
}
bindService(intent, conn, Context.BIND_AUTO_CREATE)

// 解绑
unbindService(conn)

选择原则:

  • 只需“让它干活,不用随时通信” → startService
  • 需要“指挥它干活,并获取实时结果” → bindService

1.3 至关重要的提醒:Service 默认运行在主线程!

这是新手必踩的大坑。Service 的 onCreate()、onStartCommand() 等回调全部运行在 UI 主线程。如果在这里执行耗时操作(如网络请求、大量计算),App 会直接卡顿甚至 ANR。

✅ 解决方案:耗时任务必须手动开启子线程(或使用协程)。

class MyService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // ❌ 直接在主线程睡10秒——卡死
        Thread.sleep(10000)

        // ✅ 正确做法:使用协程或线程
        thread {
            // 模拟后台下载
            Log.d("MyService", "后台任务执行中...")
            stopSelf()
        }
        return START_NOT_STICKY
    }
}

1.4 前台 Service —— 高版本保活必备

从 Android 8.0 开始,后台 Service 受到严格限制。如果要让 App 切到后台后仍可靠运行,必须将 Service 提升为前台服务,并在通知栏显示一条不可划掉的通知。

class ForegroundService : Service() {
    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
        startForeground(1, buildNotification("服务运行中"))
    }

    private fun buildNotification(text: String): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("我的服务")
            .setContentText(text)
            .setSmallIcon(R.mipmap.ic_launcher)
            .build()
    }
    // ...
}

注意:Android 14+ 需要在 Manifest 中明确声明前台服务类型 android:foregroundServiceType="specialUse",并申请相应权限。


二、BroadcastReceiver:全局事件的“顺风耳”

BroadcastReceiver 是一种全局监听器,用于接收系统或其他应用发出的广播。它是 Android 跨组件、跨 App 解耦通信的核心桥梁。

2.1 广播分类

  • 系统广播:由 Android 系统发送,如电量过低、网络切换、开机完成、时区改变等。
  • 自定义广播:开发者自己发送和接收,多用于 App 内部组件通信,比如 Service 完成后通知 Activity 更新 UI。

2.2 两种注册方式

① 静态注册(在 AndroidManifest.xml 中声明)

即使 App 没启动,也能接收到广播(但部分系统广播受限,见 2.3)。

<receiver android:name=".MyReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

② 动态注册(在代码中注册,跟随组件生命周期)

class MainActivity : AppCompatActivity() {
    private lateinit var receiver: MyReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        receiver = MyReceiver()
        val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
        registerReceiver(receiver, filter)  // 注册
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(receiver)  // 必须注销!
    }
}

简单选择:只在 App 运行时监听 → 动态注册;需要在 App 未启动时也接收 → 静态注册(注意系统限制)。

2.3 Android 8.0+ 隐式广播限制与解决方案

从 Android 8.0(API 26)开始,系统禁止绝大部分隐式广播的静态注册,旨在减少后台耗电。如果你的 App 仍静态注册这些广播,系统会直接忽略。

受影响场景举例:网络状态变化、应用安装/卸载、媒体挂载等。

✅ 解决方案:

  1. 改用动态注册:在代码中动态注册,组件存活时生效。

  2. 使用 JobScheduler / WorkManager:把后台任务交给系统统一调度。

  3. 发送显式广播:需要跨进程时,通过 setPackage() 指定接收方包名,转为显式广播。

例如,监听网络变化必须使用动态注册:

val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(networkReceiver, filter)

2.4 发送自定义广播

// 发送普通广播
val intent = Intent("com.example.MY_CUSTOM_ACTION")
intent.putExtra("data", "Hello")
sendBroadcast(intent)

// 发送有序广播(接收者可拦截)
sendOrderedBroadcast(intent, null)

应用内部通信,若不需要跨 App,可优先考虑 LiveData、Flow 或 EventBus 等更现代的方式。但对于系统级广播或跨 App 唤醒,标准 BroadcastReceiver 依旧不可替代。


三、新手必踩坑点清单(四大核心坑点,提前规避)

坑点现象/后果正确做法
1. Service 未在 Manifest 注册启动崩溃:Unable to start service ... not found必须在 AndroidManifest.xml 中声明
2. 动态广播未注销内存泄漏、Activity 销毁后收到广播崩溃onDestroy() 中调用 unregisterReceiver(),成对出现
3. Service 主线程耗时UI 卡顿、ANR 弹框耗时任务手动开协程/线程处理
4. 8.0+ 静态隐式广播失效广播接收不到,功能无响应改为动态注册,或使用显式广播、WorkManager 等替代方案

四、综合小案例:定时提醒小助手

融合今天的所有知识点,我们实现一个 “前台 Service 倒计时 → 发送自定义广播 → 弹出通知提醒” 的完整链路。覆盖 Service、BroadcastReceiver、通知栏、权限适配,新手直接套用即可。

4.1 前置环境配置(零报错关键)

① 权限声明(AndroidManifest.xml)

<!-- 前台服务权限(Android 9+ 必需) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 通知弹窗权限(Android 13+ 必需) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application ...>
    <!-- 注册定时服务,注意指定前台服务类型 -->
    <service
        android:name=".TimerService"
        android:foregroundServiceType="specialUse"
        android:exported="false" />
    <!-- 广播接收器静态注册(也可动态,此处演示静态,但注意我们发送的是自定义广播,动态接收更灵活) -->
    <!-- 本例会使用动态注册,所以这里可选 -->
</application>

② 动态权限申请(Android 13+ 通知权限)

在 MainActivity 的 onCreate 中:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    requestPermissions(
        arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1001
    )
}

4.2 定义广播 Action

// 文件:Const.kt
const val ACTION_TIMER_FINISHED = "com.example.timer.TIMER_FINISHED"

4.3 编写 TimerService(前台服务 + 倒计时)

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.CountDownTimer
import android.os.IBinder
import androidx.core.app.NotificationCompat

class TimerService : Service() {

    companion object {
        const val EXTRA_DURATION_SECONDS = "duration_seconds"
        private const val CHANNEL_ID = "timer_channel"
    }

    private lateinit var countDownTimer: CountDownTimer

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val seconds = intent?.getIntExtra(EXTRA_DURATION_SECONDS, 10) ?: 10
        // 提升为前台服务
        startForeground(1, buildNotification("倒计时 ${seconds} 秒"))

        countDownTimer = object : CountDownTimer(seconds * 1000L, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                // 可更新通知进度,此处略
            }
            override fun onFinish() {
                // 发送自定义广播
                val finishIntent = Intent(ACTION_TIMER_FINISHED)
                sendBroadcast(finishIntent)
                // 停止前台服务并关闭自己
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf()
            }
        }.start()

        return START_NOT_STICKY
    }

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

    override fun onDestroy() {
        super.onDestroy()
        if (::countDownTimer.isInitialized) {
            countDownTimer.cancel()
        }
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "定时提醒",
                NotificationManager.IMPORTANCE_LOW
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }

    private fun buildNotification(text: String): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("定时提醒运行中")
            .setContentText(text)
            .setSmallIcon(R.mipmap.ic_launcher)
            .build()
    }
}

4.4 编写广播接收器 TimerReceiver

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat

class TimerReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == ACTION_TIMER_FINISHED) {
            showNotification(context)
        }
    }

    private fun showNotification(context: Context) {
        val channelId = "remind_channel"
        val manager = context.getSystemService(NotificationManager::class.java)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            manager.createNotificationChannel(
                NotificationChannel(channelId, "完成提醒", NotificationManager.IMPORTANCE_HIGH)
            )
        }
        val notification = NotificationCompat.Builder(context, channelId)
            .setContentTitle("时间到!")
            .setContentText("您设置的倒计时已结束")
            .setSmallIcon(R.mipmap.ic_launcher)
            .setAutoCancel(true)
            .build()
        manager.notify(100, notification)
    }
}

4.5 主界面 MainActivity

class MainActivity : AppCompatActivity() {

    private lateinit var timerReceiver: TimerReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 动态申请通知权限(Android 13+)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            requestPermissions(
                arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1001
            )
        }

        // 动态注册广播接收器
        timerReceiver = TimerReceiver()
        registerReceiver(timerReceiver, IntentFilter(ACTION_TIMER_FINISHED))

        val etSeconds = findViewById<EditText>(R.id.et_seconds)
        findViewById<Button>(R.id.btn_start).setOnClickListener {
            val seconds = etSeconds.text.toString().toIntOrNull() ?: 10
            val intent = Intent(this, TimerService::class.java).apply {
                putExtra(TimerService.EXTRA_DURATION_SECONDS, seconds)
            }
            startService(intent)
            Toast.makeText(this, "倒计时开始", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(timerReceiver)  // 必须注销
    }
}

4.6 运行效果

  1. 输入秒数,点击开始,通知栏出现前台服务通知。
  2. 可切后台,倒计时继续。
  3. 倒计时结束,弹出“时间到!”的通知。
  4. 所有组件优雅销毁,无内存泄漏。

4.7 常见报错排查

报错现象原因解决
通知不弹出(Android 13+) 未动态申请通知权限 在 onCreate 中调用 requestPermissions
前台服务启动失败 缺少 FOREGROUND_SERVICE 权限或 foregroundServiceType 按 4.1 配置
广播接收不到 静态注册被系统限制 使用动态注册,组件存活时有效
界面卡死 Service 内部主线程耗时 检查是否开启了子线程/协程
内存泄漏 广播接收器未注销 onDestroy 中调用 unregisterReceiver

五、拓展:后台音乐播放器骨架

基于 Service 与广播的联动逻辑,可以快速构建一个音乐播放器骨架:

  1. MusicService:前台服务,内部用 MediaPlayer 播放音乐。
  2. Activity 绑定服务:通过 bindService 获取 Binder,控制播放/暂停/切歌。
  3. 自定义广播:播放状态变化时发送广播,通知 UI 刷新或通知栏更新。
  4. 前台保活:保证退到后台后服务不被杀死,持续播放。

核心代码结构示例:

class MusicService : Service() {
    // Binder 暴露播放控制方法
    inner class MusicBinder : Binder() {
        fun play() { ... }
        fun pause() { ... }
    }

    override fun onBind(intent: Intent?) = MusicBinder()
    // ...
}

在 Activity 中绑定并控制:

bindService(Intent(this, MusicService::class.java), connection, Context.BIND_AUTO_CREATE)

六、总结与下篇预告

今天我们解锁了 Android 后台开发的两大神器,核心要点回顾:

  • ✅ Service:后台无界面组件,分 startService 和 bindService 两种模式,默认运行在主线程,耗时任务须手动开线程。

  • ✅ BroadcastReceiver:全局监听器,支持静态/动态注册,8.0+ 禁止静态隐式广播,动态注册必须成对注销。

  • ✅ 避坑清单:注册/注销、主线程耗时、8.0 限制、前台服务权限。

  • ✅ 实战案例:定时提醒小助手,覆盖前台 Service + 自定义广播 + 通知 + 权限适配,可直接复用。

  • ✅ 拓展思路:音乐播放器骨架,展示 bindService 与广播联动的经典架构。

掌握了 Service 和 BroadcastReceiver,你的 App 就拥有了“后台执行”与“全局感知”的能力。接下来,我们将学习 Fragment 碎片化 UI 开发,让一个 Activity 能承载多块界面,实现灵活、高效的页面架构。


✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路! 下篇学习 Fragment 碎片化 UI 开发,不见不散!