Android中的广播机制

说明: 本文是郭霖《第一行代码-第3版》的读书笔记

6.1 广播机制简介

Android中的广播分为两种类型:标准广播和有序广播。

标准广播:完全异步执行的广播,在广播发出后,所有的BroadcastReceiver几乎会在同一时刻接收到该广播,因此是无序的,效率会比较高

有序广播:是一种同步执行的广播,在广播发出后,同一时刻只有一个BroadcastReceiver接收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,才能继续传给其他接收者,此时的BroadcastReceiver是有先后顺序的,优先级高的先收到消息,并且前面的还可以截断正在传递的广播

同步和异步的区别:同步是阻塞模式,异步是非阻塞模式。在操作系统中,

同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;

异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

6.2 接收系统广播

Android中内置了许多系统级别的广播,我们可以在应用程序中监听这些广播来得到系统的状态信息,例如手机开机会发送一条广播,电池的电量发生变化会发送广播、系统时间改变也会发出广播,若想接收这些广播,需要使用BroadcastReceiver

我们可以通过自由地注册BroadcastReceiver的方式自由地选择要接收哪些广播,注册的方式分为两种:

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

6.2.1 动态注册监听时间变化

要创建一个BroadcastReceiver,只需新建一个类,让他继承自BroadcastReceiver并重写父类的onReceive()方法就可以了。感觉很像Qt里的信号槽的机制。触发事件然后接收。

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(p0: Context?, p1: Intent?) {
            Toast.makeText(p0, "Time has changed.", Toast.LENGTH_SHORT).show()
        }
    }
}

首先我们创建了一个IntentionFilter的实例,并给他添加了一个值为android.intent.action.TIME_TICK的action,我们的BroadcastReceiver想要监听什么样的广播就需要添加相应的action。

接下来创建一个TimeChangeReceiver的实例,然后调用registerReceiver()方法注册,动态注册的BroadcastReceiver要取消注册,可以在onDestroy()方法中调用unregisterReceiver()方法取消注册。

查看完整的系统广播列表,可以去此路径查看:<AndroidSDK>\platforms\android-32\data\broadcast_actions.txt,可以看到,系统广播大概有三百多条。

6.2.2 静态注册实现开机启动

动态注册的BroadcastReceiver必须在程序启动后才能接收广播,在程序未启动的时候也能接收广播需要用到静态注册的方式。

理论上来说动态注册能监听到的系统广播,静态注册也能听到,但是由于恶意程序利用这个机制,Android 8.0之后,所有隐式广播都不允许使用静态注册的方式来接收,所谓隐式广播,指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播都是隐式广播。但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。

特殊的系统广播列表: https://developer.android.com/guide/components/broadcast-exceptions.html

除了使用内部类的方式创建BroadcastReceiver,也可以通过Android Studio提供的快捷方式创建。

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

onReceive()方法内使用Toast弹出一段提示信息。

静态的BroadcastReceiver一定要在AndroidManifest.xml中注册才可以使用。由于我们使用快捷方式注册,这一步已经自动生成了。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.boardcasttest">

    <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.BoardcastTest">
        <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">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

注意,我们还需要添加权限声明,为了能够接收开机的action,需要在Receiver内添加intent-filter并指定action为android.intent.action.BOOT_COMPLETED.

最后,不要在onReceive()方法内添加过多的逻辑或任何耗时操作,因为BroadcastReceiver中不允许开启线程,当此方法允许较长时间而未结束时,程序就会出错。

(这个好慢,需要耐心等一会儿,启动好一会才会显示)

6.3 发送自定义广播

6.3.1 发送标准广播

首先使用静态方法定义一个BroadcastReceiver

class MyBroadcastReceiver : BroadcastReceiver() {

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

然后在AndroidManifest.xml中对Receiver添加<intent-filter>

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

    <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.BroadcastNew">
		
        <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="www"/>
            </intent-filter>
        </receiver>
		...
    </application>

</manifest>

在布局中加入一个button,在主程序中加入点击监听事件:

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

