第一行代码第三版——第六章:详解广播机制

全局大喇叭,详解广播机制

为了便于进行系统级别的消息通知,Android 引入了一套非常灵活的广播消息机制,本章就将对这一机制的方方面面进行详细的讲解。

广播机制简介

为什么说 Android 中的广播机制非常灵活呢?这是因为 Android 中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。Android 提供了一套完整的 API,允许应用程序自由地发送和接收广播。

发送广播可以利用我们前面学过的 Intent 来实现,而接收广播的方法则需要引入一个新的概念 ——
BroadcastReceiver。

在介绍 BroadcastReceiver 之前,我们先来了解一下广播的类型,Android 中的广播主要可以分为两种类型:标准广播有序广播

  • 标准广播(normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的 BroadcastReceiver 几乎会在同一时刻收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。

  • 有序广播(ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个 BroadcastReceiver 能够收到这条广播消息,当这个 BroadcastReceiver 中的逻辑执行完毕后,广播才会继续传递。所以此时的 BroadcastReceiver 是有先后顺序的,优先级高的 BroadcastReceiver 就可以先收到广播消息,并且前面的 BroadcastReceiver 还可以截断正在传递的广播,这样后面的 BroadcastReceiver 就无法收到广播消息了。

掌握了这些基本概念后,我们就可以来学习广播的用法了,首先从接收系统广播开始吧。

接收系统广播

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

动态注册监听时间变化

注册 BroadcastReceiver 的方式一般有两种:在代码中注册和在 AndroidManifest.xml 中注册。其中前者也被称为动态注册,后者也被称为静态注册。

那么如何创建一个 BroadcastReceiver 呢?其实只需新建一个类,让它继承自 BroadcastReceiver,并重写父类的 onReceive() 方法就行了。这样当有广播到来时,onReceive() 方法就会得到执行,具体的逻辑就可以在这个方法中处理。

下面我们就先通过动态注册的方式编写一个能够监听时间变化的程序,借此学习一下 BroadcastReceiver 的基本用法。新建一个 BroadcastTest 项目,然后修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    private lateinit var timeChangedReceiver: TimeChangerReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val intentFilter = IntentFilter()
        intentFilter.addAction("android.intent.action.TIME_TICK")
        timeChangedReceiver = TimeChangerReceiver()
        registerReceiver(timeChangedReceiver, intentFilter)
    }

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

    inner class TimeChangerReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
        }
    }

}
复制代码

可以看到,我们在 MainActivity 中定义了一个内部类 TimeChangeReceiver,这个类是继承自 BroadcastReceiver 的,并重写了父类的 onReceive() 方法。这样每当系统时间发生变化时, onReceive() 方法就会得到执行,这里只是简单地使用 Toast 提示了一段文本信息。

然后观察 onCreate() 方法,首先我们创建了一个 IntentFilter 的实例,并给它添加了一个值为 android.intent.action.TIME_TICK 的 action,为什么要添加这个值呢?因为当系统时间发生变化时,系统发出的正是一条值为 android.intent.action.TIME_TICK 的广播,也就是说我们的 BroadcastReceiver 想要监听什么广播,就在这里添加相应的 action。接下来创建了一个 TimeChangeReceiver 的实例,然后调用 registerReceiver() 方法进行注册,将 TimeChangeReceiver 的实例和 IntentFilter 的实例都传了进去,这样 TimeChangeReceiver 就会收到所有值为 android.intent.action.TIME_TICK 的广播,也就实现了监听系统时间变化的功能。

最后要记得,动态注册的 BroadcastReceiver 一定要取消注册才行,这里我们是在 onDestroy() 方法中通过调用 unregisterReceiver() 方法来实现的。

整体来说,代码还是非常简单的。现在运行一下程序,然后静静等待时间发生变化。系统每隔一分钟就会发出一条 android.intent.action.TIME_TICK 的广播,因此我们最多只需要等待一分钟就可以收到这条广播了,如下图所示:
image.png

