抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

课本

书籍资源进入官网下载,PC端进入

第六章-全局大喇叭-广播机制

记得在我上学的时候,每个班级的教室里都装有一个喇叭,这些喇叭接到学校的广播室,一旦有什么重要的通知,就会播放一条广播来告知全校的师生。类似的工作机制其实在计算机领域也有很广泛的应用,如果你了解网络通信原理,应该会知道,在一个IP网络范围中,最大的IP地址是被保留作为广播地址来使用的。比如某个网络的IP范围是192.168.0.XXX,子网掩码是255.255.255.0,那么这个网络的广播地址就是192.168.0.255。广播数据包会被发送到同一网络上的所有端口,这样该网络中的每台主机都会收到这条广播。
为了便于进行系统级别的消息通知,Android也引入了一套类似的广播消息机制。相比于我前面举的两个例子,Android中的广播机制显得更加灵活,本章就将对这一机制的方方面面进行详细的讲解

广播机制简介

为什么说Android中的广播机制更加灵活呢?这是因为Android中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。Android提供了一套完整的API,允许应用程序自由地发送和接收广播发送广播的方法其实之前稍微提到过,如果你记性好的话,可能还会有印象,就是借助我们第3章学过的Intent。而接收广播的方法则需要引入一个新的概念——BroadcastReceiver

广播类型

标准广播(异步广播)(normal broadcasts)

是一种完全异步执行的广播,在广播发出之后,所有的BroadcastReceiver几乎会在同一时刻收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较,但同时也意味着它是无法被截断的。标准广播的工作流程如图所示。
博客第一行代码3学习标准广播流程图

有序广播(同步广播)(ordered broadcasts)

是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的,优先级高的BroadcastReceiver就可以先收到广播消息,并且前面的BroadcastReceiver还可以截断正在传递的广播,这样后面的BroadcastReceiver就无法收到广播消息了。有序广播的工作流程如图所示。
博客第一行代码3学习有序广播流程图

接受系统广播

Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,系统时间发生改变也会发出一条广播,等等。如果想要接收这些广播,就需要使用BroadcastReceiver,下面我们就来看一下它的具体用法。
动态注册就是在代码中注册,静态注册就是在AndroidManifest.xml中注册
那么如何创建一个BroadcastReceiver呢?其实只需新建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法就行了。这样当有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。下面我们就先通过动态注册的方式编写一个能够监听时间变化的程序,借此学习一下BroadcastReceiver的基本用法。

