6.1 广播机制简介
为了便于进行系统级别的消息通知,Android引入了一套广播消息机制。 每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。Android提供了一套完整的API,允许应用程序自由地发送和接收广播。    
Android中的广播主要可以分为两种类型:标准广播和有序广播。
标准广播
标准广播(normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的BroadcastReceiver几乎都会在同一时刻接收到收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。示意图如下:
也就说广播会同时发送给接收器。
有序广播
有序广播(ordered broadcasts)是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的,优先级高的BroadcastReceiver就可以先收到广播消息,并且前面的BroadcastReceiver还可以截断正在传递的广播,这样后面的BroadcastReceiver就无法收到广播消息了。示意图如下:
也就是这个广播不能同时传递给接收器。
6.2 接收系统广播
Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如:
- 手机开机完成后会发出一条广播
 - 电池的电量发生变化会发出一条广播
 - 系统时间发生改变也会发出一条广播,等等。
 
如果想要接收这些广播,就需要使用BroadcastReceiver。
6.2.1 动态注册监听时间变化
注册BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册。
下面通过实例来讲解。
动态注册
新建一个BroadcastTest项目,然后修改MainActivity中的代码,如下所示:
package com.example.broadcasttestimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.content.IntentFilterimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.widget.Toastclass MainActivity : AppCompatActivity() {lateinit var timeChangeReceiver: TimeChangeReceiveroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val intentFilter = IntentFilter()intentFilter.addAction("android.intent.action.TIME_TICK")timeChangeReceiver = TimeChangeReceiver()registerReceiver(timeChangeReceiver, intentFilter)}override fun onDestroy() {super.onDestroy()unregisterReceiver(timeChangeReceiver)}inner class TimeChangeReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {Toast.makeText(context, "Time has changed", Toast.LENGTH_LONG).show()}}}
这里有个新的关键词lateinit意思是延迟初始化,关于延迟初始化是什么以及为什么要延迟初始化,找到了一篇文章讲的很好,直接看链接即可,我就不讲了:
Kotlin基础之lateinit关键字(五)
    那么记下来讲重点,首先是一个内部类:
