本章我们介绍了广播分类(标准和有序)、接收系统广播(动态和静态注册)、标准和有序广播的用法和强制下线功能。此外,作者介绍了高阶函数、内联函数和noinline的用法。
6.1.广播机制简介
Android允许程序自由发送和接收广播,发送基于Intent,接收则需要引入BroadCastReceiver。广播有两种类型:标准广播和有序广播。前者完全异步执行,广播发出之后,所有BroadCastReceiver均同时接收到,效率高,无法被截断;后者是同步执行,广播发出之后,同一时刻只会有一个接收到,BroadCastReceiver有先后顺序,可以截断。
6.2.接收系统广播
Android内置许多系统广播,譬如手机开机完成发送一条,电池电量变化发送一条,系统时间改变会发出一条,如果需要接收,那使用BroadCastReceiver。
6.2.1.动态注册监听时间变化
注册BroadCastReceiver有两种:在代码中注册和在AndroidManifest中注册。前者称为动态注册,后者为静态注册。首先介绍动态。
新建一个类继承自BroadCastReceiver并重写onReceiver方法,广播到来时,在此写处理逻辑。在这里我们写一个动态注册监听时间变化的例子BroadCastTest。需要了解的是:系统每隔一分钟就会发出一条android.intent.action.TIME_TICK的广播。代码如下:
package com.example.myapplication
class MainActivity : AppCompatActivity() {
lateinit var timeChangeReceiver: TimeChangeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//第二步:构建IntentFilter实例并为其添加一个action,action接受的正是一条TIME_TICK广播。
val intentfilter = IntentFilter()
intentfilter.addAction("android.intent.action.TIME_TICK")
//第三步:创建TimeChangeReceiver实例,随后调用注册,这样TimeChangeReceiver也可以实现监听系统时间变化的共功能了
timeChangeReceiver = TimeChangeReceiver()
registerReceiver(timeChangeReceiver, intentfilter)
}
//第五步:解铃还须系铃人,动态注册的一定要取消注册。
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeChangeReceiver)
}
//第一步:定义内部类继承自BroadCastReceiver,当系统时间发生变化时,onReceiver方法会得到执行
inner class TimeChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
}
}
}
另外Android系统还会在亮屏息屏、电量变化和网络变化场景下发出广播。在此目录下可以获取所有系统广播的action:D:\tools\sdk\platforms\android-28\data\broadcast_actions
6.2.2.静态注册实现开机启动
动态的虽然灵活,但有一个缺点是必须程序启动之后才能接受广播,有没有什么办法可以让程序在不启动的情况下接收广播?静态注册。由于任何应用可以通过这个方法被频繁唤醒,Android自8.0后快速削弱静态注册的功能。仅有少数的系统广播仍允许以静态注册方式接受。譬如开机广播android.intent.action.BOOT_COMPLETED。
右击包名->New->Other->BroadCastReceiver,将类名命名为BootCompleteReceiver。Exported是允许接收程序之外的广播,Enabled表示是否启用,都选是。
package com.example.myapplication
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()
}
}
另外,Android在启动之后会发出一条android.intent.action.BOOT_COMPLETED广播,我们在receiver标签中添加一个
...
需要提醒一下,onReceiver中不要写过多的耗时逻辑,因为BroadCastReceiver中不允许开线程,如果长时间没有结束,会造成ANR。
6.3.发送自定义广播
广播有两种:标准和有序广播。
6.3.1.发送标准广播
新建MyBroadCastReceiver类,在onReceiver中重写:
class MyBroacastReceiver : 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()
}
}
其次,在AndroidManifese注册相应的Receiver和Intent-Filter:
随后在activity_main.xml中增加点击按钮,在MainActivity中增加相应的点击事件用于发送广播,甚至你还可以通过Intent携带一点数据给BroadCastReceiver。代码如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener{
//构建Intent对象并将要发送的广播传入
val intent = Intent("com.example.broad.my_broad")
//intent的setPackage将当前程序的包名传入,packagename是getPackageName的语法糖写法
//android8.0之后静态注册广播无法接受隐式的,这里一定要调用setPackage,指明发送给哪个app,从而使其成为显式的。
intent.setPackage(packageName)
//发送BroadCastReceiver
sendBroadcast(intent)
}
}
}
6.3.2.发送有序广播
有序广播同步执行且可以被截断。新建anotherBroadCastReceiver,代码如下:
class AnotherBroadReceiver : 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 anotherBroadCastReceiver",Toast.LENGTH_LONG).show()
}
}
接收相同的广播,写相应的xml:
再者,修改MainActivity里面的发送:
button.setOnClickListener{
//构建Intent对象并将要发送的广播传入
val intent = Intent("com.example.broad.my_broad")
//intent的setPackage将当前程序的包名传入,packagename是getPackageName的语法糖写法
//android8.0之后静态注册广播无法接受隐式的,这里一定要调用setPackage,指明发送给哪个app,从而使其成为显式的。
intent.setPackage(packageName)
//发送BroadCastReceiver,一个参数是intent,一个是与权限相关字符串
sendOrderedBroadcast(intent,null)
}
如何设定先后顺序,利用android.priority。
既然已经获得接收广播优先权,那么可以在MyBroadCastReceiver中选择是否继续传递,在这了我们选择不继续传递了,代码如下:
class MyBroacastReceiver : 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_LONG).show()
abortBroadcast()
}
}
这时AnotherBroadcastReceiver已然接收不到广播了。
6.4.实现强制下线功能
强制下线功能很常见,譬如QQ号在别处登陆了,会强制下线。首先使用ActivityController管理所有Activity,代码如下所示:
package com.example.myapplication
import android.app.Activity
/**
* 功能:创建一个ActivityController管理所有Activity,譬如增加删除销毁所有
*/
object ActivityController {
private val activities = ArrayList()
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()
}
}
其次,创建BaseActivity类作为所有Activity的父类,onCreate复写的时候注意参数个数。代码如下:
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityController.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityController.removeActivity(this)
}
}
创建LoginActivity作为登录页面,编辑布局文件activity_login.xml。
package com.example.myapplication
/**
* 功能:非常简单的登录功能,若成功,跳转至MainActivity,否则提示账号或者密码错误
*/
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
login.setOnClickListener {
val account = account_edit.text.toString()
val password = password_edit.text.toString()
if (account == "admin" && password == "123") {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "account or pass is invalid", Toast.LENGTH_SHORT).show()
}
}
}
}
修改LoginActivity确保密码账号一致后跳转至MainActivity。
package com.example.myapplication
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_login.*
/**
* 功能:非常简单的登录功能,若成功,跳转至MainActivity,否则提示账号或者密码错误
*/
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
login.setOnClickListener {
val account = account_edit.text.toString()
val password = password_edit.text.toString()
if (account == "admin" && password == "123") {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "account or pass is invalid", Toast.LENGTH_SHORT).show()
}
}
}
}
在activity_main.xml中加入按钮,点击这个按钮即可发送强制下线的广播。
/**
* 功能:强制下线功能的设计,点击后发送一个com.example.myapp.FORCE_LINE广播,下线逻辑应当写在接收广播的BroadCastReceiver中。
*/
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
force_offline.setOnClickListener {
val intent = Intent("com.example.myapp.FORCE_LINE")
sendBroadcast(intent)
}
}
}
最后为了让所有Activity都能响应广播的下线功能,我们在BaseActivity中动态注册一个广播即可。最后BaseActivity 的示例代码如下:
/**
* 功能:创建BaseActivity作为所有Activity的父类,
* 静态广播无法在onReceiver中弹出对话框,动态广播不能为每一个Activity注册动态广播,因而想起来父类BaseActivity
* 动态广播的处理逻辑:使用IntentFilter接收动态广播,注册广播,接收广播,销毁广播,接收后的处理逻辑,一气呵成、
*
*/
open class BaseActivity : AppCompatActivity() {
lateinit var receiver: ForceOffReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityController.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityController.removeActivity(this)
}
//在onPause和onResume方法中注册和取消注册,这是因为我们只需要保证只有处于栈顶的Activity才能接收到这个强制下线广播,
//当一个Activity失去栈顶位置后会自动取消BroadCastReceiver。
override fun onResume() {
super.onResume()
val intentfilter = IntentFilter()
intentfilter.addAction("com.example.myapp.FORCE_LINE")
receiver = ForceOffReceiver()
registerReceiver(receiver, intentfilter)
}
override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}
inner class ForceOffReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, p1: Intent) {
AlertDialog.Builder(context).apply {
setTitle("Warning")
setMessage("You are forced to be offline")
setCancelable(false)
setPositiveButton("OK") { _, _ ->
ActivityController.finishAll()
val i = Intent(context, LoginActivity::class.java)
context.startActivity(i)
}
show()
}
}
}
}
6.5.Kotlin之高阶函数详解
6.5.1.定义高阶函数
高阶函数与Lambda密不可分。无论是第二章的map、filter函数还是第三章的run、apply函数等,都需要传入Lambda作为参数,这样的函数被称为具有函数式编程风格的API,如果要定义自己的函数式API,需要借助高阶函数。所谓高阶函数,是指一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数。
我们知道,编程语言有整形、布尔型等字段类型,Kotlin中增加了函数类型的概念,函数类型规则如下:(String,Int)->Unit。->左边是函数接受什么参数,如果不接收参数,空括号就好了。右边是返回值的类型,Unit是没有返回值,相当于Void。
//example函数接受一个函数类型的参数,因此是高阶函数
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}
高阶函数有啥用?高阶函数允许让函数类型的参数来决定函数的执行逻辑。新建HigherOrderFunction.kt文件,编写如下代码:
//高阶函数,传入两个整型和一个函数类型的参数,
//在此过程中,我们并没有进行任何具体的运算。获取它的返回值。
fun num1andnum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
//定义与其函数类型相匹配的函数,加法运算和减法运算。
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
fun main() {
val num1 = 100
val num2 = 80
//在这里第三个使用了如此的写法,函数引用方式的写法,表明将两个函数作为参数传递给num1andnum2
//告诉num1andnum2函数使用传入的函数类型参数决定具体的运算逻辑
val result1 = num1andnum2(num1, num2, ::plus)
val result2 = num1andnum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")
}
虽然能够正常工作,但每次调用任何高阶函数还需要定义一个与其函数类型参数相匹配的函数,比较复杂。Kotlin还支持多种方式譬如Lambda表达式、匿名函数、成员引用等调用高级函数。以Lambda为例,上述代码可变为:
fun main() {
val num1 = 100
val num2 = 80
//Lambda表达式的语法结构为:{参数名1:参数类型,参数名2:参数类型->函数体}
//此时可以删除plus和minus方法
val result1 = num1andnum2(num1, num2) { n1, n2 -> n1 + n2 }
val result2 = num1andnum2(num1, num2) { n1, n2 -> n1 - n2 }
println("result1 is $result1")
println("result2 is $result2")
}
Apply函数可以为Lambda表达式提供一个指定的上下文,我们使用高阶函数模仿实现apply函数。
//为StringBuilder类定义一个build扩展函数,此函数接收函数类型参数,返回值类型为StringBuilder。
//函数声明与之前不一样的是加上了一个StringBuilder.语法结构,这是高阶函数完整语法规则,在函数类型前加上ClassName
//表明这是在哪一个类当中的。这样做的好处是调用该方法时自动拥有StringBuilder的上下文。
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
//这样吃水果时可以简化StringBuilder构建字符串的方式了,这与apply实现一模一样,只是只能用于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\n")
}
println(result.toString())
}
6.5.2.内联函数的作用
高阶函数很神奇,但其背后的原理机制得了解下,简单分析下:
fun num1andnum2_cross(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
fun main() {
val num1 = 100
val num2 = 80
//通过Lambda表达式指定对传入的两个整数求和,Kotlin最终还是要编程Java字节码的,但JAva中没有高阶函数一说
val result1 = num1andnum2_cross(num1, num2) { n1, n2 -> n1 + n2 }
}
Kotlin编译器能够将上述的高阶函数的语法转换为Java支持的语法结构,如下所示:
public class transferto {
public static void main(String[] args) {
int num1 = 100;
int num2 = 80;
int result1 = numAndNum2_j(num1, num2, new Function() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
}
public static int numAndNum2_j(int num1, int num2, Function operation) {
int result = (int) operation.invoke(num1, num2);
return result;
}
}
本质上讲,在调用num1andnum2函数时,Lambda表达式变成了Function接口的内部类实现方式,每调用一次,创建一次匿名内部类实例,会造成额外内存和性能开销。
为解决此问题,Kotlin提供了内联函数的功能,使得Lambda运行时的开销消除。只需要加入inlline关键字即可。
inline fun num1andnum2_cross(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
内联函数的原理如下:首先将Lambda表达式的代码替换到函数类型参数调用的地方;其次,在将内联函数中的全部代码替换到函数调用的地方。这样就能完全消除开销。
6.5.3.noinline
给函数加上inline,编译器会将所有引用的Lambda表达式内联,但如果只想内联其中一个,那么在前面加上noinline即可,示例代码如下:
inline fun inlineTest(block1:()->Unit,noinline block2:()->Unit){
}
内联和非内联区别:1.内联在编译时进行了代码替换,没有参数属性,内联函数类型只允许传递给另一个内联函数,非内联函数类型参数可以自由传递给其他任何函数;2.内联函数所引用的Lambda表达式可使用return关键字返回,非内联函数只能进行局部返回。