动态注册监听时间变化

  1. 新建一个BroadcastTest项目,然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     private lateinit var timeChangeReceiver: BroadcastReceiver
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         // 创建了IntentFilter实例,用于匹配响应的广播
         val intentFilter = IntentFilter()
         // 且添加了值为如下的参数,接收该类型的广播
         intentFilter.addAction("android.intent.action.TIME_TICK")
         // 然后实例化了TimeChangeReceiver
         timeChangeReceiver = TimeChangeReceiver()
         // 注册广播
         registerReceiver(timeChangeReceiver,intentFilter)
     }
    
     override fun onDestroy() {
         super.onDestroy()
         // 当Activity被销毁时,注销监听广播
         unregisterReceiver(timeChangeReceiver)
     }
     // 定义的内部类,继承于BroadcastReceiver接收广播
     inner class TimeChangeReceiver:BroadcastReceiver() {
         // 当接收到广播时触发该方法
         override fun onReceive(context: Context?, intent: Intent?) {
             // 简单的消息示意
             Toast.makeText(context,"时间已经改变",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()方法来实现的。

  2. 现在运行一下程序,然后静静等待时间发生变化。系统每隔一分钟就会发出一条android.intent.action.TIME_TICK的广播,因此我们最多只需要等待一分钟就可以收到这条广播了,如图所示:
    博客第一行代码3学习动态注册效果图
  3. 这就是动态注册BroadcastReceiver的基本用法,虽然这里我们只使用了一种系统广播来举例,但是接收其他系统广播的用法是一模一样的。Android系统还会在亮屏熄屏、电量变化、网络变化等场景下发出广播。如果你想查看完整的系统广播列表,可以到如下的路径中去查看:
    <Android SDK目录>/platforms/<任意android api版本>/data/broadcast_actions.txt
    

静态注册实现开机启动

动态注册的BroadcastReceiver可以自由地控制注册与注销,在灵活性方面有很大的优势。但是它存在着一个缺点,即必须在程序启动之后才能接收广播,因为注册的逻辑是写在onCreate()方法中的。那么有没有什么办法可以让程序在未启动的情况下也能接收广播呢?这就需要使用静态注册的方式了。
由于大量的恶意程序利用系统的广播,现如今只有少数的隐式广播能运行使用静态方式接收,特殊的广播列表点击这里
在接下来的我们将使用为android.intent.action.BOOT_COMPLETED开机广播来举例学习.
上一小节中我们是使用内部类的方式创建的BroadcastReceiver,其实还
可以通过Android Studio提供的快捷方式来创建。右击com.example.broadcasttest包→NewOtherBroadcast Receiver

  1. 创建类命名为BootCompleteReceiver的文件
    博客第一行代码3学习创建BroadcastReceiver方法
    Exported属性表示是否允许这个BroadcastReceiver接收本程序以外的广播,Enabled属性表示是否启用这个BroadcastReceiver。勾选这两个属性,点击“Finish”完成创建。
  2. 然后修改BootCompleteReceiver中的代码,如下所示:
    class BootCompleteReceiver : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
         //此方法在BroadcastReceiver接收Intent广播时被调用。
         Toast.makeText(context,"Boot Complete", Toast.LENGTH_SHORT).show()
     }
    }
    
    代码非常简单,我们只是在onReceive()方法中使用Toast弹出一段提示信息。
  3. 另外,静态的BroadcastReceiver一定要在AndroidManifest.xml文件中注册才可以使用。不过,由于我们是使用Android Studio的快捷方式创建的BroadcastReceiver,因此注册这一步已经自动完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>

</manifest>

可以看到,`<application>`标签内出现了一个新的标签`<receiver>`,所有静态的BroadcastReceiver都是在这里进行注册的。它的用法其实和`<activity>`标签非常相似,也是通过`android:name`指定具体注册哪一个BroadcastReceiver,而`enabled`和`exported`属性则是根据我们刚才勾选的状态自动生成的。  
不过目前的BootCompleteReceiver是`无法收到开机广播`的,因为我们还需要对AndroidManifest.xml文件进行`修改`才行
4. 声明权限和action,代码如下:
```kotlin
<?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
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BroadcastTest"
        tools:targetApi="31">
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
<!--            说明使用的action-->
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

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

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

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

由于Android系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED的广播,因此我们在<receiver>标签中又添加了一个<intent-filter>标签,并在里面声明了相应的action。另外,这里有非常重要的一点需要说明。Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,必须在AndroidManifest.xml文件中进行权限声明,否则程序将会直接崩溃。比如这里接收系统的开机广播就是需要进行权限声明的,所以我们在上述代码中使用<uses-permission>标签声明了android.permission.RECEIVE_BOOT_COMPLETED权限。

Android 6.0 系统中引入了更加严格的运行时权限,从而能够更好地保证用户设备的安全和隐私。关于这部分内容我们将在第 8章中学习。

  1. 重新运行程序,现在我们的程序已经可以接收开机广播了。长按模拟器工具栏中的Power按钮,会在模拟器界面上弹出关机重启选项,如图所示
    博客第一行代码3模拟器重启选项
  2. 点击“Restart”按钮重启模拟器,在启动完成之后就会收到开机广播,如图6.6所示:
    博客第一行代码3学习开机自启效果图
    到目前为止,我们在BroadcastReceiveronReceive()方法中只是简单地使用Toast提示了一段文本信息,当你真正在项目中使用它的时候,可以在里面编写自己的逻辑。需要注意的是,不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为BroadcastReceiver中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会出现错误

发送自定义广播

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

发送标准广播

在发送广播之前,我们还是需要先定义一个BroadcastReceiver来准备接收此广播,不然发出去也是白发。

  1. 新建一个MyBroadcastReceiver类,并在onReceive()方法中加入如下代码:
    class MyBroadcastReceiver : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
         Toast.makeText(context,"在MyBroadcastReceiver接受到了信息", Toast.LENGTH_SHORT).show()
     }
    }
    
    MyBroadcastReceiver收到自定义的广播时,就会弹出“received inMyBroadcastReceiver”的提示。
  2. 然后在AndroidManifest.xml中对这个BroadcastReceiver进行修改:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>

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

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

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <meta-data
            android:name="android.app.lib_name"
            android:value="" />
    </activity>
</application>

</manifest>

可以看到,这里让`MyBroadcastReceiver`接收一条值为`com.example.broadcasttest.MY_BROADCAST`的广播,因此待会儿在发送广播的时候,我们就需要发出这样的一条广播。
3. 接下来修改`activity_main.xml`中的代码,如下所示:
```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent">
    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发送Broadcast" />
</LinearLayout>
  1. 这里在布局文件中定义了一个按钮,用于作为发送广播的触发点。然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val button = findViewById<Button>(R.id.button1)
         button.setOnClickListener {
             // 构建了一个Intent对象,并把要发送的广播的值传入
             val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
             // 调用Intent的setPackage()方法,并传入当前应用程序的包名
             intent.setPackage(packageName)
             // 调用sendBroadcast()方法将广播发送出去
             sendBroadcast(intent)
         }
     }
    }
    

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

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

  2. 现在重新运行程序,并点击“Send Broadcast”按钮,效果如图所示
    博客第一行代码自定义广播发送效果图
    这样我们就成功完成了发送自定义广播的功能。另外,由于广播是使用Intent来发送的,因此你还可以在Intent中携带一些数据传递给相应的BroadcastReceiver,这一点和Activity的用法是比较相似的。

