BroadcastReceiver

广播机制简介

Android中的每个应用程序都可以对自己 感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自 于系统的,也可能是来自于其他应用程序的。而接收广播的方法则需要引入一个新的概念—— BroadcastReceiver。

BroadcastReceiver的具体用法将会在下一节介绍,这里我们先来了解一下广播的类型。 Android中的广播主要可以分为两种类型:标准广播有序广播

  • 标准广播(normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的 BroadcastReceiver几乎会在同一时刻收到这条广播消息,因此它们之间没有任何先后顺 序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。
  • 有序广播(ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻 只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的 逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的, 优先级高的BroadcastReceiver就可以先收到广播消息,并且前面的BroadcastReceiver 还可以截断正在传递的广播,这样后面的BroadcastReceiver就无法收到广播消息了。

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

接受系统广播

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

动态注册监听时间变化

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

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

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

class MainActivity : AppCompatActivity(){
lateinit var timeChangeReceiver:TimeChangeReceiver
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val intentFilter = IntentFilter()
intentFilter.addAction("android.intent.action.TIME_TICK")
timeChangeReceiver = TimeChangeReceiver()
registerReiver(timeChangeReceiver,intentFilter)
}
override fun onDestroy(){
super.onDestroy()
unregisterReceiver(timeChangeReceiver)
}

inner class TimeChangeReceiver : BroadcastReceiver(){
override fun onReceiver(context:context,intent:Intent()){
Toast.makeText(this,"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()方法来实现的。

这就是动态注册BroadcastReceiver的基本用法,虽然这里我们只使用了一种系统广播来举 例,但是接收其他系统广播的用法是一模一样的。Android系统还会在亮屏熄屏、电量变化、网 络变化等场景下发出广播。如果你想查看完整的系统广播列表,可以到如下的路径中去查看:

<Android SDK>/platforms/<任意android api版本>/data/broadcast_actions.txt 

静态注册实现开机自启动

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

其实从理论上来说,动态注册能监听到的系统广播,静态注册也应该能监听到,在过去的 Android系统中确实是这样的。但是由于大量恶意的应用程序利用这个机制在程序未启动的情况 下监听系统广播,从而使任何应用都可以频繁地从后台被唤醒,严重影响了用户手机的电量和 性能,因此Android系统几乎每个版本都在削减静态注册BroadcastReceiver的功能。

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

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

那么就开始动手吧。上一小节中我们是使用内部类的方式创建的BroadcastReceiver,其实还 可以通过Android Studio提供的快捷方式来创建。右击com.example.broadcasttest包 →New→Other→Broadcast Receiver

可以看到,这里我们将创建的类命名为BootCompleteReceiver,Exported属性表示是否允 许这个BroadcastReceiver接收本程序以外的广播,Enabled属性表示是否启用这个 BroadcastReceiver。勾选这两个属性,点击“Finish”完成创建

然后修改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文件瞧一瞧,代码如下所示:

<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/AppTheme">
...
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
</receiver>
</application>

</manifest>

可以看到,标签内出现了一个新的标签,所有静态的 BroadcastReceiver都是在这里进行注册的。它的用法其实和标签非常相似,也 是通过android:name指定具体注册哪一个BroadcastReceiver,而enabled和exported属 性则是根据我们刚才勾选的状态自动生成的。

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

这是你第一次遇到权限的问题,其实 Android中的许多操作是需要声明权限才可以进行的,后 面我们还会不断使用新的权限。不过目前这个接收系统开机广播的权限还是比较简单的,只需 要在 AndroidManifest.xml 文件中声明一下就可以了。Android 6.0 系统中引入了更加严格 的运行时权限,从而能够更好地保证用户设备的安全和隐私。关于这部分内容我们将在第 8章 中学习。

发送自定义广播

发送标准广播

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

class MyBroadcastReceiver : BroadcastReceiver(){
override fun onReceive(context:Context,intent:Intent){
Toast.makeText(context,"received in MyBroadcastReceiver").show()
}
}

当MyBroadcastReceiver收到自定义的广播时,就会弹出“received in MyBroadcastReceiver”的提示。

然后在AndroidManifest.xml中对这个BroadcastReceiver进行修改:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<application
android:allowBackup="true"
android:icon="mipman/ic_launcher"
android:label="@string/app_name"
android:rountIcon="mipmap/ic_launcher_round"
android:supportRtl="true"
android:theme="@style/AppTheme">
<receiver
android:name=".MyBroadcastReceiver"
android:enable="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCADT"/>
</intent-filter>
</receiver>
</application>
</manifest>

接下来修改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" >

<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)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val button = binding.button
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()方法进行更详细的说明。前面已经说过,在Android 8.0系统之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下我们发出 的自定义广播恰恰都是隐式广播。因此这里一定要调用setPackage()方法,指定这条广播是 发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将 无法接收到这条广播。

发送有序广播

和标准广播不同,有序广播是一种同步执行的广播,并且是可以被截断的。为了验证这一点, 我们需要再创建一个新的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的配置进行修改,代码如下所示:

<manifest xmlns:androd="http://schmas.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:supportRtl="true"
android:theme="@style/AppTheme">
<receiver
android:name=".AnotherBroadcastReceiver"
android:enable="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”按钮,就会分别弹出两次提示信息

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

class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedKnstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
intent.setPackage(packageName)
intent.sendOrdererBroadcast(intent,null)
}
}

可以看到,发送有序广播只需要改动一行代码,即将sendBroadcast()方法改成 sendOrderedBroadcast()方法。sendOrderedBroadcast()方法接收两个参数:第一个 参数仍然是Intent;第二个参数是一个与权限相关的字符串,这里传入null就行了。现在重 新运行程序,并点击“Send Broadcast”按钮,你会发现,两个BroadcastReceiver仍然都可 以收到这条广播。 看上去好像和标准广播并没有什么区别嘛。不过别忘了,这个时候的BroadcastReceiver是有 先后顺序的,而且前面的BroadcastReceiver还可以将广播截断,以阻止其继续传播。

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

<manifest xmlns:androd="http://schmas.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:supportRtl="true"
android:theme="@style/AppTheme">
<receiver
android:name=".AnotherBroadcastReceiver"
android:enable="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
</application>
</manifest>

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

class MyBroadcastReceiver : BroadcastReceiver() { 

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

}

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

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

先创建一个ActivityCollector类用于管理所有的Activity,代码如下所示:

obejct 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 activitise){
if(!activity.isFinishing){
acitivity.finish()
}
}
activities.clear()
}
}

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