这就是动态注册 BroadcastReceiver 的基本用法,虽然这里我们只使用了一种系统广播来举例,但是接收其他系统广播的用法是一模一样的。Android 系统还会在亮屏熄屏、电量变化、网络变化等场景下发出广播。

静态注册实现开机启动

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

在 Android 8.0 系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。

在这些特殊的系统广播当中,有一条值为 android.intent.action.BOOT_COMPLETED 的广播,这是一条开机广播,那么就使用它来举例学习吧。

这里我们准备实现一个开机启动的功能。在开机的时候,我们的应用程序肯定是没有启动的,因此这个功能显然不能使用动态注册的方式来实现,而应该使用静态注册的方式来接收开机广播,然后在 onReceive() 方法里执行相应的逻辑,这样就可以实现开机启动的功能了。

通过 Android Studio 提供的快捷方式来创建 BootCompleteReceiver,代码如下所示:

class BootCompleteReceiver : BroadcastReceiver() {

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

代码非常简单,我们只是在 onReceive() 方法中使用 Toast 弹出一段提示信息。

另外,静态的 BroadcastReceiver 一定要在 AndroidManifest.xml 文件中注册才可以使用。不过,由于我们是使用 Android Studio 的快捷方式创建的 BroadcastReceiver,因此注册这一步已经自动完成了。打开 AndroidManifest.xml文件瞧一瞧,代码如下所示:

<?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" />

    <application
        android: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">

		...

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

    </application>

</manifest>
复制代码

可以看到,application 标签内出现了一个新的标签 receiver,所有静态的 BroadcastReceiver 都是在这里进行注册的。它的用法其实和 activity 标签非常相似,也是通过 android:name 指定具体注册哪一个 BroadcastReceiver。

不过目前的 BootCompleteReceiver 是无法收到开机广播的,因为我们还需要对 AndroidManifest.xml 文件中的 receiver 进行修改才行,如下所示:

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

由于 Android 系统启动完成后会发出一条值为 android.intent.action.BOOT_COMPLETED 的广播,因此我们在 receiver 标签中又添加了一个 intent-filter 标签,并在里面声明了相应的 action。

另外,这里有非常重要的一点需要说明。Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,必须在 AndroidManifest.xml 文件中进行权限声明,否则程序将会直接崩溃。比如这里接收系统的开机广播就是需要进行权限声明的,所以我们在上述代码中使用 uses-permission 标签声明了 android.permission.RECEIVE_BOOT_COMPLETED 权限。

重新运行程序,现在我们的程序已经可以接收开机广播了。重启模拟器,模拟器重启完成之后,我们就可以收到开机广播了,如下图所示:
image.png

到目前为止,我们在 BroadcastReceiver 的 onReceive() 方法中只是简单地使用 Toast 提示了一段文本信息,当你真正在项目中使用它的时候,可以在里面编写自己的逻辑。需要注意的是,不要在 onReceive() 方法中添加过多的逻辑或者进行任何的耗时操作,因为 BroadcastReceiver 中是不允许开启线程的,当 onReceive() 方法运行了较长时间而没有结束时,程序就会出现错误。

发送自定义广播

通过上面的学习,我们已经学会了通过 BroadcastReceiver 来接收系统广播,接下来我们就要学习一下如何在应用程序中发送自定义的广播。前面已经介绍过了,广播主要分为两种类型:标准广播和有序广播。我们就通过实践的方式来看一下这两种广播具体的区别。

发送标准广播

在发送广播之前,我们还是需要先定义一个 BroadcastReceiver 来准备接收此广播,不然发出去也是白发。因此新建一个 MyBroadcastReceiver,并在 onReceive() 方法中加入如下代码:

class 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 进行修改:

<?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" />

    <application
        android: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">
        
        ...

        <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST" />
            </intent-filter>
        </receiver>
        