发送有序广播

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

  1. 新建AnotherBroadcastReceiver类,代码如下所示:
    class AnotherBroadcastReceiver : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
         Toast.makeText(context,"在AnotherBroadcastReceiver接受到信息", Toast.LENGTH_SHORT).show()
     }
    }
    
  2. 然后在AndroidManifest.xml中对这个BroadcastReceiver的配置进行修改,代码如下所示:

    <?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
         android:allowBackup="true"
         android:dataExtractionRules="@xml/data_extraction_rules"
         android:fullBackupContent="@xml/backup_rules"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:theme="@style/Theme.BroadcastTest"
         tools:targetApi="31">
         <!-- 声明触发action -->
         <receiver
             android:name=".AnotherBroadcastReceiver"
             android:enabled="true"
             android:exported="true">
             <intent-filter>
                 <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
             </intent-filter>
         </receiver>
    
         <receiver
             android:name=".MyBroadcastReceiver"
             android:enabled="true"
             android:exported="true">
             <intent-filter>
                 <action android:name="com.example.broadcasttest.MY_BROADCAST" />
             </intent-filter>
         </receiver>
         <receiver
             android:name=".BootCompleteReceiver"
             android:enabled="true"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
         </receiver>
         <activity
             android:name=".MainActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
             <meta-data
                 android:name="android.app.lib_name"
                 android:value="" />
         </activity>
     </application>
    </manifest>
    

    可以看到,AnotherBroadcastReceiver同样接收的是com.example.broadcasttest.MY_BROADCAST这条广播。

  3. 现在重新运行程序,并点击“Send Broadcast”按钮,就会分别弹出两次提示信息,如图所示。
    博客第一行代码3学习自定义广播两次广播
    不过,到目前为止,程序发出的都是标准广播,现在我们来尝试一下发送有序广播。
  4. 修改MainActivity中的代码,如下所示:
    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val button = findViewById<Button>(R.id.button1)
         button.setOnClickListener {
             // 构建了一个Intent对象,并把要发送的广播的值传入
             val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
             // 调用Intent的setPackage()方法,并传入当前应用程序的包名
             intent.setPackage(packageName)
             // 每次传递一个给接受它广播的类
             sendOrderedBroadcast(intent,null)
         }
     }
    }
    
    可以看到,发送有序广播只需要改动一行代码,即将sendBroadcast()方法改成sendOrderedBroadcast()方法。sendOrderedBroadcast()方法接收两个参数:第一个参数仍然是Intent;第二个参数是一个与权限相关的字符串,这里传入null就行了。
  5. 现在重新运行程序,并点击“Send Broadcast”按钮,你会发现,两个BroadcastReceiver仍然都可以收到这条广播。
    博客第一行代码3学习自定义广播同步接受广播
    看上去好像和标准广播并没有区别嘛。不过别忘了,这个时候的BroadcastReceiver是有先后顺序的,而且前面的BroadcastReceiver还可以将广播截断,以阻止其继续传播.那么该如何设定BroadcastReceiver的先后顺序呢?当然是在注册的时候进行设定了
  6. 修改AndroidManifest.xml中的代码,如下所示:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>

    <application

     android:allowBackup="true"
     android:dataExtractionRules="@xml/data_extraction_rules"
     android:fullBackupContent="@xml/backup_rules"
     android:icon="@mipmap/ic_launcher"
     android:label="@string/app_name"
     android:roundIcon="@mipmap/ic_launcher_round"
     android:supportsRtl="true"
     android:theme="@style/Theme.BroadcastTest"
     tools:targetApi="31">
     <receiver
         android:name=".AnotherBroadcastReceiver"
         android:enabled="true"
         android:exported="true">
         <intent-filter>
             <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
         </intent-filter>
     </receiver>
     <receiver
         android:name=".MyBroadcastReceiver"
         android:enabled="true"
         android:exported="true">
    
        <intent-filter android:priority="100">
            <action android:name="com.example.broadcasttest.MY_BROADCAST" />
        </intent-filter>

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

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

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

        <meta-data
            android:name="android.app.lib_name"
            android:value="" />
    </activity>