open class BaseActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstance)
ActivityCollector.addActivity(this)
}
override fun onDestroy(){
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextVeiw
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:"
/>
<EditText
android:id="@id/accountEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="Enter you account"
/>
</LinearLayout>

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
andorid:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password"
/>
<EditText
android:id="@id/passwordEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="Enter your password"
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>

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

class LoginActivity:BaseActivity(){
override fun onCreate(saveInstanceState:Bundle?){
super.onCreate(saveInstanceState)
setContetnView(R.layout.activity_login)
login.setOnclickListener{
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
login.setOnClickListener{
if(account=="admin"&&password=="123456"){
val intent = Intent(this,MainActivity::class.java)
startActivity(intent)
finish()
}else{
Toast.makeText(this,"account or password is valid",Toast.LENGTH_SHORT).show()
}
}
}
}
}

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

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

<LinearLayout xmlns:android="http://shemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent">
<Button
android:id="@id/forceOffline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send force offline broadCast"/>
</LinearLayout>

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

class MianActivity:BaseActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(saved)
setContentView(R.layout.activity_main)
forceOffline.setOnCLickListener{
val intent = Intent("com.example.broadcastbestparctice.FORCE_OFFLIN")
sendBoradcast(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(savedIntaceState:Bundle?){
super.onCreate(savedInstanceState)
ActivityCollector.addActivity(this)
}
override fun onResume(){
super.onResume()
val intentFilter = IntentFilter()
intetnFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE")
receiver = ForceOfflineReceiver()
registerReceiver(receiver,intentFilter)
}
override fun onPause(){
super.onPause()
unregisterReceiver(receiver)
}
override fun onDestroy(){
super.onDestroy()
ActivityyCollector.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()
val i = Intent(context,LoginActivity::class.java)
context.startActivity(i)
}
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的注册。

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

项目总结

因为是在任何Activity中都要起到强制下线的作用,所以可以使用一个ActivityCollector类用于管理所有的Activity

ActivityCollector

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

布局

MainActivity的布局文件activity_mian.xml,只需要一个button按钮

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@id/forceOffline"
android:layout_width="match_parent"
andorid:layout_height="wrap_content"
android:text="Send force offline broadcast"
/>
</LinearLayout>

LoginActivity的布局文件activity_login.xml

<LinearLayout xmlns:android="http:schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account"
/>
<EditText
android:id="@id/accountEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="Enter your Account"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
/>
<EditText
android:id="@id/passwordEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:hint="Enter your password"
/>
</LinearLayout>

<Button
android:id="@id/login"
android:layout_width="200dp"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:text="Login"
/>

</LinearLayout>

修改下AndroidManifest.xml的信息,使LoginActivity成为主Activity

<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCH"/>
</intent-filter>
</activity>

MainActivity

class MainActivity : BaseActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(saveInstaceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val forceOffline.setOnClickListener{
val intent = Intent("com.example.broadcastbestpractice.FORCE_OFFLINE")
sendBroadcast(intent)
}
}
}

LoginActivity

class LoginActivity:BaseActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstaceState)
val binding = ActivityLoginBinding.inflate(layoutInflater)
val longin = binding.login
val accountEdit = binding.accountEdit
val passwordEdit = binding.passwordEdit
login.setOnClickListener{
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
if(account=="admim"&&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()
}
}
}
}

