深入理解广播机制 (BroadcastReceiver)

前言

Android 系统引入了一套强大的广播消息机制。它不仅可用于接收系统级别的消息通知,还可用于应用与系统之间、以及不同应用之间的消息传递。

尽管广播可用于应用内不同组件之间 的通信,但需要通过系统服务进行中转,效率较低。推荐使用 ViewModel 配合 LiveData 或 StateFlow 来完成。

广播机制简介

在 Android 中,每个应用程序都可以注册广播接收器(BroadcastReceiver),从而"订阅"感兴趣的消息。广播可能来自于系统,也可能来自于其他应用,每当"发布"一个广播时,所有订阅了该广播的接收器都会进行响应。

Android 提供了一套完整的 API 来让应用程序发送和接收广播,发送广播其实是借助了 Intent 对象来承载信息,而接收广播则需要我们实现 BroadcastReceiver。

Android 中的广播主要分为两种类型:

  • 标准广播(Normal Broadcasts) :它是一种异步执行的广播,当广播消息发出后,所有匹配的 BroadcastReceiver 都会几乎同时收到这条广播消息,它们之间没有先后顺序可言。这种方式的效率很高,但中途也无法进行截断。

    标准广播的工作流程图:

  • 有序广播(Ordered Broadcasts) :它则是一种同步执行 的广播,在广播发出之后,同一时刻,只会有一个 BroadcastReceiver 能够收到这条广播消息。系统会按照接收器设置的优先级(priority)从高到低依次传递广播。每一个 BroadcastReceiver 接收器都可以选择通过 abortBroadcast() 方法截断当前正在传递的广播,这样其后的 BroadcastReceiver 就无法收到广播消息,并且接收器还能修改广播。

    有序广播的工作流程图:

我们先从接收系统广播开始,看看 BroadcastReceiver 的用法。

接收系统广播

Android 系统在运行时就会发出各种广播,比如开机完成的广播、系统时间改变的广播、电量改变的广播等。我们可以监听这些广播,得到各种系统的状态信息,并且进行逻辑处理。

动态注册:监听时间变化

注册 BroadcastReceiver 的方式有两种,分别是通过代码注册和在 AndroidManifest.xml 清单文件中注册。前者被称为动态注册,后者被称为静态注册。

我们现在通过动态注册的方式来完成对系统时间变化的监听。

首先创建一个名为 BroadcastTest 的 Empty Views Activity 项目。创建一个类 TimeChangeReceiver,继承自 BroadcastReceiver,并重写 onReceive() 方法。这样每当收到广播时,就会执行该方法内部的逻辑。

代码如下:

kotlin 复制代码
class TimeChangeReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // 确认收到的广播 Action 是 Intent.ACTION_TIME_TICK
        if (intent.action == Intent.ACTION_TIME_TICK) {
            Toast.makeText(context, "时间发生了变化", Toast.LENGTH_SHORT).show()
        }
    }
}

在这里,收到了广播消息后,我们仅是弹出了一段文字提示。

然后在 MainActivity 中进行动态注册和注销:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val timeChangeReceiver = TimeChangeReceiver()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val intentFilter = IntentFilter()
        // 添加要监听的广播 Action
        intentFilter.addAction(Intent.ACTION_TIME_TICK)

        // 注册广播接收器
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // 在Android 13 (API 33)以上,必须指定接收器是否对外部应用可见
            registerReceiver(timeChangeReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
        } else {
            registerReceiver(timeChangeReceiver, intentFilter)
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        // 取消注册
        unregisterReceiver(timeChangeReceiver)
    }
}

我们在 onCreate() 方法中,首先创建了 IntentFilter 的实例,并且调用了它的 addAction() 方法指定了我们要监听哪条广播。然后创建了 TimeChangeReceiver 的实例,调用 registerReceiver() 方法对广播接收器进行注册,这样就实现了监听系统时间变化的功能。

最后我们在 onDestroy() 方法中通过调用 unregisterReceiver() 方法取消注册了之前的 TimeChangeReceiver。