</application>

</manifest>

可以看到,我们通过`android:priority`属性给`BroadcastReceiver`设置了优先级,`优先级比较高`的BroadcastReceiver就可以`先收到广播`。这里将MyBroadcastReceiver的优先级设成了100,以保证它一定会在AnotherBroadcastReceiver之前收到广播。
7. 既然已经获得了接收广播的优先权,那么MyBroadcastReceiver就可以选择是否允许广播继续传递了。修改`MyBroadcastReceive`r中的代码,如下所示:
```kotlin
class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Toast.makeText(context,"在MyBroadcastReceiver接受到了信息", Toast.LENGTH_SHORT).show()
        // 截断
        abortBroadcast()
    }
}

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

  1. 现在重新运行程序,并点击“Send Broadcast”按钮,你会发现只有MyBroadcastReceiver中的Toast信息能够弹出,说明这条广播经过MyBroadcastReceiver之后确实终止传递了
    博客第一行代码3学习截断广播
  2. 本小节Github

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

项目概述

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

项目实施

强制下线功能需要先关闭所有的Activity,然后回到登录界面。如果你的反应足够快,应该会想到我们在第3章的最佳实践部分已经实现过关闭所有Activity的功能了,因此这里使用同样的方案即可。

  1. 新建一个BroadcastBestPractice项目
  2. 创建一个ActivityCollector类用于管理所有的Activity,代码如下所示:
    object ActivityCollector {
     // 创建一个list来保存所有的Activity
     private val activities = ArrayList<Activity>()
    //    添加Activity
     fun addActivity(activity: Activity) {
         activities.add(activity)
     }
     //    删除Activity
     fun removeActivity(activity: Activity) {
         activities.remove(activity)
     }
     //    关闭所有Activity
     fun finishAll() {
         for (activity in activities) {
             if (!activity.isFinishing) {
                 activity.finish()
             }
         }
         activities.clear()
     }
    }
    
  3. 然后创建BaseActivity类作为所有Activity的父类,代码如下所示:
    open class BaseActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         // 在继承这个类的时候会自动添加入list
         ActivityCollector.addActivity(this)
     }
     override fun onDestroy() {
         super.onDestroy()
         // 销毁的时候就移除
         ActivityCollector.removeActivity(this)
     }
    }
    
  4. 创建一个空的LoginActivityActivity来作为登录界面,并让Android Studio帮我们自动生成相应的布局文件。然后编辑布局文件activity_login.xml,代码如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <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" />
     </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"
             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: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,用于输入密码信息;第三行是一个登录按钮。这个布局文件里用到的全部都是我们之前学过的内容,相信你理解起来应该不会费劲。
  5. 接下来修改LoginActivity中的代码,如下所示:

    // 这里要继承BaseActivity
    class LoginActivity : BaseActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_login)
    
         // 登录按钮
         val login = findViewById<Button>(R.id.login)
         // 账号控件
         val accountEdit = findViewById<EditText>(R.id.accountEdit)
         // 密码控件
         val passwordEdit = findViewById<EditText>(R.id.passwordEdit)
         // 监听点击登录按钮
         login.setOnClickListener {
             // 获得账号
             val account = accountEdit.text.toString()
             // 获得密码
             val password = passwordEdit.text.toString()
             if (account == "admin" && password == "123456"){
                 // 如果相等就创建Intent
                 val intent = Intent(this,MainActivity::class.java)
                 // 启动主视图
                 startActivity(intent)
                 // 关闭这个视图
                 finish()
             }else{
                 // 提示错误
                 Toast.makeText(this,"账号或密码错误", Toast.LENGTH_SHORT).show()
             }
         }
     }
    }
    

    这里我们模拟了一个非常简单的登录功能。首先将LoginActivity的继承结构改成继承自BaseActivity,然后在登录按钮的点击事件里对输入的账号和密码进行判断:如果账号是admin并且密码是123456,就认为登录成功并跳转到MainActivity,否则就提示用户账号或密码错误。因此,你可以将MainActivity理解成是登录成功后进入的程序主界面,这里我们并不需要在主界面提供什么花哨的功能,只需要加入强制下线功能就可以了。

  6. 修改activity_main.xml中的代码,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <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"
         android:layout_height="wrap_content"
         android:text="发送强制离线broadcast" />
    </LinearLayout>
    
    非常简单,只有一个按钮用于触发强制下线功能。
  7. 然后修改MainActivity中的代码,如下所示:

    // 一定要继承BaseActivity
    class MainActivity : BaseActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
    
         val forceOffline = findViewById<Button>(R.id.forceOffline)
         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

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

    open class BaseActivity : AppCompatActivity() {
    
     // 预定义BroadcastReceiver对象,该类用于接收和处理Context.sendBroadcast(Intent)发送的广播意图的代码的基类。
     lateinit var receiver: BroadcastReceiver
    
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         ActivityCollector.addActivity(this)
     }
     override fun onDestroy() {
         super.onDestroy()
         ActivityCollector.removeActivity(this)
     }
    
     // 被激活时触发
     override fun onResume() {
         super.onResume()
         // 初始化intent
         val intentFilter = IntentFilter()
         // 添加一个广播
         intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE")
         // 实例化广播
         receiver = ForceOfflineReceiver()
         // 注册广播
         registerReceiver(receiver,intentFilter)
     }
    
     // 暂停时激活
     override fun onPause() {
         super.onPause()
         // 卸载广播
         unregisterReceiver(receiver)
     }
    
     // 该内部类继承BroadcastReceiver
     inner class ForceOfflineReceiver : BroadcastReceiver() {
         override fun onReceive(context: Context?, intent: Intent?) {
             // 创建一个弹窗
             AlertDialog.Builder(context).apply {
                 setTitle("警告")
                 setMessage("你的账号被强制下线,请重新登录")
                 // 不让取消
                 setCancelable(false)
                 // 设置确认按钮字符为ok
                 setPositiveButton("OK"){_,_->
                     // 关闭所有Activity
                     ActivityCollector.finishAll()
                     val  i = Intent(context, LoginActivity::class.java)
                     // 启动
                     context?.startActivity(i)
                 }
                 show()
             }
         }
     }
    }
    

    首先是使用AlertDialog.Builder构建一个对话框。注意,这里一定要调用setCancelable()方法将对话框设为不可取消,否则用户按一下Back键就可以关闭对话框继续使用程序了。然后使用setPositiveButton()方法给对话框注册确定按钮,当用户点击了“OK”按钮时,就调用ActivityCollectorfinishAll()方法销毁所有Activity,并重新启动LoginActivity。再来看一下我们是怎么注册ForceOfflineReceiver这个BroadcastReceiver的。可以看到,这里重写了onResume()onPause()这两个生命周期方法,然后分别在这两个方法里注册和取消了ForceOfflineReceiver。为什么要这样写呢?之前不都是在onCreate()和onDestroy()方法里注册和取消注册BroadcastReceiver的吗?这是因为我们始终需要保证只有处于栈顶的Activity才能接收到这条强制下线广播,非栈顶的Activity不应该也没必要接收这条广播,所以写在onResume()和onPause()方法里就可以很好地解决这个问题,当一个Activity失去栈顶位置时就会自动取消BroadcastReceiver的注册。

  9. 这样的话,所有强制下线的逻辑就已经完成了,接下来我们还需要对AndroidManifest.xml文件进行修改,代码如下所示:
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>

</manifest>

这里只需要对一处代码进行修改,就是将主Activity设置为LoginActivity,而不再是MainActivity,因为你肯定不希望用户在没登录的情况下就能直接进入程序主界面吧?
10. 现在来尝试运行一下程序吧。首先会进入登录界面,并可以在这里输入账号和密码,如
![博客第一行代码3学习强制离线登录效果图](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码3学习强制离线登录效果图.gif)
这时用户将无法再对界面的任何元素进行操作,只能点击“OK”按钮,然后重新回到登录界面。这样,强制下线功能就完整地实现了。
## Kotlin课堂:高阶函数详解
### 定义高阶函数
#### 概述
高阶函数和`Lambda`的关系是密不可分的。在第2章快速入门Kotlin编程的时候,我们已经学习了Lambda编程的基础知识,并且掌握了一些与集合相关的函数式API的用法,如map、filter函数等。另外,在第3章的Kotlin课堂中,我们又学习了Kotlin的标准函数,如run、apply函数等。  

你有没有发现,这几个函数有一个共同的特点:它们都会要求我们传入一个`Lambda`表达式作为参数。像这种接收Lambda参数的函数就可以称为具有函数式编程风格的API,而如果你想要定义自己的函数式API,那就得借助高阶函数来实现了,这也是我们本节Kotlin课堂所要重点学习的内容。

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

这个定义可能有点不太好理解,一个函数怎么能接收另一个函数作为参数呢?这就涉及另外一个概念了:函数类型。我们知道,编程语言中有整型、布尔型等字段类型,而Kotlin又增加了一个`函数类型`的概念。如果我们将这种函数类型添加到一个函数的`参数声明`或者`返回值声明`当中,那么这就是一个`高阶函数`了。

#### 高级函数的基本使用
1. 高级函数基本语法
```kotlin
(String, Int) -> Unit

