掌握后台服务与全局广播,实现无界面后台任务与跨组件通信
哈喽,各位坚持学习的小伙伴们!上一篇我们彻底搞定了 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()
<span>// 启动服务</span>
<span>val</span> intent = Intent(<span>this</span>, MyService::<span>class</span>.java)
startService(intent)
<span>// 停止服务(可在 Activity 中调用,也可在 Service 内调用 stopSelf())</span>
stopService(intent)
特点:
无法直接获取 Service 实例,不能调用其内部方法
适合独立后台任务,如音乐播放、倒计时
② bindService(绑定式服务)
通过 bindService() 绑定,组件与 Service 是强关联。绑定者可以拿到 Service 的 Binder 对象,直接调用其内部方法。当所有绑定者解绑后,Service 自动销毁。
生命周期:onCreate() → onBind() → 绑定中 → onUnbind() → onDestroy()
<span>// 绑定服务</span>
<span>val</span> conn = <span>object</span> : ServiceConnection {
<span>override</span> <span><span>fun</span> <span>onServiceConnected</span><span>(name: <span>ComponentName</span>?, binder: <span>IBinder</span>?)</span></span> {
<span>val</span> myBinder = binder <span>as</span> MyService.LocalBinder
<span>val</span> service = myBinder.getService()
<span>// 现在可以调用 service 的公开方法了</span>
}
<span>override</span> <span><span>fun</span> <span>onServiceDisconnected</span><span>(name: <span>ComponentName</span>?)</span></span> {}
}
bindService(intent, conn, Context.BIND_AUTO_CREATE)
<span>// 解绑</span>
unbindService(conn)
选择原则:
- 只需“让它干活,不用随时通信” → startService
- 需要“指挥它干活,并获取实时结果” → bindService
1.3 至关重要的提醒:Service 默认运行在主线程!
这是新手必踩的大坑。Service 的 onCreate()、onStartCommand() 等回调全部运行在 UI 主线程。如果在这里执行耗时操作(如网络请求、大量计算),App 会直接卡顿甚至 ANR。
✅ 解决方案:耗时任务必须手动开启子线程(或使用协程)。
<span>class</span> <span>MyService</span> : <span>Service</span>() {
<span>override</span> <span><span>fun</span> <span>onStartCommand</span><span>(intent: <span>Intent</span>?, flags: <span>Int</span>, startId: <span>Int</span>)</span></span>: <span>Int</span> {
<span>// ❌ 直接在主线程睡10秒——卡死</span>
Thread.sleep(<span>10000</span>)
<span>// ✅ 正确做法:使用协程或线程</span>
thread {
<span>// 模拟后台下载</span>
Log.d(<span>"MyService"</span>, <span>"后台任务执行中..."</span>)
stopSelf()
}
<span>return</span> START_NOT_STICKY
}
}
1.4 前台 Service —— 高版本保活必备
从 Android 8.0 开始,后台 Service 受到严格限制。如果要让 App 切到后台后仍可靠运行,必须将 Service 提升为前台服务,并在通知栏显示一条不可划掉的通知。
<span>class</span> <span>ForegroundService</span> : <span>Service</span>() {
<span>override</span> <span><span>fun</span> <span>onCreate</span><span>()</span></span> {
<span>super</span>.onCreate()
createNotificationChannel()
startForeground(<span>1</span>, buildNotification(<span>"服务运行中"</span>))
}
<span>private</span> <span><span>fun</span> <span>buildNotification</span><span>(text: <span>String</span>)</span></span>: Notification {
<span>return</span> NotificationCompat.Builder(<span>this</span>, CHANNEL_ID)
.setContentTitle(<span>"我的服务"</span>)
.setContentText(text)
.setSmallIcon(R.mipmap.ic_launcher)
.build()
}
<span>// ...</span>
}
注意:Android 14+ 需要在 Manifest 中明确声明前台服务类型 android:foregroundServiceType="specialUse",并申请相应权限。
二、BroadcastReceiver:全局事件的“顺风耳”
BroadcastReceiver 是一种全局监听器,用于接收系统或其他应用发出的广播。它是 Android 跨组件、跨 App 解耦通信的核心桥梁。
2.1 广播分类
- 系统广播:由 Android 系统发送,如电量过低、网络切换、开机完成、时区改变等。
- 自定义广播:开发者自己发送和接收,多用于 App 内部组件通信,比如 Service 完成后通知 Activity 更新 UI。
2.2 两种注册方式
① 静态注册(在 AndroidManifest.xml 中声明)
即使 App 没启动,也能接收到广播(但部分系统广播受限,见 2.3)。
<span><<span>receiver</span> <span>android:name</span>=<span>".MyReceiver"</span>
<span>android:exported</span>=<span>"true"</span>></span>
<span><<span>intent-filter</span>></span>
<span><<span>action</span> <span>android:name</span>=<span>"android.intent.action.BOOT_COMPLETED"</span> /></span>
<span></<span>intent-filter</span>></span>
<span></<span>receiver</span>></span>
② 动态注册(在代码中注册,跟随组件生命周期)
<span>class</span> <span>MainActivity</span> : <span>AppCompatActivity</span>() {
<span>private</span> <span>lateinit</span> <span>var</span> receiver: MyReceiver
<span>override</span> <span><span>fun</span> <span>onCreate</span><span>(savedInstanceState: <span>Bundle</span>?)</span></span> {
<span>super</span>.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
receiver = MyReceiver()
<span>val</span> filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(receiver, filter) <span>// 注册</span>
}
<span>override</span> <span><span>fun</span> <span>onDestroy</span><span>()</span></span> {
<span>super</span>.onDestroy()
unregisterReceiver(receiver) <span>// 必须注销!</span>
}
}
简单选择:只在 App 运行时监听 → 动态注册;需要在 App 未启动时也接收 → 静态注册(注意系统限制)。
2.3 Android 8.0+ 隐式广播限制与解决方案
从 Android 8.0(API 26)开始,系统禁止绝大部分隐式广播的静态注册,旨在减少后台耗电。如果你的 App 仍静态注册这些广播,系统会直接忽略。
受影响场景举例:网络状态变化、应用安装/卸载、媒体挂载等。
✅ 解决方案:
- 改用动态注册:在代码中动态注册,组件存活时生效。
- 使用 JobScheduler / WorkManager:把后台任务交给系统统一调度。
- 发送显式广播:需要跨进程时,通过 setPackage() 指定接收方包名,转为显式广播。
例如,监听网络变化必须使用动态注册:
<span>val</span> filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(networkReceiver, filter)
2.4 发送自定义广播
<span>// 发送普通广播</span>
<span>val</span> intent = Intent(<span>"com.example.MY_CUSTOM_ACTION"</span>)
intent.putExtra(<span>"data"</span>, <span>"Hello"</span>)
sendBroadcast(intent)
<span>// 发送有序广播(接收者可拦截)</span>
sendOrderedBroadcast(intent, <span>null</span>)
应用内部通信,若不需要跨 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)
<span><!-- 前台服务权限(Android 9+ 必需) --></span>
<span><<span>uses-permission</span> <span>android:name</span>=<span>"android.permission.FOREGROUND_SERVICE"</span> /></span>
<span><!-- 通知弹窗权限(Android 13+ 必需) --></span>
<span><<span>uses-permission</span> <span>android:name</span>=<span>"android.permission.POST_NOTIFICATIONS"</span> /></span>
<span><<span>application</span> <span>...</span>></span>
<span><!-- 注册定时服务,注意指定前台服务类型 --></span>
<span><<span>service</span>
<span>android:name</span>=<span>".TimerService"</span>
<span>android:foregroundServiceType</span>=<span>"specialUse"</span>
<span>android:exported</span>=<span>"false"</span> /></span>
<span><!-- 广播接收器静态注册(也可动态,此处演示静态,但注意我们发送的是自定义广播,动态接收更灵活) --></span>
<span><!-- 本例会使用动态注册,所以这里可选 --></span>
<span></<span>application</span>></span>
② 动态权限申请(Android 13+ 通知权限)
在 MainActivity 的 onCreate 中:
<span>if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS), <span>1001</span>
)
}
4.2 定义广播 Action
<span>// 文件:Const.kt</span>
<span>const</span> <span>val</span> ACTION_TIMER_FINISHED = <span>"com.example.timer.TIMER_FINISHED"</span>
4.3 编写 TimerService(前台服务 + 倒计时)
<span>import</span> android.app.Notification
<span>import</span> android.app.NotificationChannel
<span>import</span> android.app.NotificationManager
<span>import</span> android.app.Service
<span>import</span> android.content.Intent
<span>import</span> android.os.Build
<span>import</span> android.os.CountDownTimer
<span>import</span> android.os.IBinder
<span>import</span> androidx.core.app.NotificationCompat
<span>class</span> <span>TimerService</span> : <span>Service</span>() {
<span>companion</span> <span>object</span> {
<span>const</span> <span>val</span> EXTRA_DURATION_SECONDS = <span>"duration_seconds"</span>
<span>private</span> <span>const</span> <span>val</span> CHANNEL_ID = <span>"timer_channel"</span>
}
<span>private</span> <span>lateinit</span> <span>var</span> countDownTimer: CountDownTimer
<span>override</span> <span><span>fun</span> <span>onCreate</span><span>()</span></span> {
<span>super</span>.onCreate()
createNotificationChannel()
}
<span>override</span> <span><span>fun</span> <span>onStartCommand</span><span>(intent: <span>Intent</span>?, flags: <span>Int</span>, startId: <span>Int</span>)</span></span>: <span>Int</span> {
<span>val</span> seconds = intent?.getIntExtra(EXTRA_DURATION_SECONDS, <span>10</span>) ?: <span>10</span>
<span>// 提升为前台服务</span>
startForeground(<span>1</span>, buildNotification(<span>"倒计时 <span>${seconds}</span> 秒"</span>))
countDownTimer = <span>object</span> : CountDownTimer(seconds * <span>1000L</span>, <span>1000</span>) {
<span>override</span> <span><span>fun</span> <span>onTick</span><span>(millisUntilFinished: <span>Long</span>)</span></span> {
<span>// 可更新通知进度,此处略</span>
}
<span>override</span> <span><span>fun</span> <span>onFinish</span><span>()</span></span> {
<span>// 发送自定义广播</span>
<span>val</span> finishIntent = Intent(ACTION_TIMER_FINISHED)
sendBroadcast(finishIntent)
<span>// 停止前台服务并关闭自己</span>
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}.start()
<span>return</span> START_NOT_STICKY
}
<span>override</span> <span><span>fun</span> <span>onBind</span><span>(intent: <span>Intent</span>?)</span></span>: IBinder? = <span>null</span>
<span>override</span> <span><span>fun</span> <span>onDestroy</span><span>()</span></span> {
<span>super</span>.onDestroy()
<span>if</span> (::countDownTimer.isInitialized) {
countDownTimer.cancel()
}
}
<span>private</span> <span><span>fun</span> <span>createNotificationChannel</span><span>()</span></span> {
<span>if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
<span>val</span> channel = NotificationChannel(
CHANNEL_ID,
<span>"定时提醒"</span>,
NotificationManager.IMPORTANCE_LOW
)
<span>val</span> manager = getSystemService(NotificationManager::<span>class</span>.java)
manager.createNotificationChannel(channel)
}
}
<span>private</span> <span><span>fun</span> <span>buildNotification</span><span>(text: <span>String</span>)</span></span>: Notification {
<span>return</span> NotificationCompat.Builder(<span>this</span>, CHANNEL_ID)
.setContentTitle(<span>"定时提醒运行中"</span>)
.setContentText(text)
.setSmallIcon(R.mipmap.ic_launcher)
.build()
}
}
4.4 编写广播接收器 TimerReceiver
<span>import</span> android.app.NotificationChannel
<span>import</span> android.app.NotificationManager
<span>import</span> android.content.BroadcastReceiver
<span>import</span> android.content.Context
<span>import</span> android.content.Intent
<span>import</span> android.os.Build
<span>import</span> androidx.core.app.NotificationCompat
<span>class</span> <span>TimerReceiver</span> : <span>BroadcastReceiver</span>() {
<span>override</span> <span><span>fun</span> <span>onReceive</span><span>(context: <span>Context</span>, intent: <span>Intent</span>)</span></span> {
<span>if</span> (intent.action == ACTION_TIMER_FINISHED) {
showNotification(context)
}
}
<span>private</span> <span><span>fun</span> <span>showNotification</span><span>(context: <span>Context</span>)</span></span> {
<span>val</span> channelId = <span>"remind_channel"</span>
<span>val</span> manager = context.getSystemService(NotificationManager::<span>class</span>.java)
<span>if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
NotificationChannel(channelId, <span>"完成提醒"</span>, NotificationManager.IMPORTANCE_HIGH)
)
}
<span>val</span> notification = NotificationCompat.Builder(context, channelId)
.setContentTitle(<span>"时间到!"</span>)
.setContentText(<span>"您设置的倒计时已结束"</span>)
.setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(<span>true</span>)
.build()
manager.notify(<span>100</span>, notification)
}
}
4.5 主界面 MainActivity
<span>class</span> <span>MainActivity</span> : <span>AppCompatActivity</span>() {
<span>private</span> <span>lateinit</span> <span>var</span> timerReceiver: TimerReceiver
<span>override</span> <span><span>fun</span> <span>onCreate</span><span>(savedInstanceState: <span>Bundle</span>?)</span></span> {
<span>super</span>.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
<span>// 动态申请通知权限(Android 13+)</span>
<span>if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS), <span>1001</span>
)
}
<span>// 动态注册广播接收器</span>
timerReceiver = TimerReceiver()
registerReceiver(timerReceiver, IntentFilter(ACTION_TIMER_FINISHED))
<span>val</span> etSeconds = findViewById<EditText>(R.id.et_seconds)
findViewById<Button>(R.id.btn_start).setOnClickListener {
<span>val</span> seconds = etSeconds.text.toString().toIntOrNull() ?: <span>10</span>
<span>val</span> intent = Intent(<span>this</span>, TimerService::<span>class</span>.java).apply {
putExtra(TimerService.EXTRA_DURATION_SECONDS, seconds)
}
startService(intent)
Toast.makeText(<span>this</span>, <span>"倒计时开始"</span>, Toast.LENGTH_SHORT).show()
}
}
<span>override</span> <span><span>fun</span> <span>onDestroy</span><span>()</span></span> {
<span>super</span>.onDestroy()
unregisterReceiver(timerReceiver) <span>// 必须注销</span>
}
}
4.6 运行效果
- 输入秒数,点击开始,通知栏出现前台服务通知。
- 可切后台,倒计时继续。
- 倒计时结束,弹出“时间到!”的通知。
- 所有组件优雅销毁,无内存泄漏。
4.7 常见报错排查
| 报错现象 | 原因 | 解决 |
|---|---|---|
| 通知不弹出(Android 13+) 未动态申请通知权限 在 onCreate 中调用 requestPermissions | ||
| 前台服务启动失败 缺少 FOREGROUND\_SERVICE 权限或 foregroundServiceType 按 4.1 配置 | ||
| 广播接收不到 静态注册被系统限制 使用动态注册,组件存活时有效 | ||
| 界面卡死 Service 内部主线程耗时 检查是否开启了子线程/协程 | ||
| 内存泄漏 广播接收器未注销 onDestroy 中调用 unregisterReceiver |
五、拓展:后台音乐播放器骨架
基于 Service 与广播的联动逻辑,可以快速构建一个音乐播放器骨架:
- MusicService:前台服务,内部用 MediaPlayer 播放音乐。
- Activity 绑定服务:通过 bindService 获取 Binder,控制播放/暂停/切歌。
- 自定义广播:播放状态变化时发送广播,通知 UI 刷新或通知栏更新。
- 前台保活:保证退到后台后服务不被杀死,持续播放。
核心代码结构示例:
<span>class</span> <span>MusicService</span> : <span>Service</span>() {
<span>// Binder 暴露播放控制方法</span>
<span>inner</span> <span>class</span> <span>MusicBinder</span> : <span>Binder</span>() {
<span><span>fun</span> <span>play</span><span>()</span></span> { ... }
<span><span>fun</span> <span>pause</span><span>()</span></span> { ... }
}
<span>override</span> <span><span>fun</span> <span>onBind</span><span>(intent: <span>Intent</span>?)</span></span> = MusicBinder()
<span>// ...</span>
}
在 Activity 中绑定并控制:
bindService(Intent(<span>this</span>, MusicService::<span>class</span>.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 开发,不见不散!
内容扎实、避坑清单和综合案例是亮点,覆盖 8.0 至 14+ 的适配方案,适合有 Java/Kotlin 基础、初学 Android 后台组件的开发者按图索骥落地实践。