    </application>

</manifest>
复制代码

可以看到,这里让 MyBroadcastReceiver 接收一条值为 com.example.broadcasttest.MY_BROADCAST 的广播,因此待会儿在发送广播的时候,我们就需要发出这样的一条广播。

接下来修改 activity_main.xml 中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send Broadcast" />
    
</LinearLayout>
复制代码

这里在布局文件中定义了一个按钮,用于作为发送广播的触发点。然后修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
            intent.setPackage(packageName)
            sendBroadcast(intent)
        }
    }
    
}
复制代码

可以看到,我们在按钮的点击事件里面加入了发送自定义广播的逻辑。

首先构建了一个 Intent 对象,并把要发送的广播的值传入。然后调用 Intent 的 setPackage() 方法,并传入当前应用程序的包名。packageName 是 getPackageName() 的语法糖写法,用于获取当前应用程序的包名。最后调用 sendBroadcast() 方法将广播发送出去,这样所有监听 com.example.broadcasttest.MY_BROADCAST 这条广播的 BroadcastReceiver 就会收到消息了。此时发出去的广播就是一条标准广播。

这里我还得对第 2 步调用的 setPackage() 方法进行更详细的说明。前面已经说过,在 Android8.0 系统之后,静态注册的 BroadcastReceiver 是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。因此这里一定要调用 setPackage() 方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的 BroadcastReceiver 将无法接收到这条广播。

现在重新运行程序,并点击 “Send Broadcast” 按钮,效果如下图所示:
image.png

这样我们就成功完成了发送自定义广播的功能。

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

发送有序广播

和标准广播不同,有序广播是一种同步执行的广播,并且是可以被截断的。为了验证这一点,我们需要再创建一个新的 BroadcastReceiver。新建 AnotherBroadcastReceiver,代码如下所示:

class AnotherBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show()
    }

}
复制代码

很简单,这里仍然是在 onReceive() 方法中弹出了一段文本信息。

然后在 AndroidManifest.xml 中对这个 BroadcastReceiver 的配置进行修改,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.broadcasttest">

    ...

    <application
        android: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">

        ...
       
        <receiver
            android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST" />
            </intent-filter>
        </receiver>

    </application>

</manifest>
复制代码

可以看到,AnotherBroadcastReceiver 同样接收的是 com.example.broadcasttest.MY_BROADCAST 这条广播。现在重新运行程序,并点击 “Send Broadcast” 按钮,就会分别弹出两次提示信息,如下所示:
gif.gif

不过,到目前为止,程序发出的都是标准广播,现在我们来尝试一下发送有序广播。重新回到 BroadcastTest项目,然后修改 MainActivity 中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
            intent.setPackage(packageName)
            sendOrderedBroadcast(intent, null)
        }
    }

}
复制代码

可以看到,发送有序广播只需要改动一行代码,即将 sendBroadcast() 方法改成 sendOrderedBroadcast() 方法。sendOrderedBroadcast() 方法接收两个参数:第一个参数仍然是 Intent;第二个参数是一个与权限相关的字符串,这里传入 null 就行了。现在重新运行程序,并点击 “Send Broadcast” 按钮,你会发现,两个 BroadcastReceiver 仍然都可以收到这条广播。

看上去好像和标准广播并没有什么区别嘛。不过别忘了,这个时候的 BroadcastReceiver 是有先后顺序的,而且前面的 BroadcastReceiver 还可以将广播截断,以阻止其继续传播。

那么该如何设定 BroadcastReceiver 的先后顺序呢?当然是在注册的时候进行设定了,修改AndroidManifest.xml 中的代码,如下所示:

<?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" />

    <application
        android: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">

        ...

        <receiver
            android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="100">
                <action android:name="com.example.broadcasttest.MY_BROADCAST" />
            </intent-filter>
        </receiver>

    </application>

</manifest>
复制代码

可以看到,我们通过 android:priority 属性给 AnotherBroadcastReceiver 设置了优先级,优先级比较高的 BroadcastReceiver 就可以先收到广播。这里将 AnotherBroadcastReceiver 的优先级设成了100,以保证它一定会在 MyBroadcastReceiver 之前收到广播。