->左边声明接收的什么参数的,多个参数用,号隔开.如果不需要接收任何参数,使用一对()号即可.->右边表示该函数返回值的类型,如果没有任何返回值就可以使用Unit(相当于Java的void)

  1. 编写一个简单的高阶函数
    fun example(func:(String,Int)->Unit){
     func("hello",123)
    }
    
    在这个函数中的形参为一个函数,那么它就可以说是一个高阶函数.在函数内部调用就像调用一个普通函数一样,函数名(参数1,参数2)即可.

高阶函数的小应用

概述

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

例子
  1. 定义一个名为num1AndNum2()的高阶函数,并接收两个整形和一个函数类型的参数.会在函数内进行某种运算,但具体的运算方式由传入的函数决定的.新建个HigherOrderFunction.kt文件,编写如下代码:
    fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int)->Int):Int{
     val result = operation(num1,num2)
     return result
    }
    
    这是一个非常简单的高阶函数,就不解释了.
  2. 由于num1AndNum2()函数接收一个函数类型的参数,因此我们还得先定义与其函数类型相匹配的函数才行。在HigherOrderFunction.kt文件中添加如下代码:
    fun plus(num1:Int,num2:Int):Int = num1+num2
    fun minus(num1:Int,num2:Int):Int = num1-num2
    
    这两个函数的参数声明和返回值声明都和num1AndNum2()函数中的函数类型参数是完全匹配的。其中,plus()函数将两个参数相加并返回,minus()函数将两个参数相减并返回,分别对应了两种不同的运算操作。
  3. 有了上述函数之后,我们就可以调用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")
    }
    
    运算结果:
    result1 is 180
    result2 is 20
    
    注意这里调用num1AndNum2()函数的方式,第三个参数使用了::plus::minus这种写法。这是一种函数引用方式的写法,表示将plus()和minus()函数作为参数传递给num1AndNum2()函数。而由于num1AndNum2()函数中使用了传入的函数类型参数来决定具体的运算逻辑,因此这里实际上就是分别使用了plus()和minus()函数来对两个数字进行运算。