BaseActivity

open class BaseActivity:AppCompatActivity(){
lateinit var receiver : ForceOfflineReceiver
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstandeState)
ActivityCollertor.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()
ActivityCollector.removeActivity(this)
}
inner class ForceOfflineReceiver:BroadcastReceiver(){
override fun onReceive(context:Context?,intent:Intent?){
context?.let{
AlertDialog.Build(it).apply{
setTitle("Warning")
setMessage("You are forced to be offline. Please try to login again")
setPositiveButton("OK"){_,_->
ActivityCollector.finishAll()
val i = Intent(context,LoginActivity::class.java)
context.startActivity(i)
}
show()
}
}
}
}

}

Kotlin课堂:高阶函数详解

定义高阶函数

首先来看一下高阶函数的定义。如果一个函数接收另一个函数作为参数,或者返回值的类型是 另一个函数,那么该函数就称为高阶函数。

接下来我们就学习一下如何定义一个函数类型。不同于定义一个普通的字段类型,函数类型的 语法规则是有点特殊的,基本规则如下:

(String, Int) -> Unit

既然是定义一个函数类型,那么最关键的就是要声明该函数接收什么参数,以及它的返回值是 什么。因此,->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔 开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值 是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void。

现在将上述函数类型添加到某个函数的参数声明或者返回值声明上,那么这个函数就是一个高 阶函数了,如下所示:

fun example(func:(String,Int)->Unit){
func("hello",123)
}

可以看到,这里的example()函数接收了一个函数类型的参数,因此example()函数就是一个 高阶函数。而调用一个函数类型的参数,它的语法类似于调用一个普通的函数,只需要在参数 名的后面加上一对括号,并在括号中传入必要的参数即可

现在我们已经了解了高阶函数的定义方式,但是这种函数具体有什么用途呢?由于高阶函数的 用途实在是太广泛了,这里如果要让我简单概括一下的话,那就是高阶函数允许让函数类型的 参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它 的执行逻辑和最终的返回结果就可能是完全不同的。为了详细说明这一点,下面我们来举一个 具体的例子。

这里我准备定义一个叫作num1AndNum2()的高阶函数,并让它接收两个整型和一个函数类型的 参数。我们会在num1AndNum2()函数中对传入的两个整型参数进行某种运算,并返回最终的运 算结果,但是具体进行什么运算是由传入的函数类型参数决定的。

fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int)->Int):Int{
val result = operation(num1,num2)
return result
}

这是一个非常简单的高阶函数,可能它并没有多少实际的意义,却是个很好的学习示例。 num1AndNum2()函数的前两个参数没有什么需要解释的,第三个参数是一个接收两个整型参数 并且返回值也是整型的函数类型参数。在num1AndNum2()函数中,我们没有进行任何具体的运 算操作,而是将num1和num2参数传给了第三个函数类型参数,并获取它的返回值,最终将得到 的返回值返回。