既然已经获得了接收广播的优先权,那么 AnotherBroadcastReceiver 就可以选择是否允许广播继续传递了。修改 AnotherBroadcastReceiver 中的代码,如下所示:

class AnotherBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show()
        abortBroadcast()
    }

}
复制代码

如果在 onReceive() 方法中调用了 abortBroadcast() 方法,就表示将这条广播截断,后面的 BroadcastReceiver 将无法再接收到这条广播。

现在重新运行程序,并点击 “Send Broadcast” 按钮,你会发现只有 AnotherBroadcastReceiver 中的 Toast 信息能够弹出,说明这条广播经过 AnotherBroadcastReceiver 之后确实终止传递了,效果如下所示:
gif (1).gif

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

强制下线应该算是一个比较常见的功能,比如如果你的 QQ 号在别处登录了,就会将你强制挤下线。其实实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何其他操作,必须点击对话框中的“确定”按钮,然后回到登录界面即可。可是这样就会存在一个问题:当用户被通知需要强制下线时,可能正处于任何一个界面,难道要在每个界面上都编写一个弹出对话框的逻辑?如果你真的这么想,那思路就偏远了。我们完全可以借助本章所学的广播知识,非常轻松地实现这一功能。新建一 BroadcastBestPractice 项目,然后开始动手吧。

强制下线功能需要先关闭所有的 Activity,然后回到登录界面。我们在第 3 章探究 Activity 的最佳实践部分已经实现过关闭所有 Activity 的功能了,因此这里使用同样的方案即可。先创建一个 ActivityCollector 类用于管理所有的 Activity,代码如下所示:

object 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()
    }
}
复制代码

然后创建 BaseActivity 类作为所有 Activity 的父类,代码如下所示:

open class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ActivityCollector.addActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        ActivityCollector.removeActivity(this)
    }
}
复制代码

接下来创建一个 LoginActivity 来作为登录界面,并让 Android Studio 帮我们自动生成相应的布局文件。然后编辑布局文件 activity_login.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:text="Account:"
            android:textSize="18sp" />

        <EditText
            android:id="@+id/accountEdit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_weight="1" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:text="Password:"
            android:textSize="18sp" />

        <EditText
            android:id="@+id/passwordEdit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_weight="1"
            android:inputType="textPassword" />
    </LinearLayout>

    <Button
        android:id="@+id/login"
        android:layout_width="200dp"
        android:layout_height="60dp"
        android:layout_gravity="center_horizontal"
        android:text="Login" />
    
</LinearLayout>
复制代码

这里我们使用 LinearLayout 编写了一个登录布局,最外层是一个纵向的 LinearLayout,里面包含了 3 行直接子元素。第一行是一个横向的 LinearLayout,用于输入账号信息;第二行也是一个横向的 LinearLayout,用于输入密码信息;第三行是一个登录按钮。

接下来修改 LoginActivity 中的代码,如下所示:

class LoginActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        login.setOnClickListener {
            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()
            }
        }

    }
}
复制代码

这里我们模拟了一个非常简单的登录功能。首先将 LoginActivity 的继承结构改成继承自 BaseActivity,然后在登录按钮的点击事件里对输入的账号和密码进行判断:如果账号是 admin 并且密码是 123456,就认为登录成功并跳转到 MainActivity,否则就提示用户账号或密码错误。

因此,你可以将 MainActivity 理解成是登录成功后进入的程序主界面,这里我们并不需要在主界面提供什么花哨的功能,只需要加入强制下线功能就可以了。修改 activity_main.xml 中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/forceOffline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send force offline broadcast" />
    
</LinearLayout>
复制代码

非常简单,只有一个按钮用于触发强制下线功能。然后修改 MainActivity 中的代码,如下所示:

class MainActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        forceOffline.setOnClickListener {
            val intent = Intent("com.example.broadcastbestpractice.FORCE_OFFLINE")
            sendBroadcast(intent)
        }

    }
}
复制代码

