为了便于进行系统级别的消息通知,Android引入了广播消息机制,不过Android中的广播机制相当灵活。

Android中的广播机制

Android中的每个应用程序都可以对自己感兴趣的广播进行注册,以使程序收到自己关注的广播内容,这些广播可能是来自于系统的,也可能是来自于其它应用程序的。Android提供了伊奥完成的API,允许应用程序自由发送和接收广播。

广播借助Intent进行发送,借助BroadcastReceiver进行接收。

Android中的广播主要分为两种类型

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

接收系统广播

Android内置了很多系统级别的广播,用户可以在应用程序中通过监听这些广播来得到各种系统的状态信息。

动态注册监听时间变化

用户可以根据自己的实际情况,自由地注册BroadcastReceiver,这样当有相应的广播发出时,相应的BroadcastReceiver就能够收到广播,并可以在内部进行逻辑处理。注册BroadcastReceiver的方式一般有两种:

  • 在代码中注册:也称为动态注册
  • 在AndroidManifest.xml中注册:也称为静态注册

而创建BroadcastReceiver只需要新建一个类,使之继承自BroadcastReceiver,并重写父类的onReceive方法。这样当广播到来时,onReceive方法就会得到执行,具体的逻辑就可以在该方法中处理。

class MainActivity : AppCompatActivity() {

    lateinit var timeChangeReceiver:TimeChangeReceiver

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

        val intentFilter = IntentFilter()
        intentFilter.addAction("android.intent.action.TIME_TICK")
        timeChangeReceiver = TimeChangeReceiver()
        registerReceiver(timeChangeReceiver, intentFilter)
    }

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

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

比如在上面的代码中,先定义了一个内部类TimeChangeReceiver,使之继承自BroadcastReceiver,并重写onReceiver方法,以在系统时间发生变化时,onReceiver方法都会被执行,以提示当前时间发生变化。

而在onCreate方法中,首先创建了IntentFilter的实例,并添加了值为android:intent.action.TIME_TICK的action,当系统时间发生变化时,系统就会发出名为该action的广播,也就是说用户的BroadcastReceiver想要监听什么广播,就要添加对应的action。之后创建了TimeChangeReceiver的实例,然后调用registerReceiver方法进行注册,将TimeChangeReceiver的实例和IntentFilter的实例都传递进去,这样TimeChangeReceiver也就会收到值为android:intent.action.TIME_TICK的广播(系统每隔一分钟就会发出一条android:intent.action.TIME_TICK的广播),也就实现了监听系统时间变化的功能。

运行程序后的结果为:

Android demo 广播 android广播机制_Intent

同时需要注意的是,动态注册的BroadcastReceiver一定要取消注册,这里是在onDestroy方法中调用unregisterReceiver方法实现的。

完整的系统广播列表在如下路径:

Sdk\platforms\android-N\data\broadcast_actions.txt

静态注册实现开机启动

动态注册的BroadcastReceiver可以自由控制注册和注销,在灵活性方面有很大优势,但其必须在程序启动之后才能够接收广播,因为注册的逻辑是写在onCreate方法中的。而使用静态注册的方式就可以实现开机启动。

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

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

这些特殊的系统广播列表在以下路径:

https://developer.android.google.cn/guide/components/broadcast-exceptions.html

在这些特殊的系统广播中,存在一条开机广播android.intent.action.BOOT_COMPLETED,可以借助该广播实现应用程序的开机启动。

在开机的时候,应用程序肯定是没有启动的,因此该功能显然不能使用动态注册的方式来实现,而应该使用静态注册的方式来接收开机广播,然后在onReceiver方法中执行相应的逻辑,以实现开机启动的功能。

这里右击com.example.boradcasttest包(New-Other-Broadcast Reveiver):

Android demo 广播 android广播机制_sendBroadcast_02

 上边的两个复选框:

  • exported:表示是否允许该BroadcastReceiver接收本程序以外的广播
  • enabled:表示是否启用该BroadcastReceiver

