深入理解广播机制 (BroadcastReceiver)

前言

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

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

广播机制简介

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

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

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

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

    标准广播的工作流程图:

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

    有序广播的工作流程图:

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

接收系统广播

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

动态注册:监听时间变化

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

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

首先创建一个名为 BroadcastTestEmpty 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 将耗时任务交给系统调度,具体实现方式就不再详细说了。

相关推荐
duwei_wang2 小时前
[Android]-Admob配置过多导致的慢消息
android
雨白4 小时前
发送自定义广播
android
婵鸣空啼8 小时前
GD图像处理与SESSiON
android
sunly_9 小时前
Flutter:导航固定背景图,滚动时导航颜色渐变
android·javascript·flutter
用户20187928316710 小时前
简单了解android.permission.MEDIA_CONTENT_CONTROL权限
android
_一条咸鱼_10 小时前
Android Runtime类卸载条件与资源回收策略(29)
android·面试·android jetpack
顾林海10 小时前
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
android·面试·性能优化
砖厂小工10 小时前
Now In Android 精讲 8 - Gradle build-logic 现代构建逻辑组织方式
android
玲小珑10 小时前
Auto.js 入门指南(八)高级控件与 UI 自动化
android·前端