同样非常简单,不过这里有个重点,我们在按钮的点击事件里发送了一条广播,广播的值为 com.example.broadcastbestpractice.FORCE_OFFLINE,这条广播就是用于通知程序强制用户下线的。也就是说,强制用户下线的逻辑并不是写在 MainActivity 里的,而是应该写在接收这条广播的 BroadcastReceiver 里。这样强制下线的功能就不会依附于任何界面了,不管是在程序的任何地方,只要发出这样一条广播,就可以完成强制下线的操作了。

那么毫无疑问,接下来我们就需要创建一个 BroadcastReceiver 来接收这条强制下线广播。唯一的问题就是,应该在哪里创建呢?由于 BroadcastReceiver 中需要弹出一个对话框来阻塞用户的正常操作,但如果创建的是一个静态注册的 BroadcastReceiver,是没有办法在 onReceive() 方法里弹出对话框这样的 UI 控件的,而我们显然也不可能在每个 Activity 中都注册一个动态的 BroadcastReceiver。

那么到底应该怎么办呢?答案其实很明显,只需要在 BaseActivity 中动态注册一个 BroadcastReceiver 就可以了,因为所有的 Activity 都继承自 BaseActivity。

修改 BaseActivity中的代码,如下所示:

open class BaseActivity : AppCompatActivity() {

    lateinit var receiver: ForceOfflineReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ActivityCollector.addActivity(this)
    }

    override fun onResume() {
        super.onResume()
        val intentFilter = IntentFilter("com.example.broadcastbestpractice.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 i = Intent(context, LoginActivity::class.java)
                    context.startActivity(i) // 重新启动LoginActivity
                }
                show()
            }

        }

    }
}
复制代码

先来看一下 ForceOfflineReceiver 中的代码,这次 onReceive() 方法里可不再是仅仅弹出一个 Toast 了,而是加入了较多的代码,那我们就来仔细看看吧。首先是使用 AlertDialog.Builder 构建一个对话框。注意,这里一定要调用 setCancelable() 方法将对话框设为不可取消,否则用户按一下 Back 键就可以关闭对话框继续使用程序了。然后使用 setPositiveButton() 方法给对话框注册确定按钮,当用户点击了 “OK” 按钮时,就调用 ActivityCollector 的 finishAll() 方法销毁所有 Activity,并重新启动 LoginActivity。

再来看一下我们是怎么注册 ForceOfflineReceiver 这个 BroadcastReceiver 的。可以看到,这里重写了 onResume() 和 onPause() 这两个生命周期方法,然后分别在这两个方法里注册和取消注册了 ForceOfflineReceiver。

为什么要这样写呢?之前不都是在 onCreate() 和 onDestroy() 方法里注册和取消注册 BroadcastReceiver 的吗?这是因为我们始终需要保证只有处于栈顶的 Activity 才能接收到这条强制下线广播,非栈顶的 Activity 不应该也没必要接收这条广播,所以写在 onResume() 和 onPause() 方法里就可以很好地解决这个问题,当一个 Activity 失去栈顶位置时就会自动取消 BroadcastReceiver 的注册。

这样的话,所有强制下线的逻辑就已经完成了,接下来我们还需要对 AndroidManifest.xml 文件进行修改,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.broadcastbestpractice">

    <application
        android: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">
        <activity android:name=".MainActivity"></activity>
        <activity android:name=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
复制代码

这里只需要对一处代码进行修改,就是将主 Activity 设置为 LoginActivity,而不再是 MainActivity,因为你肯定不希望用户在没登录的情况下就能直接进入程序主界面吧?

好了,现在来尝试运行一下程序吧。首先会进入登录界面,并可以在这里输入账号和密码,如下图所示:

gif (2).gif

小结

本章我们主要是对 Android 的广播机制进行了深入的研究,不仅了解了广播的理论知识,还掌握了接收广播、发送自定义广播以及本地广播的使用方法。BroadcastReceiver 属于 Android 四大组件之一。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享