高阶函数优化

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

  1. 修改上面的代码使用Lambad表达式的写法实现,代码如下:
    fun main() {
     val num1 = 100
     val num2 = 80
     // 方式1
     val result1 = num1AndNum2(num1, # num2) { n1, n2 ->
         n1 + n2
     }
     // 方式2
     val result2 = num1AndNum2(num1, num2,{ n1, n2 ->
         n1 - n2
     })
     println("result1 is $result1")
     println("result2 is $result2")
    }
    
    Lambda表达式的语法规则我们在2.6.2小节已经学习过了,因此这段代码对于你来说应该不难理解。你会发现,Lambda表达式同样可以完整地表达一个函数的参数声明和返回值声明(Lambda表达式中的最后一行代码会自动作为返回值),但是写法却更加精简。现在你就可以将刚才定义的plus()和minus()函数删掉了,重新运行一下代码,你会发现结果是一模一样的
  2. 模仿apply,let等实现连续调用对象的多个方法.
    1. 修改HigherOrderFunction.kt文件,在其中加入如下代码:
      fun StringBuilder.build(block:StringBuilder.()->Unit):StringBuilder{
       // 这里直接调用传入的函数
       block()
       // 执行完代码返回this
       return this
      }
      
      这里我们给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是StringBuilder。注意,这个函数类型参数的声明方式和我们前面学习的语法有所不同:它在函数类型的前面加上了一个StringBuilder. 的语法结构。这是什么意思呢?其实这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个当中的。那么这里将函数类型定义到StringBuilder类当中有什么好处呢?好处就是当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply函数的实现方式。
    2. 现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。这里仍然用吃水果这个功能来举例:
       fun main() {
       val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
       // 使用方式一
       val result = StringBuilder().build {
           // 这里使用匿名函数传入逻辑代码
           append("开始吃水果了\n")
           for (fruit in list) {
               append(fruit).append("\n")
           }
           append("吃所有的水果。")
       }
       // 使用方式二
       val result2 = StringBuilder().build({
           append("开始吃水果了\n")
           for (fruit in list) {
               append(fruit).append("\n")
           }
           append("吃所有的水定义高阶果。")
       })
       println(result.toString())
      }
      
      可以看到,build函数的用法和apply函数基本上是一模一样的,只不过我们编写的build函数目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。如果想实现apply函数的这个功能,需要借助于Kotlin的泛型才行,我们将在第8章学习泛型的相关内容。