        val button: Button = findViewById(R.id.button)
        button.setOnClickListener {
            val intent = Intent()
            intent.action = "www"
            intent.setPackage(packageName)
            sendBroadcast(intent)
        }
    }
}

注意:setPackage()指定广播发给哪个应用程序,前面已经说过,静态注册的BroadcastReceiver无法接收隐式广播,所以需要调用该方法让其成为显示广播。,再调用sendBroadcast()将广播发出去。

记一下Bug

在重写方法的时候,Android Studio一般会提示TODO("Not yet implemented"), 你自己写代码的时候一定要覆盖这句啊,如果这条语句在的话是会报错的,在允许的时候会报xxx keep stoping等等。具体看报错可以在logcat窗口搜索AndroidRuntime

6.3.2 发送有序广播

再加一个AnotherBroadcastReceiver,使其也能收到"www"的自定义广播,然后点击按钮,可以弹出两个Toast。

要发送有序广播只需改动一行代码:

button.setOnClickListener {
    val intent = Intent()
    intent.action = "www"
    intent.setPackage(packageName)
    //sendBroadcast(intent)
    sendOrderedBroadcast(intent, null)
}

sendOrderedBroadcast()接收两个参数,一个是intent,另一个是与权限相关的字符串,这里填null就行。

有序广播指定顺序在注册的时候:

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

    <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.BroadcastNew">
        
        <receiver
            android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="www"/>
            </intent-filter>
        </receiver>

        <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="100">
                <action android:name="www" />
            </intent-filter>
        </receiver>

    </application>

</manifest>

通过intent-filterandroid:priority来指定优先级,值必须是一个整数,如“100”。数值越高,优先级也就越高。默认值为 0。

class MyBroadcastReceiver : BroadcastReceiver() {

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

如果在onReceive()方法内调用了abortBroadcast()方法,则会将这条广播截断,后续的接收器就收不到了。

6.4 广播的最佳实践:强制下线

不论当前在哪一个界面都强制退出,可以使用之前写的管理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()
    }
}

然后创建Base类作为所有Activity的父类:

open class BaseActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        ActivityCollector.addActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        ActivityCollector.removeActivity(this)
    }
}

再新建一个Login的登陆界面:

<?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"/>
    </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(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        login.setOnClickListener {
            val account = accountEdit.text.toString()
            val password = passwordEdit.text.toString()
            if (account == "admin" && password == "123456") {
                val intent = Intent(this, MainActivity::class.java)
                startActivity(intent)
                finish()
            } else {
                Toast.makeText(this, "invalid account or password.", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

Login界面检查账户和密码是否输入正确,正确就转进MainActivity。MainActivity只有一个Button,用于发送强制下线的广播,这里采用的是动态注册的方法。

class MainActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        forceOffLine.setOnClickListener {
            val intent = Intent("FORCE_OFFLINE")
            sendBroadcast(intent)
        }
    }
}

我们想要的效果是:点击强制下线按钮,会弹出一个对话框,点击确认键后退出所有Activity并返回Login界面,如果是静态注册的BroadcastReceiver,是无法弹出AlertDialog的,因此接收广播需要定义在BaseActivity中,保证无论在哪个Activity都能收到广播。

open class BaseActivity: AppCompatActivity() {

    lateinit var receiver: ForceOfflineReceiver

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        ActivityCollector.addActivity(this)
    }

    override fun onResume() {
        super.onResume()
        val intentFilter = IntentFilter()
        intentFilter.addAction("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, p1: 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()
            }
        }
    }
}

我们将接收广播写在了onResume()onPause()方法中。这是因为我们只需要返回栈栈顶的Activity收到广播,其他Activity没必要收。

最后还需要在AndroidManifest.xml中将Login设为启动界面就可以了。

注意:onCreate()方法有多个重载版本,这里用的是只有一个参数Bundle?那个,用错了会出问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值