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

文章来源声明: 原文作者:Android_实战之道; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684659924174798874; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

内容扎实、避坑清单和综合案例是亮点,覆盖 8.0 至 14+ 的适配方案,适合有 Java/Kotlin 基础、初学 Android 后台组件的开发者按图索骥落地实践。

第 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()

<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 仍静态注册这些广播,系统会直接忽略。

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

✅ 解决方案

  1. 改用动态注册:在代码中动态注册,组件存活时生效。
  2. 使用 JobScheduler / WorkManager:把后台任务交给系统统一调度。
  3. 发送显式广播:需要跨进程时,通过 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 运行效果

  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. 前台保活:保证退到后台后服务不被杀死,持续播放。

核心代码结构示例:

<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 开发,不见不散!