内联函数的作用

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

高阶函数的实现原理

  1. 这里仍然使用刚才编写的num1AndNum2()函数来举例,代码如下所示:
    ```kotlin
    fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    return operation(num1, num2)
    }

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代码:
2. 上面转换的Java代码如下:
原反编译代码
```Java
public static final int num1AndNum2(int num1, int num2, @NotNull Function2 operation) {
      Intrinsics.checkNotNullParameter(operation, "operation");
      return ((Number)operation.invoke(num1, num2)).intValue();
   }

   public static final void main() {
      int num1 = 100;
      int num2 = 80;
      num1AndNum2(num1, num2, (Function2)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

简化代码

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

考虑到下面这段代码进行了些许调整.在这里num1AndNum2()函数的第三个参数变成了一个Function接口,这是一种Kotlin内置的接口,里面有一个待实现的invoke()函数。而num1AndNum2()函数其实就是调用了Function接口的invoke()函数,并把num1num2参数传了进去。在调用num1AndNum2()函数的时候,之前的Lambda表达式在这里变成了Function接口的匿名类实现,然后在invoke()函数中实现了n1 + n2的逻辑,并将结果返回。这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。

内联函数

为了解决开销问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除

语法格式
  1. 内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可,如下所示:
    inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
     return operation(num1, num2)
    }
    
  2. Kotlin编译器会将内联函数中代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。如下图所示:
    博客第一行代码3学习内联函数替换过程1
  3. 接下来,再将内联函数中的全部代码替换到函数调用的地方,如图所示。
    博客第一行代码3学习内联函数替换过程2
  4. 最终的代码就被替换成了如下。
    fun main() {
     val num1 = 100
     val num2 = 80
     val result = num1+num2
    }
    
    也正是如此,内联函数才能完全消除Lambda表达式所带来的运行时开销。

noinline与crossinline

概述

如果我们在高阶函数中接收两个及以上的函数类型参数,如果加上inline关键字,编译器则会对所有Lambda表达式进行内联.
假设我们只想对其中一个Lambda进行内联,这时候就可以使用noinline关键字

noinline

  1. 使用noinline,代码如下

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

    在这个例子中,对block2使用了noinline关键字这样它就不会被内联了.
    至于为什么要使用内联函数,这是因为内联函数在编译时会被替换成代码块因此它没有真正的参数属性.非内联函数的函数参数类型可以很方便的传递给其它函数,因为它是一个真实的函数,而内联函数只允许传给另一个内联函数,这就是局限性.

  2. 内联和非内联函数还有一个重要区别,内联函数传入的Lambda表达式内部可以使用return关键字进行函数返回的,而非内联只能进行局部返回.来看下面这个例子:

    fun printStrings(str:String,block:(String) -> Unit) {
     println("printStrings开始")
     block(str)
     println("printStrings结束")
    }
    fun main() {
     println("main开始")
     val str = ""
     printStrings(str){
         println("lambda开始")
         if(it.isEmpty()) return@printStrings
         println(it)
         println("lambda结束")
     }
    }
    

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

    main开始
    printStrings开始
    lambda开始
    printStrings结束
    

    代码执行的逻辑大致如下
    博客第一行代码3学习内联函数运行步骤1
    可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。

  3. 但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:
    inline fun printStrings(str:String,block:(String) -> Unit) {
     println("printStrings开始")
     block(str)
     println("printStrings结束")
    }
    fun main() {
     println("main开始")
     val str = ""
     printStrings(str){
         println("lambda开始")
         if(it.isEmpty()) return
         println(it)
         println("lambda结束")
     }
     println("main结束")
    }
    
    现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程。
    结果如下:
    main开始
    printStrings开始
    lambda开始
    
    运行大致流程如下:
    博客第一行代码3学习内联函数运行步骤2
    可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止执行了,和我们所预期的结果一致。
  4. 将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例:
    fun runRunnable(block: () -> Unit) {
     val runnable = Runnable {
         block()
     }
     runnable.run()
    }
    
    这段代码加上inline关键字会报错,是因为RunnableLambda表达式中调用了传入的函数类型参数,而Lambda表达式在编译时被转换为匿名类的实现方式,上面的代码相当于在匿名类调用了传入函数类型参数.
    内联函数引用的Lambda表达式允许使用return关键字进行函数返回,但是我们是在匿名类中调用的函数类型参数,此时就不能进行外层函数返回的,只能对匿名类中的函数进行返回,因此报错
    总结就是:在内联高阶函数中创建了另外的Lambda匿名类的实现且在其中调用函数类型参数,则会报错

crossinline

在上面的代码中我们遇到了在内联高阶函数中Lambda表达式调用了传入的函数类型参数报错的问题.为了解决这个问题,我们可以使用crossinline关键字来解决这个问题

  1. 解决问题,代码如下:
    inline fun runRunnable(crossinline block: () -> Unit) {
     val runnable = Runnable {
         block()
     }
     runnable.run()
    }
    
    这里再函数类型参数的前面加上了crossinline声明,代码就能正常编译通过了.
  2. 前面的问题简述就是Lambda允许使用return,而高阶函数的匿名内部类又不允许使用return.crossinline关键字约定了它们都不允许使用return关键字,从而解决了问题.虽然无法使用return了,但是我们仍然可以使用用return@runRunnable的写法进行局部返回。

Git部分过于简单,不再赘述

评论