说明: 本文是郭霖《第一行代码-第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-filter
的android: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?
那个,用错了会出问题。