inner class TimeChangeReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {Toast.makeText(context, "Time has changed", Toast.LENGTH_LONG).show()}}
这个内部类的名字:`TimeChangeReceiver`是我们**自己定义**的,这个就是我们需要注册的**广播**,它继承于`BroadcastReceiver`,它需要重写一个接收方法,在这个方法里面执行我们广播需要执行的内容。<br />接下来看`onCreate`方法,重点是下面四个语句:
val intentFilter = IntentFilter()intentFilter.addAction("android.intent.action.TIME_TICK")timeChangeReceiver = TimeChangeReceiver()registerReceiver(timeChangeReceiver, intentFilter)
首先定义了一个**意图过滤器**`intentFilter`,给这个过滤设置一个**动作**`action`,括号中就是动作的详细名称,这里的意思是**时间变换**作为一个动作。<br />第三行把`timeChangeReceiver`实例化了,实例化的内容就是上面定义的内部类的内容,最后一步很重要,就是**注册广播**:`registerReceiver(timeChangeReceiver, intentFilter)`<br />注册之后,如果`intentFilter`中的**动作**发生了,那么这个广播就会**被触发**。<br />最后注意当这个`Activity`被销毁的时候,广播也要被销毁`unregisterReceiver()`!
不讲广播销毁的话,关闭
Activity的时候,广播仍然会运行,会占用内存。
静态注册
动态注册必须在程序启动之后才能接收广播,因为注册的逻辑是写在onCreate()方法中的。那么有没有什么办法可以让程序在未启动的情况下也能接收广播呢?这就需要使用静态注册的方式了。
其实从理论上来说,动态注册能监听到的系统广播,静态注册也应该能监听到,在过去的Android系统中确实是这样的。但是由于大量恶意的应用程序利用这个机制在程序未启动的情况下监听系统广播,从而使任何应用都可以频繁地从后台被唤醒,严重影响了用户手机的电量和性能,因此Android系统几乎每个版本都在削减静态注册
BroadcastReceiver的功能。 在Android 8.0系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。这些特殊的系统广播列表详见: 以上了解即可。
隐式广播例外情况 | Android 开发者 | Android Developers
我们只需要了解其中一条广播:开机广播
android.intent.action.BOOT_COMPLETED
步骤如下:<br />首先创建`BroadcastReceiver`:<br />
这里因为我已经创建好了,所以背景有已经创建的。
修改BootCompleteReceiver的代码,静态注册的代码,如下所示:
package com.example.receiverimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.widget.Toastclass BootCompleteReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {// This method is called when the BroadcastReceiver is receiving an Intent broadcast.Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show()}}
其实就相当于之前动态注册当中的内部类提出来了而已。
最后需要对AndroidManifest.xml进行修改:
添加如下代码:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter>
完整代码为:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.broadcasttest"><!--新增代码--><uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.BroadcastTest"><receiverandroid:name="com.example.receiver.BootCompleteReceiver"android:enabled="true"android:exported="true"><!--新增代码--><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter></receiver><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
Android系统启动完成后会发出一条值为
**android.intent.action.BOOT_COMPLETED**的广播,故需在<receiver>标签中添加一个<intent-filter>标签,并声明相应的action。 这里接收系统的开机广播需要进行权限声明,故在上述代码中使用<uses-permission>标签声明**android.permission.RECEIVE_BOOT_COMPLETED**权限。
最后重启一下手机,就会显示这个事件:<br />
6.3 发送自定义广播
6.3.1 发送标准广播
在发送广播之前,需要先定义一个BroadcastReceiver来准备接收此广播,不然发出去也是白发。因此新建一个**MyBroadcastReceiver**,并在onReceive()方法中加入如下代码:
package com.example.receiverimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.widget.Toastclass MyBroadcastReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {Toast.makeText(context, "received in MyBroadcastReceiver",Toast.LENGTH_SHORT).show()}}
当`MyBroadcastReceiver`收到**自定义的广播**时,就会弹出“received in MyBroadcastReceiver”的提示。然后在`AndroidManifest.xml`中对这个`BroadcastReceiver`进行修改:<br />找到对应的`receiver`:
<receiverandroid:name="com.example.receiver.MyBroadcastReceiver"android:enabled="true"android:exported="true"><!-- 新增代码 --><intent-filter><action android:name="com.example.broadcasttest.MY_BROADCAST"/></intent-filter></receiver>
记住这里的动作名称:
com.example.broadcasttest.MY_BROADCAST。
接下来我们就需要在MainActivity当中调用这个广播,那么先设置一个按钮,用于触发这个广播,修改activity_main.xml中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent" ><Buttonandroid:id="@+id/button"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Send Broadcast"/></LinearLayout>
接着修改`MainActivity`中的代码,如下所示:
package com.example.broadcasttestimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.content.IntentFilterimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.widget.Buttonimport android.widget.Toastclass MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val button: Button = findViewById(R.id.button)button.setOnClickListener {val intent = Intent("com.example.broadcasttest.MY_BROADCAST")intent.setPackage(packageName)sendBroadcast(intent)}}}
这里解释一下`button`控件的点击事件,首先是获取一个意图,注意这里的参数是之前广播接收器的**动作名称**,然后就是申明**包的名字(packageName)**,最后把这个意图作为参数,发送广播`sendBroadcast`即可。
对第2步调用的
setPackage()方法进行详细说明。在Android 8.0系统之后,静态注册的BroadcastReceiver无法接收隐式广播,而默认情况下我们发出的自定义广播都是隐式广播。因此一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将无法接收到这条广播。
6.3.2 发送有序广播
不熟悉的可以会看一下有序广播的定义。
下面直接看实例:
新建AnotherBroadcastReceiver,代码如下所示:
package com.example.receiverimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.widget.Toastclass AnotherBroadcastReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {Toast.makeText(context, "received in AnotherBroadcastReceiver",Toast.LENGTH_SHORT).show()}}
接下来在`AndroidMainifest`当中配置相应的`receiver`;
<receiverandroid:name="com.example.receiver.AnotherBroadcastReceiver"android:enabled="true"android:exported="true"><!-- 新增代码 --><intent-filter><action android:name="com.example.broadcasttest.MY_BROADCAST" /></intent-filter></receiver>
注意这里动作的名称:
com.example.broadcasttest.MY_BROADCAST,和之前的标准广播的是一样的。
到这里位置,运行之后点击按钮,由于两个广播接收器的动作名称都是com.example.broadcasttest.MY_BROADCAST,所以两个广播都会被触发。
而到上面,一直使用的都是标准广播,如果想要使用有序广播,只需要修改一下MainActivity当中的一行代码即可:
package com.example.broadcasttestimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.content.IntentFilterimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.widget.Buttonimport android.widget.Toastclass MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val button: Button = findViewById(R.id.button)button.setOnClickListener {val intent = Intent("com.example.broadcasttest.MY_BROADCAST")intent.setPackage(packageName)// 原代码为sendBroadcast(intent)sendOrderedBroadcast(intent, null)}}}
sendOrderedBroadcast()方法接收两个参数:
- 第一个参数仍然是
Intent - 第二个参数是一个与权限相关的字符串,这里传入null。
 
现在重新运行程序,并点击“Send Broadcast”按钮,你会发现,两个BroadcastReceiver仍然都可以收到这条广播。看上去好像和标准广播并没有什么区别,但这个时候的BroadcastReceiver是有先后顺序的。
而且前面的BroadcastReceiver还可以将广播截断,以阻止其继续传播。
接下来我们设定一下BroadcastReceiver的先后顺序,并且设置截断:
修改AndroidManifest.xml中的代码,如下所示:
找到想要设定顺序的接收器,添加**android:priority**属性。
<receiverandroid:name="com.example.receiver.MyBroadcastReceiver"android:enabled="true"android:exported="true"><!-- 新增代码 --><intent-filter android:priority="100"><action android:name="com.example.broadcasttest.MY_BROADCAST" /></intent-filter></receiver>
这个属性值越大,那么越先触发这个广播。 不设置的时候,默认的顺序是在
AndroidMainifest的先手顺序: 比如:
<receiverandroid:name="com.example.receiver.AnotherBroadcastReceiver"android:enabled="true"android:exported="true"><!-- 新增代码 --><intent-filter><action android:name="com.example.broadcasttest.MY_BROADCAST" /></intent-filter></receiver><receiverandroid:name="com.example.receiver.MyBroadcastReceiver"android:enabled="true"android:exported="true"><!-- 新增代码 --><intent-filter><action android:name="com.example.broadcasttest.MY_BROADCAST" /></intent-filter></receiver>
这里的两个广播,先触发
AnotherBroadcastReceiver,再触发MyBroadcastReceiver
那么如果想要截断某一个广播之后的所有广播,就只需要增加如下代码:
class MyBroadcastReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show()// 截断之后的所有广播abortBroadcast()}}
如果在onReceive()方法中调用了abortBroadcast()方法,就表示将这条广播截断,后面的BroadcastReceiver将无法再接收到这条广播。
6.4 实现强制下线功能
功能需求
首先看一个实际的例子:
比如如果你的QQ号在别处登录了,就会将你强制挤下线。其实实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何其他操作,必须点击对话框中的“确定”按钮,然后回到登录界面即可。
那么回归到代码当中,其实我们可以通过广播的方式来发送这个对话框,因为这样可以实现无论你处于app的哪个页面当中,都只需要使用调用广播即可,不需要每个Activity都实现一个对话框逻辑。
而如果要实现强制下限这个功能,就需要将之前打开的所有Activity进行销毁,而且当这个广播发送完成之后也需要进行销毁,避占用内存。
下面就是一步一步来实现上述的需求。
管理所有的Activity
如果想要做到把所有的Activity同时进行销毁,就需要一个单独的类对所有的Activity进行管理,所以这里我们需要建立一个ActivityCollector类,具体代码如下:
package com.example.collectorimport android.app.Activityobject ActivityCollector {private val activities = ArrayList<Activity>()fun addActivity(activity: Activity) {activities.add(activity)}fun removeActivity(activity: Activity) {activities.remove(activity)}fun finishAll() {for (activity in activities) {if (!activity.isFinishing) {activity.finish()}}activities.clear()}}
这里类的申明上面有个新的关键词,`object`,这个关键词的意思就是将这个类申明为**单例模式**。<br />什么是单例模式?简单来讲就是只允许`ActivityCollector`对自己进行实例化,不允许其他的类对`ActivityCollector`进行实例化,这样做的好处是保证所有的数据能够**由一个实例进行控制**。
关于单例模式的详解参见: 真的写的很好。
我给面试官讲解了单例模式后,他对我竖起了大拇指!_小菠萝的IT之旅的博客-CSDN博客
    后面的代码都比较简单,定义了一个Activity的集合,并且定义三个方法,对这个集合进行增加、删除以及销毁所有**Activity**的操作。
设计基础Activity
如果想要所有的Activity都可以直接实现相同的功能,比如这里的强制下限功能,那么就需要有一个共同的父类,所有的Activity都继承于这个父类即可。我们称之为:BaseActivity。
注意:这个父类还是一个Activity,但是不需要对他进行布局设计,因为本身这个Activity并不需要显示。具体代码如下:
package com.example.broadcastbestpracticeimport android.content.BroadcastReceiverimport android.content.Contextimport android.content.Intentimport android.content.IntentFilterimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport androidx.appcompat.app.AlertDialogimport com.example.collector.ActivityCollector// open 关键词表示这个类可以被继承open class BaseActivity : AppCompatActivity() {lateinit var receiver: ForceOfflineReceiveroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 将Activity增加到集合当中ActivityCollector.addActivity(this)}override fun onResume() {super.onResume()// 定义意图过滤器,实现广播的注册,动作表示的就是强制下限val intentFilter = IntentFilter()intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE")receiver = ForceOfflineReceiver()registerReceiver(receiver, intentFilter)}override fun onPause() {super.onPause()// 注销广播unregisterReceiver(receiver)}override fun onDestroy() {super.onDestroy()// 销毁Activity的时候,同时也把这个Activity从集合中去除ActivityCollector.removeActivity(this)}// 定义内部类,方便调用inner class ForceOfflineReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {// 构建一个对话框,并且进行应用AlertDialog.Builder(context).apply {// 设立对话框的标题setTitle("Warning")// 设立对话框的内容setMessage("You are forced to be offline. Please try to login again.")// 设立对话框不可被Back键取消setCancelable(false)// 设立确定按钮的内容与事件setPositiveButton("OK") { _, _ ->ActivityCollector.finishAll() // 销毁所有 Activityval i = Intent(context, LoginActivity::class.java)context.startActivity(i) // 重新启动 LoginActivity}// 展示对话框show()}}}}
代码的解释在注释里面都写好了,比较简单不细讲了,讲一下设计的思路:
- 首先设计
BroadcastReceiver,这是一个内部类,因为由于BroadcastReceiver中需要弹出一个对话框来阻塞用户的正常操作,但如果创建的是一个静态注册的BroadcastReceiver,是没有办法在onReceive()方法里弹出对话框这样的UI控件的,设计内部类,由于所有的Activity都继承于这个类,所以都可以调用BroadcastReceiver。 - 接下来只需要在
Activity的四个周期当中实行相应的方法即可。 
设立子类
接下来我们只需要设计子Activity继承于基础类,进行操作即可。
这里演示的一个是LoginActivity和MainActivity,下面简单看一下他们的代码:
首先是这两个类的布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="60dp"><TextViewandroid:layout_width="90dp"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:textSize="18sp"android:text="Account:" /><EditTextandroid:id="@+id/accountEdit"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_gravity="center_vertical" /></LinearLayout><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="60dp"><TextViewandroid:layout_width="90dp"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:textSize="18sp"android:text="Password:" /><EditTextandroid:id="@+id/passwordEdit"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_gravity="center_vertical"android:inputType="textPassword" /></LinearLayout><Buttonandroid:id="@+id/login"android:layout_width="200dp"android:layout_height="60dp"android:layout_gravity="center_horizontal"android:text="Login" /></LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent" ><Buttonandroid:id="@+id/forceOffline"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Send force offline broadcast"/></LinearLayout>
都是很简单的布局就不讲了。
下面看一下核心逻辑部分:
package com.example.broadcastbestpracticeimport android.content.Intentimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.widget.Buttonimport android.widget.EditTextimport android.widget.Toastclass LoginActivity : BaseActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_login)val login: Button = findViewById(R.id.login)login.setOnClickListener {// 获取账户编辑框val accountEdit: EditText = findViewById(R.id.accountEdit)// 获取密码编辑框val passwordEdit:EditText = findViewById(R.id.passwordEdit)// 获取账户编辑框的内容val account = accountEdit.text.toString()// 获取密码编辑框的内容val password = passwordEdit.text.toString()// 如果账号是admin且密码是123456,就认为登录成功if (account == "admin" && password == "123456") {// 转跳代码val intent = Intent(this, MainActivity::class.java)startActivity(intent)finish()}else{// 如果登录不成功,则弹出提示,账户或者密码错误Toast.makeText(this, "account or password is invalid", Toast.LENGTH_SHORT).show()}}}}
package com.example.broadcastbestpracticeimport android.content.Intentimport android.os.Bundleimport android.widget.Buttonclass MainActivity : BaseActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val forceOffline: Button = findViewById(R.id.forceOffline)forceOffline.setOnClickListener {val intent = Intent("com.example.broadcastbestpractice.FORCE_OFFLINE")sendBroadcast(intent)}}}
最后别忘记在`AndroidMainifest`当中,把程序的主入口改为:`LoginActivity`:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.broadcastbestpractice"><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.BroadcastBestPractice"><activityandroid:name=".LoginActivity"android:exported="true" ><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity><activityandroid:name=".BaseActivity"android:exported="false" /><activityandroid:name=".MainActivity"android:exported="true"></activity></application></manifest>