注意:动态注册的 BroadcastReceiver 一定要取消注册,否则会造成内存泄漏。

运行程序,最多等待一分钟,你就可以看到弹出的提示。如图所示:

这就是动态注册 BroadcastReceiver 的基本用法,接收其他系统广播的方式是一样的。

完整的广播列表,请查看你本地的 <Android SDK 路径>\platforms\android-<api 版本号>\data\broadcast_actions.txt 文件,如我的是 D:\Develop\SDK\platforms\android-34\data\broadcast_actions.txt。

静态注册:实现开机启动

动态注册虽然可以自由控制 BroadcastReceiver 的注册和注销,但它只能在程序运行后,才能接收广播。有时,你需要在程序未启动的情况下也能接收广播消息,如监听系统启动,这就需要使用静态注册了。

按理来说,任何可以动态注册方式监听到的广播,以静态注册的方式也能监听到,但 Android 8.0 (API 26)以后,为了降低系统功耗、提升性能,所有的隐式广播都不允许使用静态注册的方式来接收了。隐式广播指没有具体指定发送给哪个应用的广播,这样看来,其实大部分的系统广播都属于隐式广播,但有少部分特殊的系统广播仍然允许以静态注册的方式来监听。

详见:隐式广播例外情况

其中 ACTION_BOOT_COMPLETED 是开机完成的广播,我们来完成监听系统开机完成。

我们通过 Android Studio 提供的快捷方式来创建 BroadcastReceiver:右键包名,点击 New->Other->Broadcasr Receiver,从而来到创建窗口。

其中新建类的类名为 BootCompleteReceiver,Exported 选项表示是否允许接收外部程序的广播,Enabled 选项表示是否启用当前的 BroadcastReceiver,这两个选项我们都勾选上。

创建完成后,我们修改 onReceive() 方法,在内部发送一条通知,你可以先不用管发送通知的具体细节。

kotlin 复制代码
class BootCompleteReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // 发送通知
            showBootNotification(context)
        }
    }

    private fun showBootNotification(context: Context) {
        val channelId = "boot_channel"
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 创建通知渠道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Boot Complete Notifications",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }

        // 创建通知
        val notification = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle("应用已准备就绪")
            .setContentText("成功接收到开机广播。")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .build()

        // 发送通知
        notificationManager.notify(1, notification)
    }
}

随后需要在 AndroidManifest.xml 清单文件中注册,不过由于我们是通过 Android Studio 提供的快捷方式创建的 BroadcastReceiver,所以它会帮我们自动注册,如下所示:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        ...>
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">

        </receiver>

        ...
    </application>

</manifest>

不过现在我们还没有"订阅"任何广播消息,我们通过 <intent-filter> 标签,来指定其所接收 action。

xml 复制代码
<receiver
    android:name=".BootCompleteReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

Android 规定:对用户来说敏感的操作,应用需要在清单文件中进行权限声明。就比如监听系统开机的广播,我们就需要声明,如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!--声明监听系统开机广播的权限-->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        ...>
        ...
    </application>

</manifest>

并且从 Android 13 (API 33) 开始,发送通知需要 POST_NOTIFICATIONS 权限,你也需要进行声明:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    
    <!--声明发送通知的权限-->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    ...

</manifest>

最后你需要在虚拟机中启用该应用的通知权限:

重新启动程序,并且重启虚拟机,然后在开机完成的时候你可以看到:

注意:不要在 onReceive() 方法中执行耗时操作,否则会阻塞主线程,导致程序出现无响应错误(ANR)。

那么可以在 onReceive() 方法中开启子线程去执行耗时操作吗?

onReceive() 方法的生命周期特别短,方法返回后,其所在的进程随时可能被回收,导致开启的子线程可能会在任务完成前被终止,所以应该使用 WorkManager 将耗时任务交给系统调度,具体实现方式就不再详细说了。

相关推荐
三少爷的鞋2 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)6 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码8 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程11 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓13 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer