1.kotlin基础: From Java To Kotlin
常量与变量
//java
String name = "niubi"; final String name = "niubi";
//kotlin
var name = "niubi" val name = "niubi"
null 声明
//java
String name= null;
//kotlin
var name:String?=null
空判断
//Java
if (text != null) {
int length = text.length();
}
//Kotlin
text?.let {
val length = text.length
}// or simply val length = text?.length
字符串拼接
//Java
String firstName = "firstName ";
String lastName = "lastName ";
String message = "My name is: "+ firstName + " " + lastName;
//kotlin
val firstName = "firstName "
val lastName = "lastName "
val message = "My name is: $firstName $lastName"
换行
//Java
String text = "First Line\n" +
"Second Line\n" +
"Third Line";
//Kotlin
val text = """
|First Line
|Second Line
|Third Line
""".trimMargin()
三元表达式
//Java
String text = x > 5 ? "x > 5" : "x <= 5";
//Kotlin
val text = if (x > 5)
"x > 5"
else "x <= 5"
操作符
//java
final int andResult = a & b;
final int orResult = a | b;
final int xorResult = a ^ b;
final int rightShift = a >> 2;
final int leftShift = a << 2;
//Kotlin
val andResult = a and b
val orResult = a or b
val xorResult = a xor b
val rightShift = a shr 2
val leftShift = a shl 2
。。。。等等
2.Kotlin 的延迟初始化: lateinit var 和 by lazy
private var name0: String //报错
private var name1: String = "xiaoming"//不报错
private var name2: String? = null //不报错
可是有的时候,我并不想声明一个类型可空的对象,而且我也没办法在对象一声明的时候就为它初始化,那么这时就需要用到 Kotlin 提供的延迟初始化。Kotlin 中有两种延迟初始化的方式:
一种是 lateinit var,一种是 by lazy。
-
lateinit var
lateinit var 只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。lateinit var 的作用:让编译期在检查时不要因为属性变量未被初始化而报错。
Kotlin 相信当开发者显式使用 lateinit var 关键字的时候,他一定也会在后面
某个合理的时机将该属性对象初始化的(然而,谁知道呢,也许他用完才想起还
没初始化)。 -
by lazy
by lazy的作用:真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。
by lazy 本身是一种属性委托。属性委托的关键字是 by。by lazy 的写法如下:
//用于属性延迟初始化val name: Int by lazy { 1 }
//用于局部变量延迟初始化
public fun foo() { val bar:String by lazy { "hello" } println(bar) }
by lazy 要求属性声明为 val,即不可变变量,在 java 中相当于被 final 修饰。这意味着该变量一旦初始化后就不允许再被修改值了(基本类型是值不能被修改,对象类型是引用不能被修改)。{}内的操作就是返回唯一一次初始化的结果。
by lazy 可以使用于类属性或者局部变量。
实现原理:(了解附加属性)
当一个属性 name 需要 by lazy 时,具体是怎么实现的:- 生成一个该属性的附加属性:name$$delegate;
- 在构造器中,将使用 lazy(()->T)创建的 Lazy 实例对象赋值给 name$$delegate;
- 当该属性被调用,即其 getter 方法被调用时返回 name$delegate.getVaule(),而
name$delegate.getVaule()方法的返回结果是对象name$delegate内部的_value属性
值,在 getVaule()第一次被调用时会将_value 进行初始化,往后都是直接将_value 的
值返回,从而实现属性值的唯一一次初始化。
3 Kotlin Tips:怎么用 Kotlin 去提高生产力(kotlin优势)
Tip1- 更简洁的字符串
kotlin除了有单个双引号的字符串,还对字符串加强,引入了三个引号,"""中可以包含换行、反斜杠等等特殊字符;同时,Kotlin中引入了字符串模版,方便字符串的拼接,可以用$符号拼接变量和表达式。注意,在kotlin中,美元符号$是特殊字符,在字符串中不能直接显示,必须经过转义,方法1是用反斜杠,方法二是${‘$’}
Tip2- Kotlin中大多数控制结构都是表达式
首先,需要弄清楚一个概念语句和表达式,然后会介绍控制结构表达式的优点:简洁
语句和表达式是什么?
表达式有值,并且能作为另一个表达式的一部分使用
语句总是包围着它的代码块中的顶层元素,并且没有自己的值
Kotlin与Java的区别
Java中,所有的控制结构都是语句,也就是控制结构都没有值
Kotlin中,除了循环(for、do和do/while)以外,大多数控制结构都是表达式(if/when等)
Example1:if语句
java中,if 是语句,没有值,必须显式的return
public int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
kotlin中,if 是表达式,不是语句,因为表达式有值,可以作为值return出去,类似于java中的三目运算符a > b ? a : b
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
上面的if中的分支最后一行语句就是该分支的值,会作为函数的返回值。这其实跟java中的三元运算符类似,
public int max2(int a, int b) {
return a > b ? a : b;
}
上面是java中的三元运算符,kotlin中if是表达式有值,完全可以替代,故kotlin中已没有三元运算符了,用if来替代。
上面的max函数还可以简化成下面的形式
fun max2(a: Int, b: Int) = if (a > b) a else b
Example2:when语句
Kotlin中的when非常强大,完全可以取代Java中的switch和if/else,同时,when也是表达式,when的每个分支的最后一行为当前分支的值
// java中的switch
public String getPoint(char grade) {
switch (grade) {
case 'A':
return "GOOD";
default:
return "UN_KNOW";
}
}
java中的switch有太多限制,我们再看看Kotlin怎样去简化的
fun getPoint(grade: Char) = when (grade) {
'A' -> "GOOD"
else -> "UN_KNOW"
}
同样的,when语句还可以取代java中的if/else if
Tip3- 更好调用的函数:显式参数名(命名参数)/默认参数值
Kotlin的函数更加好调用,主要是表现在两个方面:
1,显式的标示参数名,可以方便代码阅读;
2,函数可以有默认参数值,可以大大减少Java中的函数重载。
@JvmOverloads
在java与kotlin的混合项目中,会发现用kotlin实现的带默认参数的函数,在java中去调用的化就不能利用这个特性了。这时候可以在kotlin的函数前添加注解@JvmOverloads,添加注解后翻译为class的时候kotlin会帮你去生成多个函数实现函数重载。
Tip4-扩展函数和属性
以项目中StringExt.kt中部分代码示例:
//扩展属性:获取String最后一个字符
val String.lastChar: Char
get() = get(length - 1)
//扩展函数:字符串保留2位小数
fun String.keep2Decimal(): String {
val format = DecimalFormat()
format.maximumFractionDigits = 2
format.isGroupingUsed = false //不对数字串进行分组
return format.format(this.toDouble())
}
var name:String="123.321"
//只需import完了就跟使用自己的属性一样方便了。
//使用扩展属性
val lastChar = name.lastChar
//使用扩展函数
val keep2Decimal = name.keep2Decimal()
Kotlin为什么能实现扩展函数和属性这样的特性?
在Kotlin中要理解一些语法,只要认识到Kotlin语言最后需要编译为class字节码,Java也是编译为class执行,也就是可以大致理解为Kotlin需要转成Java一样的语法结构,Kotlin就是一种强大的语法糖而已,Java不具备的功能Kotlin也不能越界的。
上面的扩展函数转成Java后的代码
/*
* 扩展函数会转化为一个静态的函数,同时这个静态函数的第一个参数就是该类的实例对象
* */
/*
* 获取的扩展属性会转化为一个静态的get函数,同时这个静态函数的第一个参数就是该类的实例对象
* */
public static final char getLastChar(@NotNull StringBuilder $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
return $receiver.charAt($receiver.length() - 1);
}
对于扩展函数,转化为Java的时候其实就是一个静态的函数
对于扩展属性也类似,获取的扩展属性会转化为一个静态的get函数
可以看到扩展函数和扩展属性适用的地方和缺陷,有两点:
扩展函数和扩展属性内只能访问到类的公有方法和属性,私有的和protected是访问不了的
扩展函数不能被override,因为Java中它是静态的函数
Tip5- 懒初始化by lazy 和 延迟初始化lateinit
上面已有详细介绍。
Tip6- 不用再手写findViewById
利用kotlin-android-extensions插件,activity中import对应的布局即可。插件会自动根据布局的id生成对应的View成员。直接拿id用即可。如: tip6Tv.text = “XXXX”
原理是什么?插件帮我们做了什么?
在编译阶段,插件会帮我们生成视图缓存,视图由一个Hashmap结构的_$_findViewCache变量缓存,会根据对应的id先从缓存里查找,缓存没命中再去真正调用findViewById查找出来,再存在HashMap中。另外在onDestroyView会清掉缓存。
Fragment需要注意,不能在onCreateView方法里用view,不然会出现空指针异常,需要在onViewCreate里,原理是插件用了getView来findViewById.故在onViewCreated中getView还是空的,原理就好理解了。
Tip7- 利用局部函数抽取重复代码
Kotlin中提供了函数的嵌套,在函数内部还可以定义新的函数。这样我们可以在函数中嵌套这些提前的函数,来抽取重复代码。
Java写法
private void checkIsBlank(){
if (TextUtils.isEmpty(textviewOne.getText())){
throw new IllegalArgumentException("");
}
if (TextUtils.isEmpty(textviewTwo.getText())){
throw new IllegalArgumentException("");
}
if (TextUtils.isEmpty(editText.getText())){
throw new IllegalArgumentException("");
}
}
Java优化后代码
private void checkIsBlank(){
checkTextView(textviewOne);
checkTextView(textviewTwo);
checkTextView(editText);
}
private void checkTextView(TextView view){
if (TextUtils.isEmpty(view.getText())){
throw new IllegalArgumentException("");
}
}
Kotlin写法
fun checkIsBlank(){
fun checkTextView(view: TextView){
if (view.text.isNullOrBlank())
throw IllegalArgumentException("")
}
checkTextView(textviewOne)
checkTextView(textviewTwo)
checkTextView(editText)
}
Kotlin的写法和Java优化后代码相比,代码量并没有减少,那为什么我们推荐使用局部函数,而不推荐把重复代码提取成一个独立的函数呢?那是因为,在当前代码文件中,我们只有checkIsBlank一个函数使用到了这段重复的代码,别的函数并没有任何相关逻辑代码,所以使用局部函数的话,不仅让重复代码的用途和用处更明确了,函数相关性也大大提高了。
Tip8- 使用数据类来快速实现model类
//Kotlin会为类的参数自动实现get set方法
class User(val name: String, val age: Int, val gender: Int, var address: String)
//用data关键词来声明一个数据类,除了会自动实现get set,还会自动生成equals hashcode toString
data class User2(val name: String, val age: Int, val gender: Int, var address: String)
Tip9- 用类委托来快速实现装饰器模式
通过继承的实现容易导致脆弱性,例如如果需要修改其他类的一些行为,这时候Java中的一种策略是采用装饰器模式:创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个成员变量。
与原始类拥有相同行为的方法不用修改,只需要直接转发给原始类的实例。如下所示:
* 常见的装饰器模式,为了修改部分的函数,却需要实现所有的接口函数
* */
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> {
var objectAdded = 0
override val size: Int
get() = innerSet.size
/*
* 需要修改的方法
* */
override fun add(element: T): Boolean {
objectAdded++
return innerSet.add(element)
}
/*
* 需要修改的方法
* */
override fun addAll(elements: Collection<T>): Boolean {
objectAdded += elements.size
return innerSet.addAll(elements)
}
override fun contains(element: T): Boolean {
return innerSet.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return innerSet.containsAll(elements)
}
override fun isEmpty(): Boolean {
return innerSet.isEmpty()
}
override fun clear() {
innerSet.clear()
}
override fun iterator(): MutableIterator<T> {
return innerSet.iterator()
}
override fun remove(element: T): Boolean {
return innerSet.remove(element)
}
override fun removeAll(elements: Collection<T>): Boolean {
return innerSet.removeAll(elements)
}
override fun retainAll(elements: Collection<T>): Boolean {
return innerSet.retainAll(elements)
}
}`
如上所示,想要修改HashSet的某些行为函数add和addAll,需要实现MutableCollection接口的所有方法,将这些方法转发给innerSet去具体的实现。虽然只需要修改其中的两个方法,其他代码都是模版代码。
只要是重复的模版代码,Kotlin这种全新的语法糖就会想办法将它放在编译阶段再去生成。
这时候可以用到类委托by关键字,如下所示:
/*
* 通过by关键字将接口的实现委托给innerSet成员变量,需要修改的函数再去override就可以了
* */
class CountingSet2<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {
var objectAdded = 0
override fun add(element: T): Boolean {
objectAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
objectAdded += elements.size
return innerSet.addAll(elements)
}
}
通过by关键字将接口的实现委托给innerSet成员变量,需要修改的函数再去override就可以了,通过类委托将10行代码就可以实现上面接近100行的功能,简洁明了,去掉了模版代码。
Tip10- Lambda表达式简化OnClickListener
Tip11- kotlin常见内联扩展函数来简化代码
内联扩展函数之let
let扩展函数的实际上是一个作用域函数
场景一: 针对一个可null的对象统一做判空处理。
场景二: 明确一个变量所处特定的作用域范围内。
内联函数之with
适用于调用同一个类的多个方法时,可以省去类名重复,直接调用类的方法即可。经常用于Android中RecyclerView中onBinderViewHolder中,数据model的属性映射到UI上
使用前:
使用后:
内联扩展函数之run
适用于let,with函数任何场景。因为run函数是let,with两个函数结合体,准确来说它弥补了let函数在函数体内必须使用it参数替代对象,在run函数中可以像with函数一样可以省略,直接访问实例的公有属性和方法,另一方面它弥补了with函数传入对象判空问题,在run函数中可以像let函数一样做判空处理
借助上个例子,
使用前:
使用后:
内联扩展函数之apply
整体作用功能和run函数很像,唯一不同点就是它返回的值是对象本身,而run函数是一个闭包形式返回,返回的是最后一行的值。正是基于这一点差异它的适用场景稍微与run函数有点不一样。
apply一般用于一个对象实例初始化的时候,需要对对象中的属性进行赋值。或者动态inflate出一个XML的View的时候需要给View绑定数据也会用到,这种情景非常常见。特别是在我们开发中会有一些数据model向View model转化实例化的过程中需要用到。
menuAdapter = HomeMenuAdapter().apply {
bindToRecyclerView(recyclerView_menu)
setOnItemClickListener { _, _, position ->
when (position) {
3 -> ActivityManager.start(ActivateActivity::class.java)
}
}
}
内联扩展函数之also
also函数的结构实际上和let很像唯一的区别就是返回值的不一样,let是以闭包的形式返回,返回函数体内最后一行的值,如果最后一行为空就返回一个Unit类型的默认值。而also函数返回的则是传入对象的本身
适用于let函数的任何场景,also函数和let很像,只是唯一的不同点就是let函数最后的返回值是最后一行的返回值而also函数的返回值是返回当前的这个对象。一般可用于多个扩展函数链式调用
总结:(extension指是否为扩展函数)
Tip11- 高阶函数简化代码
高阶函数:以另一个函数作为参数或者返回值的函数
4 Kotlin数组和集合
4.1 kotlin数组
kotlin为数组增加了一个Array类,为元素是基本类型的数组增加了xxArray类(其中xx也就是Byte,Short, Int等基本类型)
Kotlin创建数组大致有如下两种方式:
1.使用arrayOf(), arrayOfNulls(),emptyArray()工具函数。
var model1=CommonModel()
var model2=CommonModel()
var model3=CommonModel()
//创建包含指定元素的数组,相当于java数组的静态初始化
var a= arrayOf(model1,model2,model3)
var b= intArrayOf(1,2,3)
//创建指定长度为3,元素为null的数组,相当于java数组动态初始化
var c= arrayOfNulls<Int>(3)
2.使用Array(size: Int, init:(Int) -> T)
第一个参数就是数组的大小。第二个参数是一个函数, init:(Int) -> T 代表这这个方法返回的类型是T只能有一个参数类型是Int型。Int就是该array的所对应的索引。
private fun arrInit(): (Int) -> Int = { it * 2 }
var array1 = Array<Int>(5, arrInit())
var array2 = Array<Int>(5, {it})
4.2 kotlin集合
kotlin集合类同样有两个接口派生:Collection和Map。但Kotlin的结合被分成两个大类,可变集合和不可变集合。只有可变集合才可以添加修改,删除等处理操作。不可变集合只能读取元素。
kotlin只提供了HashSet,HashMap, LinkedHashSet, LinkedHashMap, ArrayList这5个集合实现类,而且他们都是可变集合,那么说好的不可变集合呢。kotlin的不可变集合类并没有暴露出来,我们只能通过函数来创建不可变集合。
list集合
创建一个不可变的list
val mList = listOf<Int>(1, 2, 3)
创建一个可变的list
val mList = mutableListOf<Int>(1, 2, 3)
emptyList()——创建一个空集合
listOfNotNull ()—— 创建的集合中不能插入null值
Map
创建一个不可变的Map
val mList = mapOf(Pair("key1", 1), Pair("key2", 2))
或者
//推荐
val mList = mapOf("key1" to 1, "key2" to 2)
创建一个可变的Map
val mList = mutableMapOf("key1" to 1, "key2" to 2)
此外还有
emptyMap()——创建一个空map
hashMapOf()——创建一个hashMap
linkedMapOf()——创建一个linkedMap
sortedMapOf()——创建一个sortedMap
5 Kotlin集合操作符
Kotlin中关于集合的操作符有六类:
总数操作符
过滤操作符
映射操作符
顺序操作符
生产操作符
元素操作符
-
总数操作符
any —— 判断集合中 是否有满足条件 的元素;
all —— 判断集合中的元素 是否都满足条件;
none —— 判断集合中是否 都不满足条件,是则返回true;
count —— 查询集合中 满足条件 的 元素个数;
reduce —— 从 第一项到最后一项进行累计 ;
reduceRight —— 从 最后一下到第一项进行累计;
fold —— 与reduce类似,不过有初始值,而不是从0开始累计;
foldRight —— 和reduceRight类似,有初始值,不是从0开始累计;
forEach —— 循环遍历元素,元素是it,可对每个元素进行相关操作;
forEachIndexed —— 循环遍历元素,同时得到元素index(下标);
max —— 查询最大的元素,如果没有则返回null;
maxBy —— 获取方法处理后返回结果最大值对应的那个元素的初始值,如果没有则返回null;
min —— 查询最小的元素,如果没有则返回null;
minBy —— 获取方法处理后返回结果最小值对应那个元素的初始值,如果没有则返回null;
sumBy —— 获取 方法处理后返回结果值 的 总和;
dropWhile —— 返回从第一项起,去掉满足条件的元素,直到不满足条件的一项为止 -
过滤操作符
过滤后会返回一个处理后的列表结果,但不会改变原列表!!!
filter —— 过滤 掉所有 满足条件 的元素
filterNot —— 过滤所有不满足条件的元素
filterNotNull —— 过滤NULL
take —— 返回从第一个开始的n个元素
takeLast —— 返回从最后一个开始的n个元素
takeWhile —— 返回不满足条件的下标前面的所有元素的集合
drop —— 返回 去掉前N个元素后 的列表
dropLastWhile —— 返回从最后一项起,去掉满足条件的元素,直到不满足条件的一项为止
slice —— 过滤掉 非指定下标 的元素,即保留下标对应的元素过滤list中
指定下标的元素(比如这里只保留下标为1,3,4的元素) -
映射操作符
map —— 将集合中的元素通过某个 方法转换 后的结果存到一个集合中;
mapIndexed —— 除了得到 转换后的结果 ,还可以拿到Index(下标);
mapNotNull —— 执行方法 转换前过滤掉 为 NULL 的元素
flatMap —— 合并两个集合,可以在合并的时候做些小动作;
groupBy —— 将集合中的元素按照某个条件分组,返回Map; -
顺序操作符
reversed —— 相反顺序
sorted —— 自然排序(升序)
sortedBy —— 根据方法处理结果进行自然(升序)排序
sortedDescending —— 降序排序
sortedByDescending —— 根据方法处理结果进行降序排序 -
生产操作符
zip —— 两个集合按照下标组合成一个个的Pair塞到集合中返回
partition —— 根据判断条件是否成立,拆分成两个 Pair
plus —— 合并两个List,可以用"+"替代
unzip —— 将包含多个Pair的List 转换成 含List的Pair -
元素操作符
contains —— 判断集合中是否有指定元素,有返回true
elementAt —— 查找下标对应的元素,如果下标越界会抛IndexOutOfBoundsException
elementAtOrElse —— 查找下标对应元素,如果越界会根据方法返回默认值(最大下标经方法后的值)
elementAtOrNull —— 查找下标对应元素,越界会返回Null
first —— 返回符合条件的第一个元素,没有 抛NoSuchElementException
firstOrNull —— 返回符合条件的第一个元素,没有 返回null
indexOf —— 返回指定下标的元素,没有 返回-1
indexOfFirst —— 返回第一个符合条件的元素下标,没有 返回-1
indexOfLast —— 返回最后一个符合条件的元素下标,没有 返回-1
last —— 返回符合条件的最后一个元素,没有 抛NoSuchElementException
lastIndexOf —— 返回符合条件的最后一个元素,没有 返回-1
lastOrNull —— 返回符合条件的最后一个元素,没有 返回null
single —— 返回符合条件的单个元素,如有没有符合或超过一个,抛异常
singleOrNull —— 返回符合条件的单个元素,如有没有符合或超过一个,返回null
6 说一下Kotlin的伴生对象(关键字companion)
在Java中可以通过static关键字声明静态的属性或方法。但是在Kotlin中并没有延续这个关键字,而是使用伴生对象实现,在class内部声明一个companion object代码块,其内部的成员变量和方法都将被编译为静态的。
class TestStatic {
//伴生对象
companion object Factory {
val str: String = ""
fun create(): TestStatic {
println(this)
return TestStatic()
}
}
}
Factory为最终生成的静态内部类类名,通常来说Factory名字可以省略,如果省略,类名为默认的Companion。
Kotlin中的也可写静态代码块,只需在companion object中嵌套一个init代码块。
companion object {
//静态代码块
init {
val a = "adc"
}
}
注意事项
一个类中最多只能有一个companion object代码块。
伴生对象本质上就是一个静态内部类,所以它还能继承其他类。
7.Kotlin 顶层函数和属性
创建一个文件写需要的属性或方法。在其它地方直接,import 包名.函数名来导入我们将要使用的函数,然后就可以直接使用了。Kotlin中通过使用顶层函数和顶层属性帮助我们消除了Java中常见的静态工具类,使我们的代码更加整洁,值得一试。
8 协程(Coroutines=cooperation+routines)
8.1 协程是什么
Kotlin 官方文档说「本质上,协程是轻量级的线程」。
进程,线程,协程的抽象概念
- 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
- 线程是操作系统能够进行调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是在进程下,所以同一进程下的多个线程是能共享资源的。
- 协程是单线程下实现多任务,它通过 yield 关键字来实现,能有效地减少多线程之间切换的开销。它是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程基本使用
// 方法一,使用 runBlocking 顶层函数
// 通常适用于单元测试的场景,而业务开发中不会用到这种方法
runBlocking {
getImage(imageId)
}
// 方法二,使用 GlobalScope 单例对象
//和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和app一致,且不能取消
GlobalScope.launch {
getImage(imageId)
}
// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的context和Android里的不是一个东西,是一个更通用的概念,会有一个Android平台的封装来配合使用)。
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getImage(imageId)
}
协程最常用的功能是并发,而并发的典型场景就是多线程。可以使用 Dispatchers.IO 参数把任务切到 IO 线程执行:
coroutineScope.launch(Dispatchers.IO) {
...
}
也可以使用 Dispatchers.Main 参数切换到主线程:
coroutineScope.launch(Dispatchers.Main) {
...
}
异步请求的例子完整写出来是这样的:
coroutineScope.launch(Dispatchers.Main) { // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
nameTv.text = user.name // 主线程更新 UI
}
而通常用java来写,是少不了回调方法的。
协程的「1 到 0」
多层网络请求:
coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程
val token = api.getToken() // 网络请求:IO 线程
val user = api.getUser(token) // 网络请求:IO 线程
nameTv.text = user.name // 更新 UI:主线程
}
如果遇到的场景是多个网络请求需要等待所有请求结束之后再对 UI 进行更新。
比如以下两个请求:
api.getAvatar(user, callback)
api.getCompanyLogo(user, callback)
如果使用回调式的写法,本来能够并行处理的请求被强制通过串行的方式去实现,可能会导致等待时间长了一倍,也就是性能差了一倍:
api.getAvatar(user) { avatar ->
api.getCompanyLogo(user) { logo ->
show(merge(avatar, logo))
}
}
而如果使用协程,可以直接把两个并行请求写成上下两行,最后再把结果进行合
并即可:
coroutineScope.launch(Dispatchers.Main) {
val avatar = async { api.getAvatar(user) } // 获取用户头像
val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
val merged = suspendingMerge(avatar, logo) // 合并结果
show(merged) // 更新 UI
}
可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码。需要注意的是 suspendingMerge 并不是协程 API 中提供的方法,而是我们自定义的一个可「挂起」的结果合并方法。至于挂起具体是什么,可以看后面。
让复杂的并发代码,写起来变得简单且清晰,是协程的优势。
这里,两个没有相关性的后台任务,因为用了协程,被安排得明明白白,互相之间配合得很好,也就是我们之前说的「协作式任务」。本来需要回调,现在直接没有回调了,这种从 1 到 0 的设计思想真的妙哉。
8.2 suspend
suspend 是 Kotlin 协程最核心的关键字。代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。
创建协程的函数:
• launch
• runBlocking
• async
runBlocking 通常适用于单元测试的场景,而业务开发中不会用到这个函数,因为它是线程阻塞的。
接下来我们主要来对比 launch 与 async 这两个函数。
• 相同点:它们都可以用来启动一个协程,返回的都是 Coroutine,我们这里不需要纠结具体是返回哪个类。
• 不同点:async 返回的 Coroutine 多实现了 Deferred 接口。
Deferred的意思就是延迟,也就是结果稍后才能拿到。
我们调用 Deferred.await() 就可以得到结果了。
看看 async 是如何使用的:
coroutineScope.launch(Dispatchers.Main) {
val avatar: Deferred = async { api.getAvatar(user) } // 获取用户头像
val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
show(avatar.await(), logo.await()) // 更新 UI
}
可以看到 avatar 和 logo 的类型可以声明为 Deferred ,通过 await 获取结果并且更新到 UI 上显示。
await 函数签名:
public suspend fun await(): T
前面有个关键字是—— suspend
8.3 「挂起」的本质
协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?
都不对,我们挂起的对象是协程
协程可以使用 launch 或者 async 函数,协程其实就是这两个函数中闭包的代码块。launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。
那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。
注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。
suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的suspend 函数的时候,暂时不继续执行协程代码了。
互相脱离的线程和协程接下来将会发生什么事情:
举例:获取一个图片,然后显示出来:
// 主线程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 获取图片
avatarIv.setImageBitmap(image) // 显示出来
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
这段执行在主线程的协程,它实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码:
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
线程:
如果它是一个后台线程:
• 要么无事可做,被系统回收
• 要么继续执行别的后台任务
跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。
如果它是 Android 的主线程,那它接下来就会继续回去工作:也就是
一秒钟 60 次的界面刷新任务。
协程:
线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程。
谁指定的?是 suspend 函数指定的,比如我们这个例子中,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程。
Dispatchers:调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行,关于 Dispatchers 这里先不展开了。
常用的 Dispatchers ,有以下三种:
• Dispatchers.Main:Android 中的主线程
• Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
• Dispatchers.Default:适合 CPU 密集型的任务,比如计算
回到我们的协程,它从 suspend 函数开始脱离启动它的线程,继续执行在 Dispatchers 所指定的 IO 线程。在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。
结论:
协程在执行到有 suspend 标记的函数的时候,会被suspend也就是被挂起,就是切个线程;不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作。
8.4 suspend 的意义?
随便写一个自定义的 suspend 函数:
suspend fun suspendingPrint() {
println("Thread: ${Thread.currentThread().name}")
}
System.out: Thread: main
输出的结果还是在主线程。
为什么没切换线程?因为它不知道往哪切,需要我们告诉它。
对比之前例子中 suspendingGetImage 函数代码:
suspend fun suspendingGetImage(id: String) =
withContext(Dispatchers.IO) {
...
}
通过 withContext 源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher 参数,依赖这个 Dispatcher 参数的指示,你的协程被挂起,然后切到别的线程。所以这个 suspend,其实并不是起到把任何把协程挂起,或者说切换线程的作用。真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的。所以我们想要自己写一个挂起函数,仅仅只加上 suspend 关键字是不行的,还需要函数内部直接或间接地调用到 Kotlin 协程框架自带的 suspend 函数才行。
这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?
它其实是一个提醒。
函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。为什么 suspend 关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来?因为它本来就不是用来操作挂起的。挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字。
所以suspend关键字,只是一个提醒。
你创建一个 suspend 函数但它内部不包含真正的挂起逻辑,编译器会给你一个提醒:redundant suspend modifier,告诉你这个 suspend 是多余的。所以,创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数,你的这个 suspend 才是有意义的。
8.5 到底什么是「非阻塞式」挂起?协程真的更轻量级吗?
什么是「非阻塞式挂起」?
线程中的阻塞式:在单线程情况下,在单线程下执行耗时操作是会阻塞线程的,如果在多线程情况下,那么此时的线程也是非阻塞式的。
非阻塞式是相对阻塞式而言的。 Kotlin 协程在单协程的情况下也是非阻塞式 的,因为它可以利用挂起函数来切换线程。(阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。)即:协程可以用看起来阻塞的代码写出非阻塞式的操作
阻塞的本质?
首先,所有的代码本质上都是阻塞式的,而只有比较耗时的代码才会导致人类可感知的等待,比如在主线程上做一个耗时 50 ms 的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。举个例子,当你开发的 app 在性能好的手机上很流畅,在性能差的老手机上会卡顿,就是在说同一行代码执行的时间不一样。视频中讲了一个网络 IO 的例子,IO 阻塞更多是反映在「等」这件事情上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。而这跟协程半毛钱关系没有,切线程解决不了的事情,协程也解决不了。
总结:
• 协程就是切线程;
• 挂起就是可以自动切回来的切线程;
• 挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,就这么简单。