然后修改BootCompleteReceiver中的代码:

class BootCompleteReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show()
    }
}

 并且,静态BroadcastReceiver一定要在AndroidManifest.xml文件中注册才能够使用。不过由于该BroadcastReceiver的创建方式,Android Studio已经在AndroidManifest.xml文件中自动完成了注册:

<?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=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true"></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>
        </activity>
    </application>

</manifest>

上面的代码中,application标签中出现了receiver标签,所有静态的BroadcastReceiver都是在这里进行注册的。

不过目前的BootCompleteReceiver是无法接收开机广播的,还需要修改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">
            <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>
        </activity>
    </application>

</manifest>

由于Android系统启动完成后会发出android.permission.RECEIVE_BOOT_COMPLETED的广播,因此需要在reveiver中的intent-filter标签中添加相对应的action。

而Android系统为了保护用户设备的安全和隐私,做出了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,必须在AndroidManifest.xml中进行权限说明,否则程序就会直接崩溃。比如这里就是用了uses-permission标签声明了android.permission.RECEIVE_BOOT_COMPLETED权限。

运行程序,长按电源键进行重启后的结果为:

Android demo 广播 android广播机制_Android demo 广播_03

 发送自定义广播

之前提到的都是接收广播,这里看一下如何发送自定义广播。

发送标准广播

在发送广播之前,需要先定义一个BroadcastReceiver来准备接收该广播。新建一个MyBroadcastReceiver:

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

当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>

        <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>
        </activity>
    </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: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)
        setContentView(R.layout.activity_main)
        
        button.setOnClickListener {
            val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
            intent.setPackage(packageName)
            sendBroadcast(intent)
        }
    }
}

上面的代码中,首先构建了Intent对象,并传入要发送的广播值,然后调用Intent的setPackage方法传入当前程序的包名。最后调用sendBroadcast方法将该广播发送出去,这样监听该广播的BroadcastReceiver就会收到该消息了,此时发出去的广播就是一条标准广播。

需要注意的是,在Android 8.0系统之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下发出的自定义广播都是隐式广播,因此这里需要调用setPackage方法,指定该广播要发送给哪个应用程序,从而使之变为一条显式广播,否则静态注册的BroadcastReceiver将无法收到该广播。

运行程序后的结果为:

Android demo 广播 android广播机制_Android demo 广播_04

 这样也就发送了自定义广播,同时还可以在Intent中携带一些数据传递给相应的BroadcastReceiver。

发送有序广播

和标准广播不同,有序广播是一种同步执行的广播,并且是可以被截断的。这里验证一下,新建AnotherBroadcastReceiver:

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

然后修改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=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </intent-filter>

        </receiver>
        <receiver
            android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </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>
        </activity>
    </application>

</manifest>

运行程序的结果为:

Android demo 广播 android广播机制_sendBroadcast_05

Android demo 广播 android广播机制_Intent_06

 也就是说,此时的该广播会被两个BroadcastReceiver接收。但是程序发出的都是标准广播,这是尝试一下发送有序广播。修改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。该方法接收两个参数,第一个参数仍是Intent,第二个参数是和权限相关的字符串,这里传入null即可。

程序运行后的结果为:

Android demo 广播 android广播机制_Broadcast_07

 

Android demo 广播 android广播机制_sendBroadcast_08

 不过这里的结果和之前并没有什么区别,而既然是有序广播,便是可以设置其接收的先后顺序的,此时需要修改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=".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=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </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>
        </activity>
    </application>

</manifest>

上面的代码只是多给MyBroadcastReceiver指定了android:priority属性,该属性对应接收有序广播时的优先级,优先级高的会先收到广播。

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

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

上面的代码就表示在MyBroadcastReceiver中截断该广播的传递,也就是说,后续的BroadcastReceiver便无法再接收到该广播。

此时,程序运行后的结果为:

Android demo 广播 android广播机制_sendBroadcast_09