现在高阶函数已经定义好了,那么我们该如何调用它呢?由于num1AndNum2()函数接收一个函 数类型的参数,因此我们还得先定义与其函数类型相匹配的函数才行。在 HigherOrderFunction.kt文件中添加如下代码:

fun plus(num1:Int,num2:Int):Int{
return num1+num2
}
fun minus(num1:Int,num2:Int):Int{
return num1-num2
}

这里定义了两个函数,并且这两个函数的参数声明和返回值声明都和num1AndNum2()函数中的 函数类型参数是完全匹配的。其中,plus()函数将两个参数相加并返回,minus()函数将两个 参数相减并返回,分别对应了两种不同的运算操作。

有了上述函数之后,我们就可以调用num1AndNum2()函数了,在main()函数中编写如下代 码:

fun main(){
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1,num2,::plus)
val result2 = num1AndNum2(num1,num2,::minus)
println("result1 is $result1")
println("result2 is $result2")
}

注意这里调用num1AndNum2()函数的方式,第三个参数使用了::plus和::minus这种写法。 这是一种函数引用方式的写法,表示将plus()和minus()函数作为参数传递给 num1AndNum2()函数。而由于num1AndNum2()函数中使用了传入的函数类型参数来决定具体 的运算逻辑,因此这里实际上就是分别使用了plus()和minus()函数来对两个数字进行运算。

使用这种函数引用的写法虽然能够正常工作,但是如果每次调用任何高阶函数的时候都还得先 定义一个与其函数类型参数相匹配的函数,这是不是有些太复杂了? 没错,因此Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员 引用等。其中,Lambda表达式是最常见也是最普遍的高阶函数调用方式,也是我们接下来要 重点学习的内容。 上述代码如果使用Lambda表达式的写法来实现的话,代码如下所示:

fun main(){
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1,num2){n1,n2->n1+n2}
val result = num1AndNum2(num1,num2){n1,n2->n1-n2}
println("result is $result1")
println("reuslt is $result2")
}

Lambda表达式的语法规则我们在2.6.2小节已经学习过了,因此这段代码对于你来说应该不难 理解。你会发现,Lambda表达式同样可以完整地表达一个函数的参数声明和返回值声明 (Lambda表达式中的最后一行代码会自动作为返回值),但是写法却更加精简

下面我们继续对高阶函数进行探究。回顾之前在第3章学习的apply函数,它可以用于给 Lambda表达式提供一个指定的上下文,当需要连续调用同一个对象的多个方法时,apply函 数可以让代码变得更加精简,比如StringBuilder就是一个典型的例子。接下来我们就使用高 阶函数模仿实现一个类似的功能。

修改HigherOrderFunction.kt文件,在其中加入如下代码:

fun StringBuiler.build(block:StringBuiler.()->Unit):StringBuilder{
block()
return this
}

这里我们给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参 数,并且返回值类型也是StringBuilder。

注意,这个函数类型参数的声明方式和我们前面学习的语法有所不同:它在函数类型的前面加 上了一个StringBuilder. 的语法结构。这是什么意思呢?其实这才是定义高阶函数完整的语 法规则,在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的。 那么这里将函数类型定义到StringBuilder类当中有什么好处呢?好处就是当我们调用build 函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply函数 的实现方式。 现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。这里 仍然用吃水果这个功能来举例:

