前言

就像计算机网络中的广播一样,我们的Android中也存在类似的广播机制,并且Android中的广播机制更加灵活。


广播机制简介

Android中的每个应用程序都可以对自己感兴趣的广播进行注册,这样改程序就只会收到自己关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。Android提供了一套完整的API,允许应用程序自由地发送和接收广播。发送广播的方式就是我们熟知的Intent,而接收广播的主角就是四大组件之一的BroadcastReceiver。

Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量变化会发出一条广播,系统时间发生改变也会发出一条广播,等等。

广播主要有两种类型

  • 标准广播:一种完全异步执行的广播,在发送广播之后,所有的BroadcastReceiver几乎会在同一时刻收到这条广播信息,因此它们之间没有任何先后顺序可言。
  • 有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播信息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是又先后顺序的,优先极高的BroadcastReceiver就可以先收到广播信息,并且前面的BroadcastReceiver还可以截断正在传递的广播,使后面的BroadcastReceiver无法收到这条广播。

接收系统广播

动态注册方式监听广播

我们可以根据自己感兴趣的广播,自由注册BroadcastReceiver,这样当有相应的广播发出时,相应的BroadcastReceiver就能够收到该广播,并可以在内部进行逻辑处理。注册BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被成为动态注册,后者也被成为静态注册。

BroadcastReceiver的创建:

只需要创建一个类继承自BroadcastReceiver并重写onReceive()方法就行。这样当有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。下面,我来演示一下

class MainActivity : BaseActivity() {
lateinit var binding:ActivityMainBinding
lateinit var timeChangeReceiver: TimeChangeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

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

}
  • 首先,我们在MainActivity中定义了一个内部类,并使其继承自BroadcastReceiver,onReceive()里面是简单的打印信息
  • onCreate()方法中,我们创建了一个IntentFilter实例,并添加了一个值为android.intent.action.TIME_TICK的action,这个action是系统提供的,时间发生变化系统就会发送这条广播
  • 调用registerReceiver()方法进行注册,将TImeChangeReceiver()的实例和IntentFilter()的实例传进去,这样就实现了监听时间变化的功能
  • 最后一定要记得取消注册,否则将带来内存泄漏的风险。

静态注册方式监听广播

动态的BroadcastReceiver可以自由地控制注册和注销,在灵活性方面有很大优势。但是它存在一个缺点,即必须在程序启动之后才能接受广播,因为注册的逻辑是卸载onCreate()方法中的。那么有什么方法可以让程序在未启动的情况下也能接受广播呢?这就需要使用静态注册的方式了。

理论上讲,动态注册能监听到的系统广播,静态注册也能监听到,再过去的Android系统中确实是这样的。但是由于大量恶意的应用程序利用这个机制在程序未启动的情况下监听系统广播,因此Android系统几乎每个版本都在削弱静态注册BroadcastReceiver的功能。

在Android8.0之后,所有隐式广播都不允许使用静态注册的方式接收了。隐式广播指的是哪些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式接收。详情见:https://developer.android.google.cn/guide/components/broadcast-exceptions.html

这些特殊广播中有一条非常值得关注的是android.intent.action.BOOT_COMPLETED的广播,这是一条开机广播。

接下来我们来实操一下,要在开机后做一些事:

  • 首先按如下方式创建一个BroadcastReceiver,右键New->Other->Broadcast Receiver,这样的好处是我们不用再手动在AndroidManifest.xml注册BroadcastReceiver

image-20221012115440320

class BootCompleteReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context,"Boot Complete",Toast.LENGTH_LONG).show()
}
}

在AndroidManifest.xml中

  • 添加权限声明

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  • 标签中又添加了一个标签,并在里面声明了相应的action

    <receiver
    android:name=".BootCompleteReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
    </receiver>

到此,我们就实现了静态监听广播,我这里只是做了个简单的打印,你可以在你的项目中做自己的逻辑。

注意:不要再onReceive()方法中做过多的逻辑或者任何耗时的操作,因为BroadcastReceiver中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会出现错误。


发送自定义广播

