第 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 仍静态注册这些广播,系统会直接忽略。
受影响场景举例:网络状态变化、应用安装/卸载、媒体挂载等。
✅ 解决方案:
-
改用动态注册:在代码中动态注册,组件存活时生效。
-
使用 JobScheduler / WorkManager:把后台任务交给系统统一调度。
-
发送显式广播:需要跨进程时,通过 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 运行效果
- 输入秒数,点击开始,通知栏出现前台服务通知。
- 可切后台,倒计时继续。
- 倒计时结束,弹出“时间到!”的通知。
- 所有组件优雅销毁,无内存泄漏。
4.7 常见报错排查
| 报错现象 | 原因 | 解决 |
|---|---|---|
| 通知不弹出(Android 13+) 未动态申请通知权限 在 onCreate 中调用 requestPermissions | ||
| 前台服务启动失败 缺少 FOREGROUND_SERVICE 权限或 foregroundServiceType 按 4.1 配置 | ||
| 广播接收不到 静态注册被系统限制 使用动态注册,组件存活时有效 | ||
| 界面卡死 Service 内部主线程耗时 检查是否开启了子线程/协程 | ||
| 内存泄漏 广播接收器未注销 onDestroy 中调用 unregisterReceiver |
五、拓展:后台音乐播放器骨架
基于 Service 与广播的联动逻辑,可以快速构建一个音乐播放器骨架:
- MusicService:前台服务,内部用 MediaPlayer 播放音乐。
- Activity 绑定服务:通过 bindService 获取 Binder,控制播放/暂停/切歌。
- 自定义广播:播放状态变化时发送广播,通知 UI 刷新或通知栏更新。
- 前台保活:保证退到后台后服务不被杀死,持续播放。
核心代码结构示例:
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 开发,不见不散!