fun main(){
val list = listOf("Apple","Banana","Orange","Pear","Grape")
val result = StringBuilder().build{
append("Start eating fruits.\n")
for(fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

可以看到,build函数的用法和apply函数基本上是一模一样的,只不过我们编写的build函 数目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。

内联函数的作用

高阶函数确实非常神奇,用途也十分广泛,可是你知道它背后的实现原理是怎样的吗?当然, 这个话题并不要求每个人都必须了解,但是为了接下来可以更好地理解内联函数这个知识点, 我们还是简单分析一下高阶函数的实现原理。

这里仍然使用刚才编写的num1AndNum2()函数来举例,代码如下所示:

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { 
val result = operation(num1, num2)
return result
}

fun main() {
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1, num2) { n1, n2 ->
n1 + n2
}
}

可以看到,上述代码中调用了num1AndNum2()函数,并通过Lambda表达式指定对传入的两个 整型参数进行求和。这段代码在Kotlin中非常好理解,因为这是高阶函数最基本的用法。可是我 们都知道,Kotlin的代码最终还是要编译成Java字节码的,但Java中并没有高阶函数的概念。

那么Kotlin究竟使用了什么魔法来让Java支持这种高阶函数的语法呢?这就要归功于Kotlin强大 的编译器了。Kotlin的编译器会将这些高阶函数的语法转换成Java支持的语法结构,上述的 Kotlin代码大致会被转换成如下Java代码:

public static int num1AndNum2(int num1, int num2, Function operation) { 
int result = (int) operation.invoke(num1, num2);
return result;
}

public static void main() {
int num1 = 100;
int num2 = 80;
int result = num1AndNum2(num1, num2, new Function() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
}

考虑到可读性,我对这段代码进行了些许调整,并不是严格对应了Kotlin转换成的Java代码。可 以看到,在这里num1AndNum2()函数的第三个参数变成了一个Function接口,这是一种 Kotlin内置的接口,里面有一个待实现的invoke()函数。而num1AndNum2()函数其实就是调 用了Function接口的invoke()函数,并把num1和num2参数传了进去。

调用num1AndNum2()函数的时候,之前的Lambda表达式在这里变成了Function接口的匿 名类实现,然后在invoke()函数中实现了n1 + n2的逻辑,并将结果返回。 这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的Lambda表达式在底层 被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新 的匿名类实例,当然也会造成额外的内存和性能开销。 为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时 开销完全消除。

内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可,如下所 示:

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { 
val result = operation(num1, num2)
return result
}

那么内联函数的工作原理又是什么呢?其实并不复杂,就是Kotlin编译器会将内联函数中的代码 在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。

当然,仅仅一句话的描述可能还是让人不太容易理解,下面我们通过图例的方式来详细说明内 联函数的代码替换过程

首先,Kotlin编译器会将Lambda表达式中的代码替换到函数类型参数调用的地方,如图6.13 所示。

接下来,再将内联函数中的全部代码替换到函数调用的地方,如图6.14所示。

最终的代码就被替换成了如图6.15所示的样子。

noinline与crossinline

接下来我们要讨论一些更加特殊的情况。比如,一个高阶函数中如果接收了两个或者更多函数 类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的 Lambda表达式全部进行内联

但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?这时就可以使用noinline关 键字了,如下所示:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) { 
}

可以看到,这里使用inline关键字声明了inlineTest()函数,原本block1和block2这两 个函数类型参数所引用的Lambda表达式都会被内联。但是我们在block2参数的前面又加上了 一个noinline关键字,那么现在就只会对block1参数所引用的Lambda表达式进行内联了。 这就是noinline关键字的作用。

前面我们已经解释了内联函数的好处,那么为什么Kotlin还要提供一个noinline关键字来排除 内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正 的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的 参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式 中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这 个问题,我们来看下面的例子

fun printString(str: String, block: (String) -> Unit) { 
println("printString begin")
block(str)
println("printString end")
}

fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return@printString
println(s)
println("lambda end")
}
println("main end")
}

这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串 参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接 使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且 不再执行Lambda表达式的剩余部分代码。

可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日 志是正常打印的,说明return@printString确实只能进行局部返回。

但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:

inline fun printString(str: String, block: (String) -> Unit) { 
println("printString begin")
block(str)
println("printString end")
}

fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")
}

现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键 字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么 的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程

可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止 执行了,和我们所预期的结果一致。

将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声 明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例:

inline fun runRunnable(block: () -> Unit) { 
val runnable = Runnable {
block()
}
runnable.run()
}

这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关 键字之后就会提示如图6.18所示的错误

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创 建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而 Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在 匿名类中调用了传入的函数类型参数

而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在 匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中 的函数调用进行返回,因此这里就提示了上述错误。

也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现 中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键 字就可以很好地解决这个问题

inline fun runRunnable(crossinline block: () -> Unit) { 
val runnable = Runnable {
block()
}
runnable.run()
}

可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过 了

那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示图6.18所示的 错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实 现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用 于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问 题也就巧妙地解决了

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用 return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返 回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数 的其他所有特性。