发送标准广播

  1. 首先需要一个BroadcastReceiver,这里就不在放代码了,前面提过很多次了。

  2. AndroidManifest.xml中添加如下信息

    com.example.broadcast.MY_BROADCAST是一会儿自定义的广播,可以看到我的格式是模拟系统的广播,可以自己任意命名

    <receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
    <action android:name="com.example.broadcast.MY_BROADCAST"/>
    </intent-filter>
    </receiver>
  3. ```kotlin
    class MainActivity : BaseActivity() {
    lateinit var binding:ActivityMainBinding

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


    binding.startActivityBtn.setOnClickListener {
    SecondActivity.actionStart(this,”data1”,”data2”)
    }
    binding.sendBroadcast.setOnClickListener {
    val intent = Intent(“com.example.broadcast.MY_BROADCAST”)
    intent.setPackage(packageName)
    sendBroadcast(intent)
    }
    }

    override fun onDestroy() {
    super.onDestroy()
    }
    }


    > - 创建一个Intent对象,并把要发送的广播的值传入
    > - 然后调用Intent的`setPackage()`方法,并传入当前应用程序的包名。`packageName`是`getPackageName()`的语法糖写法。
    > - Android8.0之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。因此这里一定要调用`setPackage()`方法,指定这条广播发送给哪个应用程序,从而把它编程一条显示广播。
    > - 最后调用`sendBroadcast()`方法将广播发送出去

    这样,我们的自定义广播的步骤就完成了。

    另外,由于广播是使用Intent来发送的,因此我们可以在Intent值中携带一些数据传递给相应的BroadcastReceiver,这一点和Activity的用法是比较相似的。

    ***



    ## 发送有序广播

sendOrderedBroadcast(intent,null)


发送有序广播只需要该一行代码即可,即将`sendBroadcast()`方法改成sendOrderedBroadcast()方法。sendOrderedBroadcast()方法接收两个参数:第一个参数仍然是Intent;第二个参数是一个与权限相关的字符串,这里传入null就行了。

还没结束,有序广播最大的好处就是按顺序接收,那么如何确定接收顺序呢?

- 设置权重:

`priority`的值越高,优先级越高。

![image-20221012144015349](https://banmaman-1312858980.cos.ap-chengdu.myqcloud.com/image-20221012144015349.png)

- 截断广播

可以在优先级高的BroadcastReceiver中截断这条,使后面的BroadcastReceiver都接收不到这条广播。

![image-20221012144112193](https://banmaman-1312858980.cos.ap-chengdu.myqcloud.com/image-20221012144112193.png)

***





## 广播的最佳实践:实现强制下线功能

> 强制下线应该算是一个比较常见的功能,比如如果你的QQ号在别处登录了,就会将你强制挤下线。其实实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何其他操作,必须点击对话框的“确定”按钮,然后回到登录界面即可。可是这样就会存在一个问题:当用户被通知需要强制下线时,可能正处于任何一个界面,难道需要在每个界面上都编写弹出对话框的逻辑?那当然不是,且听我娓娓道来。
>
> 强制下线功能需要先关闭所有Activity,然后回到登录界面。这部分大家应该很熟悉,没错,就是我前面在介绍Activity的声明周期时提到的一套实践方法,详情见[Activity声明周期和启动模式](http://banmaman.com/2022/10/01/Activity%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E4%BA%8E%E5%90%AF%E5%8A%A8%E6%A8%A1%E5%BC%8F/),下面小编来贴上核心代码和讲解。

核心代码:

```kotlin
open class BaseActivity: AppCompatActivity() {
lateinit var receiver:ForceOfflineReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ActivityCollector.addActivity(this)
}

override fun onResume() {
super.onResume()
val intentFilter = IntentFilter()
//com.example.broadcastpractice.FORCE_OFFLINE为自定义下线广播
intentFilter.addAction("com.example.broadcastpractice.FORCE_OFFLINE")
receiver = ForceOfflineReceiver()
registerReceiver(receiver,intentFilter)
}

override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}

override fun onDestroy() {
super.onDestroy()
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.")
setCancelable(false)
setPositiveButton("OK"){_,_->
ActivityCollector.finishAll()//销毁所有Activity
val intent = Intent(context,LoginActivity::class.java)
context.startActivity(intent)//重新启动LoginActivity
}
show()
}
}
}

}
  • 只要触发机制发送一条值为com.example.broadcastpractice.FORCE_OFFLINE的广播,这条广播就是用于通知程序强制用户下线的,也就是说,强制用户下线的逻辑并不是写在MainActivity里,而是应该写在接收这条广播的BroadcastReceiver里。这样强制下线的功能就不会依附于任何界面了,不管是在程序的任何地方,只要发出这样的广播,就可以完成强制下线的操作。
  • 由于BroadcastReceiver中需要弹出一个对话框来阻塞用户的正常操作,但如果创建的是一个静态注册的BroadcastReceiver,是没有办法在onReceive()方法里弹出对话框这样的UI控件(弹窗依附于Activity),而我们显然不可能在每个Activity中都注册一个动态的BroadcastReceiver。怎么办?很简单——只需要在BaseActivity中动态注册一个BroadcastReceiver即可,毕竟此程序中所有的Activity都继承自BaseActivtiy。
  • 在ForceOfflineReceiver中:
    • 首先是使用 AlertDialog.Builder构建一个对话框。注意,这里一定要调用setCancelable()方法将对话框设置为不可取消,否则用户按一下Back按钮就可以关闭对话框继续使用程序了
    • 使用setPositiveButton()方法给对话框注册确定按钮,当用户点击了“OK”按钮时,就调用ActivityCollectorfinishAll()方法销毁所有的Activity,并重新启动LoginActivity
  • 再来看看ForceOfflineReceiver这个BroadcastReceiver的注册:
    • 分别在onResume()和onPause()这两个声明周期方法中完成注册和注销
    • 为什么不在onDestroy()中注销呢?因为我们始终需要保证只有处于栈顶的Activity才能收到这条下线广播,产生弹窗。如果其他Activity都能弹窗,那么岂不是做了无用功!
  • 当然还需要配置的是,在AndroidManifest.xml中将LoginActivity设置为主Activity,因为你不可能不登录就进入程序主页面吧?

参考资料:《第一行代码》