Kotlin语言

2021-12-08 fishedee 前端

0 概述

Kotlin是Android开发的官方语言了,跟Java能实现100%的互操作性,同时新增了更多的语法糖,和安全性。难度不高,值得学习。

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.3"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

在项目级别的build.gradle里面,kotlin-gradle-plugin指定的就是Kotlin版本,例如,这份配置文件中的Kotlin版本是1.5.20

1 基础

代码在这里

1.1 常量与变量

package com.example.myapplication

fun VariableTest_Go(){
    //val声明常量
    val a = 3;
    //val的不能被赋值
    //a = 2;

    //var声明变量
    var b = 4;
    b = 5;
}

val声明常量,var声明变量,同时Kotlin能自动推导类型。

1.2 函数

package com.example.myapplication

fun maxNumber(a:Int,b:Int):Int{
    if( a > b){
        return a;
    }else{
        return b
    }
}

fun maxNumber2(a:Int,b:Int):Int = Math.max(a, b)

fun minNumber(left:Int,right:Int):Int{
    if( left < right ){
        return left
    }else{
        return right
    }
}

fun FunctionTest_Go(){
    println(maxNumber(1,2));
    println(maxNumber2(2,3));
    println(minNumber(
        right = 20,
        left = 10
    ))
}

函数的写法也比较简单,注意对于单个语句的函数,可以省略括号,直接用等于号就可以了。

注意调用参数的时候,可以使用命令参数的传递方式,比较方便。这种命令的方式来传递参数也适用于构造函数。

1.3 判断控制流

package com.example.myapplication

fun IfConfidition(a:Int,b:Int):Int{
    if( a > b ){
        return a;
    }else{
        return b;
    }
}

fun IfConfidition2(a:Int,b:Int):Int{
    //if语句也可以有返回值
    var result = if( a > b ){
        a
    }else{
        b
    }
    return result;
}

fun IfConfidition3(a:Int,b:Int) = if( a> b) {
    a
}else{
    b
}

fun whenCondition(a:Int):String{
    return when(a){
        1->"One"
        2->"Two"
        else->"Other"
    }
}

fun whenConditionForType(a:Number):String{
    return when(a){
        is Int->"Int"
        is Double->"Double"
        else->"Other"
    }
}

fun whenConditionForExpression(name:String):Int{
    //when的参数为空
    return when{
        name.startsWith("Fish_")->100
        //不需要用equals来比较
        name == "Jim"->200
        else->-1
    }
}

fun ConditionTest_Go(){
    println(IfConfidition(1,2))
    println(IfConfidition2(2,3))
    println(IfConfidition3(3,4))

    println(whenCondition(2))
    println(whenConditionForType(1.1))
    println(whenConditionForExpression("Fish_gg"))
}

重点如下:

  • if语句有值的,而且默认if分支的最后一个语句作为if的值
  • when语句,可以带括号,同时能进行值判断,和类型判断。没有括号情况下,相当于多个if语句

1.4 循环控制流

package com.example.myapplication

fun whileLoop():String{
    var out = ""
    var a = 0
    while( a <= 10 ){
        out = out+a+" "
        a++
    }
    return out
}

fun forLoop():String{
    var out = ""
    //左闭右闭区间,只能升序
    for( i in 0..10){
        out = out + i +" "
    }
    return out
}

fun forLoop2():String{
    var out = ""
    //左闭右开区间,只能升序
    for( i in 0 until 10 ){
        out = out + i +" "
    }
    return out
}


fun forLoop3():String{
    var out = ""
    //左闭右开区间,升序,且设置步长
    for( i in 0 until 10 step 2 ){
        out = out + i +" "
    }
    return out
}

fun forLoop4():String{
    var out = ""
    //左闭右闭区间,只能降序
    for( i in 10 downTo 1 ){
        out = out + i +" "
    }
    return out
}



fun LoopTest_Go(){
    println(whileLoop())
    println(forLoop())
    println(forLoop2())
    println(forLoop3())
    println(forLoop4())
}

重点如下:

  • while语句跟Java写法都是一样的。
  • for语句,有升序(默认右闭,右开的时候用util),降序(默认右闭),和步长(step)的三种区别写法,要注意区别。

1.5 非空安全性

package com.example.myapplication

class Person(var name:String,var age:Int){

}

//参数类型为Person的时候,就是排除了null的这种情况
fun go1(a:Person){
    println("person.name = ${a.name}")
}

//参数类型为Person?的时候,才能包含有null的这种情况
fun go2(a:Person?){
    //这种情况下,需要先判断null,再获取字段
    if( a != null){
        println("person.name2 = ${a.name}")
    }

    //kotlin有省事的?判断操作符
    println("person.name2 = ${a?.name}")
}

fun go3(a:Person?){
    //这种情况下,需要先判断null,再获取字段,否则取默认值
    var result = "";
    if( a != null){
        result = a.name;
    }else{
        result = "[null]";
    }
    println("person.name3 = ${result}")

    //kotlin有省事的?判断操作符,再结合省事的?:二元操作符来表达
    println("person.name3 = ${a?.name?:"[null]"}")
}

fun go4(a:Person?){
    //!!是一个危险的操作,强行指定这个变量是非空的,出了问题由开发者自己兜底
    println("person.name4 = ${a!!.name}")
}
fun OptionTest_Go(){
    //编译不通过
    //go1(null)
    go1(Person("fish",123))

    go2(null)
    go2(Person("fish",123))


    go3(null)
    go3(Person("fish",123))

    go4(Person("fish",123))
    go4(null)//这里能编译通过,但是运行时抛出NullPointerException

}

非空安全性是Kotlin里面的一个重要特性,Java在这点确实做得不好。重点如下:

  • 没有问号的类型,就是明确的非空类型。
  • 含有问号的类型,使用的时候需要用?先判断,另外有搭配的二元操作符取默认值。还有双!号的强行指定操作。

这些设计和TS其实都很像,没啥好说的,就是二元操作符?:有点意思而已。

1.6 lateinit

package com.example.myapplication

class MyPerson{
    //由开发者来确定它是否已经初始化了
    //编译时,编译器都认为它是非空的
    private lateinit var target:String;

    fun isInit():Boolean{
        return ::target.isInitialized
    }
    fun get():String{
        return target;
    }

    fun set(a:String){
        this.target = a;
    }
}

fun LateInitTest_Go(){
    var person = MyPerson()
    println("hasInit ${person.isInit()}")
    person.set("fish")
    println(person.get())
    println("hasInit ${person.isInit()}")

    var person2 = MyPerson()
    println("hasInit ${person2.isInit()}")
    //这里运行时会报错,因为读取对象的时候,发现对象还没有被初始化
    //lateinit property target has not been initialized
    println(person2.get())
}

lateinit,是一个非空安全性的折中设计。有时候,是需要动态确定该变量是否已经初始化的,我们又不希望编译器来插手告诉我们这个变量初始化了没有。这个时候就用lateinit关键字就可以了。

通过反编译代码,我们得到一个简单的原理,对于lateinit的变量,在每次进行get操作的时候,kotlin都会进行判断null的操作。

比起NullPointerException的做法,这种方式更安全。因为NullPointerException,只会在取到String的方法的时候才会报错。而这种方法是,取target成员的时候就报错,报错的时机提前了。

1.7 枚举

package com.example.myapplication

enum class Color {
    RED,
    GREEN,
    BLUE,
}

fun enumTest1(){
    val output = {input:Color->
        println("name ${input.name} ordinal ${input.ordinal}")
    }
    output(Color.RED)
    output(Color.GREEN)
    output(Color.BLUE)
}


enum class Color2(val index:String,val aliasName:String) {
    RED("#FF0000","红色"),
    GREEN("#00FF00","绿色"),
    BLUE("#0000FF","蓝色"),
}

fun enumTest2(){
    val output = {input:Color2->
        println("name ${input.name} ordinal ${input.ordinal} index ${input.index} alias ${input.aliasName}")
    }
    output(Color2.RED)
    output(Color2.GREEN)
    output(Color2.BLUE)
}

fun EnumTest_Go(){
    enumTest1()
    enumTest2()
}

Kotlin中的枚举类型,可以带参数,和继承接口,和Java也很相似。

2 类

代码看这里

2.1 基础

package com.example.myapplication

//默认是不可以被继承的
class Person {

    //默认成员为public,自动生成对应的getter与setter
    var name = ""
    var age = 0

    //私有化setter
    var height:Float = 0f
        private set

    //自定义getter与setter
    var color = ""
        get(){
            println("color getter run")
            return field
        }
        set(value) {
            println("color setter run")
            field = value
        }

    fun eat(){
        println(name +" is eating. He is "+ age +" years old")
    }

    fun setHeight(height:Float){
        this.height = height
    }
}

fun ClassTest_Go(){
    val p = Person()
    p.age = 121
    p.name = "Fish"
    //失败,因为setter被设置为private
    //p.height = "123"
    //必须通过指定方法
    p.setHeight(123f)
    p.color = "blue"
    println("age = ${p.age} and name = ${p.name} and height = ${p.height} and color = ${p.color}")
    p.eat()
}

基础用法,没啥好说的。

默认情况下,对于public成员变量,Kotlin会自动生成getter与setter方法。对于private成员变量,Kotlin不会自动生成getter与setter方法。

另外,我们可以通过private set或者set(){},等方式,将自动生成的getter与setter重新设置权限,我们甚至可以修改getter与setter的实现。

2.2 初始化

package com.example.myapplication

class Animal(private var name:String){
    var nameLength:Int;

    init{
        //类实例时初始化的方法
        nameLength = name.length;
    }

    fun show(){
        println("name ${name} nameLength ${nameLength}")
    }
}
fun ClassInitTest_Go(){
    val animal = Animal("Dog");
    animal.show()
}

声明类里面的括号,称为主构造函数,该构造函数里面的var和val注解的变量,会自动赋值到成员变量中,这点和TS也很像。

如果在主构造函数以外,还需要额外的初始化逻辑的,就需要一个init代码块

2.3 继承与构造函数

package com.example.myapplication

//案例1
//要想让类继承,必须有open关键字
//无参数的Person2,默认有一个空参数的主构造函数
open class Person2{

}

//Student也没有参数,所以它也有一个空参数的主构造函数
//因此必须显式地调用Person2的空参数主构造函数
class Student2: Person2(){

}

//案例2
//主构造函数上的参数,默认会成为类的成员变量。
//而且默认这些成员为final,不能被覆写
open class Person3(val name:String,val age:Int){
    fun show(){
        println("name ${this.name} age ${this.age}")
    }
}

//Student有一个主构造函数,需要显式地调用父类的主构造函数
class Student3(val clazz:String, name2:String, age2:Int): Person3(name2,age2){
    fun showClazz(){
        println("clazz ${this.clazz}");
        //没有name2的成员,因为主构造函数上面没有val,或者var注解
        //println("name: ${this.name2} age: ${this.age2}")
    }
}

//案例3
// 可以被覆写的成员,需要加入open关键字
open class Person4( open val name:String,open val age:Int){
    fun show(){
        println("name ${this.name} age ${this.age}")
    }
}

//对于覆写的成员,也需要指定override关键字
class Student4(val clazz:String, override val name:String, override val age:Int): Person4(name,age){
    fun showClazz(){
        println("clazz ${this.clazz} name: ${this.name} age: ${this.age}")
    }
}

//案例4
//Person5有主构造函数
open class Person5( val name:String, val age:Int){
    fun show(){
        println("name ${this.name} age ${this.age}")
    }
}

//Student5有主构造函数,也有次构造函数
//每个类仅仅只能有一个主构造函数,次构造函数可以是多个
//对于每个次构造函数,它们都需要显式地调用自身的主构造函数
//次构造函数上面的参数不会成为成员。
class Student5(val clazz:String,val name2:String, val age2:Int): Person5(name2,age2){
    constructor(name:String,age:Int):this("go",name,age){

    }

    constructor(age:Int):this("go","fish",age){

    }

    fun showClazz(){
        println("clazz ${this.clazz} name: ${this.name} age: ${this.age}")
    }
}

//案例5
//Person6有主构造函数
open class Person6( val name:String, val age:Int){
    fun show(){
        println("name ${this.name} age ${this.age}")
    }
}

//相当特殊的情况
//Student6没有主构造函数,只有次构造函数的时候,需要显式地调用父类的主构造函数
//这个时候省略了括号
class Student6: Person6{
    var clazz:String = ""

    constructor(clazz:String,name:String,age:Int):super(name,age){
        this.clazz = clazz
    }

    constructor(clazz:String,age:Int):super("fish",age){
        this.clazz = clazz
    }

    fun showClazz(){
        println("clazz ${this.clazz} name ${this.name} age ${this.age}")
    }
}

//案例6
//Person7有主构造函数,没有次构造函数
open class Person7{
    var name:String = "";
    var age:Int = 0;
    constructor( name:String, age:Int){
        this.name = name;
        this.age =age;
    }
    constructor( age:Int){
        this.name = "fish2";
        this.age =age;
    }
    fun show(){
        println("name ${this.name} age ${this.age}")
    }
}

//相当特殊的情况
//Student7没有主构造函数
class Student7: Person7{
    var clazz:String = ""

    constructor(clazz:String,name:String,age:Int):super(name,age){
        this.clazz = clazz
    }

    constructor(clazz:String,age:Int):super(age){
        this.clazz = clazz
    }

    fun showClazz(){
        println("clazz ${this.clazz} name ${this.name} age ${this.age}")
    }
}

fun ExtendAndConstructorTest_Go(){
    val student2 = Student2();

    val student3 = Student3("student_clazz3","fish3",123);
    student3.show();
    student3.showClazz();

    val student4 = Student4("student_clazz4","fish4",124);
    student4.show();
    student4.showClazz();

    val student5 = Student5("fish5",125);
    student5.show();
    student5.showClazz();

    val student5_2 = Student5("fish5",25);
    student5_2.show();
    student5_2.showClazz();

    val student6 = Student6("student_clazz6",1);
    student6.show();
    student6.showClazz();

    val student7 = Student7("student_clazz7",1);
    student7.show();
    student7.showClazz();
}

总共有6个案例,描述主构造函数与次构造函数各种情况,重点是:

  • Kotlin的类默认为final,不能被继承的,除非你明确地指定为open。
  • Kotlin的主构造函数除了构造效果以外,还有自动赋值到成员变量的能力(需要加上var,或者val关键字)
  • Kotlin的次构造函数,没有自动赋值到成员变量的能力
  • Kotlin的主构造函数存在时,次构造函数都需要用this方法来调用自身的主构造函数
  • Kotlin的主构造函数不存在时,次构造函数都需要用super方法来调用父类的主或者次构造函数,这时候继承时没有括号,因为自身没有主构造函数
  • Kotlin的成员变量默认为final的,需要显式地用open关键字来指定。子类覆写成员的时候,也需要显式的override关键字

2.4 接口

package com.example.myapplication

interface Flyer{
    fun fly()
}

class Bird:Flyer{
    override fun fly(){
        println("brid fly")
    }
}

fun InterfaceTest_Go(){
    val bird = Bird()
    bird.fly()
}

接口也没啥好说,注意加上override关键字就可以了

2.5 权限

package com.example.myapplication

class MyClass{
    //私有权限,仅当前类可以看到
    private var a = 1

    //保护权限,当前类,子类可以看到
    protected var b = 3

    //internal权限,仅同一个模块中类可以看到,(与Java的不同)
    internal var d:Boolean = true

    //公开权限,所有类可以看到,默认方式
    public val c:String = "123"
}

fun AccessTest_Go(){
    var myClass = MyClass()
    println("c = ${myClass.c} d= ${myClass.d}")
}

权限也没啥好说的,Kotlin废弃了包权限,而采用模块权限,这个更改更为合理

2.6 data类

package com.example.myapplication

//默认的data class没有空构造函数,通过默认参数的方式,给与空构造函数
data class Phone(val brand:String = "",val price:Double = 0.0) {

}

fun DataClassTest_Go(){
    var a = Phone("MOTO",1.1)
    var b = Phone()
    var c = Phone("MOTO",1.1)

    //data class默认有equals方法
    println("a == c 为 "+(a==c))
    println("a == b 为 "+(a==b))

    //data class默认有toString方法
    println("a.toString "+ a.toString())

    //data class默认有hashCode方法
    println("a.hasCode "+ a.hashCode())

}

写Java的时候最麻烦就是各种DTO,需要补上getter和setter方法,还有equals和hashCode等方法,所以我们才需要用loombook插件。在kotlin,我们只需要加上一个data关键字就可以了。

另外,在Kotlin中,值比较不再需要用equals,而是简单地使用==就可以了

2.7 object类

package com.example.myapplication

//注意没有class关键字,object表示是单例方式的类
//Kotlin会为这个类型自动生成一个唯一的伴生对象,该对象就是单例
object Single {
    private var count = 1;
    fun inc(){
        count ++;
    }
    fun get():Int{
        return count;
    }
}

fun SingletonClassTest_Go(){
    println("count ${Single.get()}")
    Single.inc()
    println("count ${Single.get()}")
}

Java中单例类,Kotlin对应的就是一个简单的object关键字而已,不再需要考虑并发单例的各种问题。

2.8 companion类

package com.example.myapplication

class Util {
    fun doAction1(){
        println("do action1")
    }
    companion object{
        //伴生对象,一个具体的实例,不是Java的static方法
        fun doAction2(){
            println("do Action2")
        }

        //与Java一样的static方法,以方便与Java对象进行交互
        @JvmStatic
        fun doAction3(){
            println("do Action3")
        }
    }
}

fun CompaionClassTest_Go(){
    var util = Util()
    util.doAction1()

    Util.doAction2()
    Util.doAction3()
}

对于Java的静态方法,Kotlin的对应方法是伴生对象。但是有所不同,Kotlin的伴生对象是一个确定的实例的方法,而不是Java的类的全局静态方法。

2.9 sealed类

package com.example.myapplication

import java.lang.Exception

sealed class Result{
    private val age:Int = 12
}

//有括号,是一个类
class Success:Result(){}

class Fail:Result(){}

interface Result2

//没有括号,是一个接口
class Success2:Result2{}

class Fail2:Result2{}

fun showResult2(input:Result2) = when(input){
    is Success2->println("success")
    is Fail2->println("fail")
    //普通interface类型的时候,缺少了else操作会报错,因为存在非Success2或者非Fail2的情况。
    // 因为,其他代码文件中可能存在实现Result2接口的类
    else ->throw Exception("123")
}

fun showResult(input:Result) = when(input){
    is Success->println("success")
    is Fail->println("fail")
    //sealed类型,因为sealed class规定所有继承sealed class的其他类必须在同一个代码文件中,因此不需要else操作
}

fun SealedClassTest_Go(){
    var result2:Result2 = Success2()

    showResult2(result2)

    var result:Result = Success()

    showResult(result)
}

密封类也是一种语法糖,强行要求所有实现sealed class的类都在同一个代码文件中,因此Kotlin中编译的时候就能确信,该sealed class的所有可能性有哪些。当新增一个继承sealed class的类的时候,它也会自动算出来从而报错。

一个让枚举常量安全性更高的语法糖设计

2.10 inner类

package com.example.myapplication

class Garage(val address:String) {
    inner class Car(var name:String){
        fun getGarageAddress():String{
            return address
        }
    }

    fun createCar():Car{
        return Car("fish")
    }
}

class Garage2(val address:String) {
    class Car2(var name:String){
        fun getGarageAddress():String{
            //非内部类无法获取外部类的成员
            //return address
            return ""
        }
    }

    fun createCar():Car2{
        return Car2("fish")
    }
}

fun InnerClassTest_Go(){
    val garage = Garage("a")
    val car1 = garage.createCar()
    println(car1.getGarageAddress())

    //内部类无法在外部类之外创建,因为它需要获取外部类的引用
    //var car1_out = Garage.Car("kk")
    //内部类,需要用外部类引用才能创建,看一下和上一个语句区别
    var car1_out = garage.Car("fish")

    var garage2 = Garage2("b")
    var car2 = garage2.createCar()

    //无法获取address,因为是普通的嵌套类
    println(car2.getGarageAddress())

    //嵌套类,可以仅仅通过外部类名称就能创建
    var car2_out = Garage2.Car2("kk")
    //嵌套类,无需用外部类引用来创建
    //var car3_out = garage2.Car2("fish")
}

重点如下:

  • Kotlin默认的是嵌套类,内部类需要显式加入inner关键字。Java的话刚好相反。
  • 内部类,创建的时候需要指定外部类实例,因此也能随意获取到外部类的成员。嵌套类就是反过来,创建的时候不需要指定外部类实例,也无法获取到外部类的成员。

2.11 object匿名类

package com.example.myapplication

//注意,这里的接口有个fun关键字,相当于Java里面的@FunctionalInterface注解
fun interface MyAnimal{
    fun call():String
}

fun goAnimal(animal:MyAnimal){
    println("animal call [${animal.call()}]")
}

class MyDog:MyAnimal{
    override fun call(): String {
        return "dog call"
    }
}

fun AnonymousClassTest_Go(){
    //明确地创建一个类
    goAnimal(MyDog())

    //匿名类
    goAnimal(object :MyAnimal{
        override fun call():String{
            return "Cat call"
        }
    })

    //可以用闭包传入参数,但是需要interface声明时有fun关键字才行
    goAnimal{
        "Closure Call"
    }
}

Kotlin中的匿名类写法也是比较简洁的,一个object关键字就可以了。另外,我们可以用fun接口的方式,定义参数,这样匿名类和闭包都能传递进去

2.12 by委托

委托是组合和继承之类,复用逻辑的另外一种方法。Kotlin对此有完善的支持。

2.12.1 委托类

package com.example.myapplication

//我们声明为可继承的类
open class QAnimal(protected var name:String){
    fun callName(){
        println("call ${this.name}")
    }

    fun walk(){
        println("walk")
    }
}

//QDog可以通过继承的方式,复用QAnimal的行为逻辑
class QDog:QAnimal("Dog"){
    fun run(){
        println("Dog run")

        //继承的问题在于,强约束
        //子类可以修改父类的变量,而且每个类只能有一个父类
        this.name = "xxx"
    }
}

//QAnimal2是不可被继承的
class QAnimal2(var name:String){
    fun callName(){
        println("call ${this.name}")
    }

    fun walk(){
        println("walk")
    }
}

//QDog2复用QAnimal2的另外一种方式是,组合,创建一个内部的Animal类
//然后将QAnimal2的接口手动复制过来,转发给QAnimal2
class QDog2{
    private val myAnimal:QAnimal2 = QAnimal2("Dog2")

    fun callName(){
        myAnimal.callName()
    }

    fun walk(){
        myAnimal.walk()
    }

    fun run(){
        println("Dog run")

        //组合更加灵活,我们可以只转发一部分接口
        //另外,一个类允许组合多个类的行为,没有继承带来的强约束问题
        //使用这种方法的麻烦在于,当QAnimal2后续新增了行为以后,QDog2都要手动添加对应接口来转发,比较麻烦
        //this.name = "xxx"
    }
}

interface QAnimal3Interface{
    fun callName()
    fun walk()
}

//QAnimal3是不可被继承的
class QAnimal3(var name:String):QAnimal3Interface{
    override fun callName(){
        println("call ${this.name}")
    }

    override fun walk(){
        println("walk")
    }
}

//QDog3使用委托机制来实现组合的灵活性,同时避免组合需要手动添加接口的麻烦。
class QDog3:QAnimal3Interface by QAnimal3("Dog3") {

    fun run(){
        println("Dog run")

        //这个时候,by委托当Dog的接口新增的时候,不需要更改。
    }
}



fun ByClassTest_Go(){
    val dog1 = QDog()
    dog1.walk()
    dog1.run()
    dog1.callName()

    val dog2 = QDog2()
    dog2.walk()
    dog2.run()
    dog2.callName()

    val dog3 = QDog3()
    dog3.walk()
    dog3.run()
    dog3.callName()
}

在类上使用by委托,我们既能规避继承复用代码的问题,而且能很好地复用被委托类的行为。

2.12.2 委托属性

package com.example.myapplication

import kotlin.reflect.KProperty

class ReadOnlyPropertyDelegate{
    operator fun getValue(thisRef:Any?,property:KProperty<*>):String{
        return "${thisRef?.javaClass}, thank you for delegating '${property.name}' to me!"
    }
}

class WritePropertyDelegate{
    operator fun getValue(thisRef:Any?,property:KProperty<*>):String{
        return "${thisRef?.javaClass}, thank you for delegating '${property.name}' to me!"
    }
    operator fun setValue(thisRef:Any?,property:KProperty<*>,value:String){
        println("${value} has been assigned to '${property.name}' in ${thisRef?.javaClass}")
    }
}

//属性的by委托,就是将属性的读写操作委托到另外一个类去
class ByPropertyExample{
    //val声明,只读
    val str:String by ReadOnlyPropertyDelegate()

    //var声明,可读写
    var str2:String by WritePropertyDelegate()
}

fun ByPropertyTest_Go(){
    val example = ByPropertyExample()

    println(example.str)
    println(example.str2)
    example.str2 = "789"
}

委托属性,相当于Javascript的defineProperty,对类特定的属性的读写操作进行监控。

2.12.3 内置委托

package com.example.myapplication

import kotlin.properties.Delegates

class example2 {
    //lazy,惰性获取变量,变量只能是只读的
    //该方法是线程安全的
    val no:Int by lazy {
        println("begin get no")
        200
    }

    //observable,可观察数据,首个参数的默认值,闭包是修改时候触发的
    var name: String by Delegates.observable("wang", {
        kProperty, oldName, newName ->
        println("kProperty:${kProperty.name} | oldName:$oldName | newName:$newName")
    })

    //notNull,与lateinit的效果一样,只是支持基元类型
    var instance:Int by Delegates.notNull()
}
fun LazyByTest(){
    val a = example2()
    println("after init a")
    println(a)
    println("get a")
    println(a.no)
    println(a.no)
}

fun ObservableByTest(){
    val a = example2()
    println("after init a")
    println(a)
    println("get a")
    println(a.name)
    a.name = "bb"
    a.name = "cc"
}

fun NotNullByTest(){
    val a = example2()
    //这里会产生运行是报错
    //println(a.instance)

    a.instance = 123
    println(a.instance)
}

fun BuiltInByPropertyTest_Go(){
    LazyByTest()
    ObservableByTest()
    NotNullByTest()
}

Kotlin内置的委托属性方法有:

  • lazy,延迟初始化属性
  • observable,获得属性设置通知
  • notNull,相当于lateinit

3 Lambda

代码在这里

3.1 集合操作

package com.example.myapplication

fun ListTest(){
    var a = listOf("a","b")

    //listOf 创建的是不可变集合,所以不能被Add
    //a.add("c")

    //读取下标
    println(a[0])

    //遍历list,不需要声明single
    for( single in a ){
        println(single)
    }
}

fun ListTest2(){
    var b = mutableListOf("a","b")

    //可修改集合
    b.add("c")

    //读取下标
    println(b[0])

    //遍历list
    for( single in b ){
        println(single)
    }
}

fun MapTest(){
    var a = mapOf("A" to 1,"B" to 2)

    //不可变的Map,不能被修改
    //a["B"] = 3;

    //读取下标
    println(a["A"])
    println(a["C"])//不存在的时候返回null

    //遍历map
    for( (key,value) in a ){
        println("key is ${key} value is ${value}")
    }
}

fun MapTest2(){
    var a = mutableMapOf("A" to 1,"B" to 2)

    //不变的Map,能被修改
    a["B"] = 3;

    //读取下标
    println(a["A"])
    println(a["C"])

    //遍历map
    for( (key,value) in a ){
        println("key is ${key} value is ${value}")
    }
}

fun CollectionTest_Go(){
    ListTest()
    ListTest2()
    MapTest()
    MapTest2()
}

重点为:

  • 用mapOf,setOf,listOf生成的都是不可变集合,只有用对应的mutableMapOf,mutableListOf,mutableListOf才能创建可变集合
  • 集合的遍历有更简单的for in工具。在Java中,那是for +冒号的工具,语法有所改变而已,含义一样。
  • 集合可以用下标操作了,不再需要用明确的get set方法。

3.2 Lambda基础

package com.example.myapplication

fun BasicLambdaTest_Go(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape")

    //lambda的基础写法
    val r1 = list.map({
        fruit:String->fruit.length
    })
    println("r1 is ${r1}")


    //lambda作为函数的最后一个参数的时候,可以移出到括号外面
    val r2 = list.map(){
            fruit:String->fruit.length
    }
    println("r1 is ${r2}")

    //map函数的参数为空的时候,可以省略括号
    val r3 = list.map{
            fruit:String->fruit.length
    }
    println("r1 is ${r3}")

    //可以省略lambda的参数类型,由kotlin来自己推导
    val r4 = list.map{
            fruit->fruit.length
    }
    println("r1 is ${r4}")

    //使用it关键字,来代表lambda的唯一参数的情况,这样能避免尖括号
    val r5 = list.map{
            it.length
    }
    println("r1 is ${r5}")
}

重点有:

  • lambda参数可以放在函数调用的外面,用一个单独的{}包围
  • 使用it参数来简化单参数的Lambda的编写

(Kotlin对于Lambda的语法糖特别执着,毕竟UI开发有很多闭包)

3.3 内置lambda操作

package com.example.myapplication

import java.io.BufferedOutputStream
import java.io.FileOutputStream

data class Person(private var name:String,private var age:Int){
    fun incAge(){
        this.age = this.age+1
    }

    fun getAge():Int{
        return this.age
    }
}

fun let_Go(){

    val a = Person("fish",123)
    //let 关键字,相当于js的with,只是需要明确指定一个参数可以
    a.let{
            person->
        println("age is ${person.getAge()}")
        println("age is ${person.getAge()}")
    }

    //等效于以上的效果,用it关键字能更省事一点
    a.let{
        println("age is ${it.getAge()}")
        println("age is ${it.getAge()}")
    }
}

fun with_Go(){
    var a = Person("fish",123)
    //let 关键字,与js的with,完全等价,可以直接调用对象的方法和变量。
    // 另外,最后一个语句的值,作为整个with语句的结果。
    var newAge = with(a){
        incAge()
        incAge()
        getAge()
    }
    println("new Age ${newAge}")
}

fun run_Go(){
    var a = Person("fish",123)
    //与let关键字等价,语法不同而已
    var newAge = a.run{
        incAge()
        incAge()
        getAge()
    }
    println("new Age ${newAge}")
}

fun apply_Go(){
    var a = Person("fish",123)
    //与let关键字相似,返回值是对象自身,而不是最后一个语句的值
    var newA = a.apply{
        incAge()
        incAge()
        getAge()
    }
    println("new Age ${newA}")
}

fun use_Go(){
    val fo = FileOutputStream("data")
    val bfo = BufferedOutputStream(fo)
    //外层流关闭的时候,内层流就会关闭
    //use会在闭包结束的时候自动调用close
    bfo.use {
        bfo.write("Hello".toByteArray())
    }
    bfo.close()
}


fun BuiltInLambdaTest_Go(){
    let_Go()
    with_Go()
    run_Go()
    apply_Go()
    use_Go()
}

我们最常用的四个闭包操作:

  • let,闭包里面需要指定object,let操作符没有返回值
  • with,闭包里面不需要指定object,直接指定方法就行。最后一行为返回值
  • run,闭包里面不需要指定object,直接指定方法就行。最后一行为返回值。与with完全等价,写法不同而已
  • apply,闭包里面不需要指定object,直接指定方法就行。object为返回值
  • use,自动关闭流,流的关闭在装饰器模式的时候,只需要关闭最外部的流就可以了。

4 函数高等用法

代码在这里

Kotlin的语法糖有点多,无法无天的感觉

4.1 扩展方法

package com.example.myapplication

fun String.lettersCount():Int{
    var count = 0
    for( char in this ){
        if( char.isLetter() ){
            count++
        }
    }
    return count
}

//使用泛型,可以对任何类型进行方法扩展
fun <A> A.with(b:String):String{
    return this.toString()+"#"+b;
}

data class testClass(val name:String,val age:Int)

fun ExtendFunctionTest_Go(){
    var a = "21 Hello22"
    println("lettersCount ${a.lettersCount()}")

    var b = "b".with("c")
    println("b ${b}")

    var c = testClass("fish",123).with("kk")
    println("c ${c}")
}

Kotlin支持对现有类扩展方法,这种语法糖其实是一种双刃剑,得仔细用好

另外,配合使用Kotlin的泛型,我们可以对所有类型加入一个统一的扩展

4.2 操作符与参数重载

package com.example.myapplication

class Money(val value:Int) {
    operator fun plus(money:Money):Money{
        val sum = this.value + money.value
        return Money(sum)
    }

    operator fun plus(money:Int):Money{
        val sum = this.value + money
        return Money(sum)
    }

    operator fun minus(money:Money):Money{
        val sum = this.value - money.value
        return Money(sum)
    }

    operator fun minus(money:Int):Money{
        val sum = this.value - money
        return Money(sum)
    }

    operator fun times(money:Money):Money{
        val sum = this.value * money.value
        return Money(sum)
    }

    operator fun times(money:Int):Money{
        val sum = this.value * money
        return Money(sum)
    }

    operator fun div(money:Money):Money{
        val sum = this.value / money.value
        return Money(sum)
    }

    operator fun div(money:Int):Money{
        val sum = this.value / money
        return Money(sum)
    }

    operator fun compareTo(money:Money):Int{
        return this.value - money.value
    }

    override fun toString():String{
        return this.value.toString()
    }
}

fun MoneyTest(){
    val money1 = Money(10)
    val money2 = Money(20)
    var money3 = Money(10)
    println("money1 + money2 = ${money1+money2}")
    println("money1 - money2 = ${money1-money2}")
    println("money1 * money2 = ${money1*money2}")
    println("money1 / money2 = ${money1/money2}")
    println("money1 > money2 = ${money1 > money2}")
    println("money1 < money2 = ${money1 < money2}")
    println("money1 == money2 = ${money1 == money2}")
    println("money1 == money3 = ${money1 == money3}")


    println("money1 + 20 = ${money1+20}")
    println("money1 - 20 = ${money1-20}")
    println("money1 * 20 = ${money1*20}")
    println("money1 / 20 = ${money1/20}")
}

class MyArray {
    val list:ArrayList<Int> = ArrayList()

    fun add(data:Int){
        this.list.add(data)
    }

    operator fun get(index:Int):Int{
        return this.list.get(index)
    }

    operator fun set(index:Int,data:Int){
        this.list.set(index,data)
    }

    operator fun contains(data:Int):Boolean{
        return this.list.contains(data)
    }
}

fun MyArrayList_Go(){
    val list = MyArray()
    list.add(1)
    list.add(3)
    list.add(5)
    println("list[1] ${list[1]}")
    list[0] = 3
    println("list[0] ${list[0]}")
    println("3 in list is ${3 in list}")
    println("2 in list is ${2 in list}")
}

fun OperatorTest_Go(){
    MoneyTest()
    MyArrayList_Go()

    //返回值是IntRange类型
    //左闭右闭区间
    val target = (1..20)
    println("IntRange ${target}")
    val random = target.random()
    println("IntRange Random random ${random}")
    val newStr = "fish".repeat( random)
    println("newStr ${newStr}")
}

要点如下:

  • Kotlin支持以下的操作符重载,加减乘除,比较,in和下标操作,是真的支持得比较多
  • Kotlin还支持同一个函数名,或者同一个操作符的参数重载,这个是Java的特性了

另外要注意,Kotlin里面有Range操作

4.3 高阶函数

package com.example.myapplication

//Unit相当于Void类型
fun num1AndNum2(num1:Int,num2:Int,operatorion:(Int,Int)->Unit):Unit{
    operatorion(num1,num2)
}

fun print(num1:Int,num2:Int){
    println("num1 ${num1} and num2 ${num2}")
}

//一个特殊的闭包,类似apply方式的闭包,stringBuilder是该闭包的this参数
fun build(myBuilder:StringBuilder,block:StringBuilder.()->Unit):StringBuilder{
    block(myBuilder)
    return myBuilder
}

fun HighOrderFunctionText_Go(){
    val a = 10
    var b = 20
    //传入一个已存在的全局函数,用:;
    num1AndNum2(a,b,::print)
    //传入一个闭包
    num1AndNum2(a,b){c,d->
        println("c = ${c} and d = ${d}")
    }

    val c = StringBuilder()
    val d = build(c){
        //调用的都是StringBuilder里面的方法
        append(10)
        append("cc")
    }
    println("d is ${d}")
}

高阶函数在Javascript里面玩得比较多,也就不多说了。Kotlin另外还支持类似apply的闭包参数传递,这点要注意的。

4.4 内联函数

package com.example.myapplication

/*
 inline的优势:
 * 不需要进行函数的实际调用
 * 附加可以提前整个函数中退出的功能,return
 */
inline fun printStringInline(str:String, block:(String)->Unit){
    println("printString begin")
    block(str)
    println("pringString end")
}

fun printStringNoInline(str:String,block:(String)->Unit){
    println("printString begin")
    block(str)
    println("pringString end")
}

inline fun printStringInline2(str:String, block:(String)->Unit){
    println("printString begin")
    block(str)
    println("pringString end")
}


inline fun printStringInline3(str:String, block:(String)->Unit){
    println("printString begin")
    //inline的局限性一,inline函数中只能调用inline函数,不能调用noinline函数
    // 解决方法是,用noinline关键字包装,例如printStringInline3_2,同时失去闭包的内联能力
    //printStringNoInline(str,block)
    printStringInline2(str,block)
    println("pringString end")
}

inline fun printStringInline3_2(str:String, noinline block:(String)->Unit){
    println("printString begin")
    //inline的局限性
    printStringNoInline(str,block)
    println("pringString end")
}

inline fun printStringInline4(str:String, block:(String)->Unit){
    println("printString begin")
    //inline的局限性二,inline函数中不能将闭包block放在另外一个闭包runnable里面,因为没人清楚闭包runnable什么时候会触发
    // 解决方法是,用crossinline关键字包装,例如printStringInline4_2,同时会失去提前return全局函数的能力
    /*
    var runnable = Runnable {
        block(str)
    }
     */
    println("pringString end")
}

inline fun printStringInline4_2(str:String, crossinline block:(String)->Unit){
    println("printString begin")
    var runnable = Runnable {
        block(str)
    }
    println("pringString end")
}


fun inline_test1(){
    var str = ""
    println("init test1 begin")
    printStringInline(str){
        println("lambda start")
        if( str.isEmpty()){
            //这里的做法相当诡异,与普通语言不同
            //在闭包中return不是结束闭包,而是结束整个inline_test1
            return
        }
        println("lambda end")
    }
    println("init test1 end")
}

fun inline_test2(){
    var str = ""
    println("init test2 begin")
    printStringInline(str){
        println("lambda start")
        if( str.isEmpty()){
            //这里的return才是结束闭包
            return@printStringInline
        }
        println("lambda end")
    }
    println("init test2 end")
}

fun no_inline_test3(){
    var str = ""
    println("init test3 begin")
    printStringNoInline(str){
        println("lambda start")
        if( str.isEmpty()){
            //非inline函数,不能实现提前结束no_inline_test3
            //return
        }
        println("lambda end")
    }
    println("init test3 end")
}

fun no_inline_test4(){
    var str = ""
    println("init test4 begin")
    printStringInline4_2(str){
        println("lambda start")
        if( str.isEmpty()){
            //结束闭包
            return@printStringInline4_2
            //不能使用return,虽然是inline,因为有crossinline关键字
            //return
        }
        println("lambda end")
    }
    println("init test4 end")
}

fun crossinline_test5(){
    var str = ""
    println("init test4 begin")
    printStringNoInline(str){
        println("lambda start")
        if( str.isEmpty()){
            //结束闭包
            return@printStringNoInline
        }
        println("lambda end")
    }
    println("init test4 end")
}

fun InlineFunctionTest_Go(){
    inline_test1()
    inline_test2()
    no_inline_test3()
    no_inline_test4()
    crossinline_test5()
}

Kotlin的内联函数就比较复杂了,inline的优势是:

  • 提高性能,不需要进行函数的实际调用
  • 附加可以提前整个函数中退出的功能,return。这个跟其他语言都不同,结束当前闭包用的是return@xxxx的操作。结束当前外部函数用的是return操作,这点容易混淆。

inline的缺陷是:

  • inline函数中只能调用inline函数,不能调用noinline函数。解决方法是,用noinline关键字包装,例如printStringInline3_2,同时失去闭包的内联能力
  • inline函数中不能将闭包block放在另外一个闭包runnable里面,因为没人清楚闭包runnable什么时候会触发。解决方法是,用crossinline关键字包装,例如printStringInline4_2,同时会失去提前return全局函数的能力

4.5 不定参数的函数

package com.example.myapplication

//Any 类型是非空的
//vararg 是不定参数的关键字
fun cvOf(vararg pairs:Pair<String,Any?>):Map<String,Any>{
    val result = HashMap<String,Any>()
    for( pair in pairs ){
        var key = pair.first
        var value = pair.second
        when(value){
            is Int->result.put(key,value)
            is String->result.put(key,value)
            null->{
                println("key ${key} is null")
            }
        }
    }
    return result
}

fun VarargFunctionTest_Go(){
    val result = cvOf("a" to 1,"b" to "fish","c" to 12,"d" to null)
    println(result)
}

要点如下:

  • vararg是不定参数的关键字
  • to函数可以生成pair对
  • Any类型是非空的,Int类型是非空的,所以他们都不能赋值到Object类型(允许为空)

4.6 infix函数

package com.example.myapplication

//infix是一种语法糖,可以简化函数的调用语法,不需要点号,和括号来引用参数
//限制在于,必须是某个类的成员方法或者扩展方法,方法有且仅有一个参数
infix fun String.beginsWithFish(prefix:String):Boolean{
    return this.startsWith(prefix+"#")
}

fun InfixFunctionTest_Go(){
    var a = "ccasd"
    var b = "cc#adsf"
    println("a beginsWithFish ${a beginsWithFish "cc"}")
    println("b beginsWithFish ${b beginsWithFish "cc"}")
}

infix 函数也是写法上的语法糖了,可以省略点号,和括号,限制在于:

  • 必须是某个类的成员方法或者扩展方法
  • 方法有且仅有一个参数

5 泛型

代码在这里

5.1 泛型基础

package com.example.myapplication

fun <T> printTemp(a:T){
    println(a.toString()+"_temp")
}

//带泛型上界的声明
fun <T :Person > printPerson(a:T){
    println(a.name+","+a.age);
}

open class Person(val name:String,val age:Int)

class Student:Person("student",123)

class SimpleData<T>{
    private var data:T? = null

    fun set(t:T?){
        this.data = t
    }

    fun get():T?{
        return this.data
    }
}

//函数参数接受任意的泛型类型
fun printAllSimpleData(data:SimpleData<*>){
    val re = data.get()
    println("re ${re}")
}

fun BasicGenericTest_Go(){
    printTemp("123Hello")
    printTemp(78)
    printTemp(Person("fish",123))

    printPerson(Person("cat",780))
    printPerson(Student())

    val data1 = SimpleData<Int>()
    data1.set(123)
    println("data1 ${data1.get()}")

    var data2 = SimpleData<Person>()
    data2.set(Person("cat",879))
    println("data2 ${data2.get()}")
    //这里会报错,因为类型不匹配
    //data2.set(123)

    //可以传入不同的泛型参数
    printAllSimpleData(data1)
    printAllSimpleData(data2)
}

泛型基础,这里与Java其实是相似的,没啥好说。

另外,要注意,可以用,星号,来代表接受任意的泛型参数

5.2 泛型实化

package com.example.myapplication

//以下的写法会报错,因为Java的泛型是编译时的,运行时会擦除类型
/*
fun <T> getClass(input:T):Class<T> = {
    return T::class.java
}
 */

//使用inline和reified关键字,能实现编译时泛型展开(实化),所以就能实现这样的代码
inline fun <reified T> getClass(input:T):Class<out T> {
    return input!!::class.java
}

//甚至不需要传入参数
inline fun <reified T> getClass2():Class<T> {
    return T::class.java
}

fun ReifiedGenericTest_Go(){
    println("class1 ${getClass(1)}")
    println("class1 ${getClass("fish")}")

    println("class2 ${getClass2<Int>()}")
    println("class2 ${getClass2<String>()}")
}

因为Kotlin有内联的实现,所以Kotlin能够在编译时做泛型的实际替换,这其实是C++的泛型实现方法。我们只需要在inline关键字的基础上,加上reified关键字就可以了,这里也没啥好说的。

不过,这确实解决了一大问题。Java里面的有些接口就是因为类型擦除的问题,接口设计得很奇怪。

5.3 协变与逆变

package com.example.myapplication

//显然Dog是Animal的子类
open class Animal(val name:String)

class Dog(name:String):Animal(name){}

//一个简单的泛型
class SimpleGeneric<T>(val data:T?){
    fun get():T?{
        return data
    }
}

fun TestError(){
    val animalTpl:SimpleGeneric<Animal>
    val dogTpl:SimpleGeneric<Dog> = SimpleGeneric(Dog("dog1"))

    //Dog是Animal的子类
    //但是SimpleGeneric<Dog>并不是SimpleGeneric<Animal>的子类,无法赋值
    //以下这句会报错,因为泛型包装的新类型,与原类型的关系,并不是简单的问题
    //animalTpl = dogTpl
}

//协变的泛型
class SimpleGeneric2<out T>(val data:T?){
    fun get():T?{
        return data
    }
}

fun TestCovariantGeneric(){
    val animalTpl:SimpleGeneric2<Animal>
    val dogTpl:SimpleGeneric2<Dog> = SimpleGeneric2(Dog("dog1"))

    //Dog是Animal的子类
    //当SimpleGeneric类型是协变的时候,泛型只能在函数的out位置,而不是in位置的时候。
    //SimpleGeneric<Dog>才是SimpleGeneric<Animal>的子类
    //所以,这句才会正确,编译不报错
    animalTpl = dogTpl
    println("animalTpl ${animalTpl}")

    //因为,能返回Animal类型的泛型的接口,肯定能接受,返回Dog类型的泛型的实现
}


//逆变的泛型
class SimpleGeneric3<in T>{
    fun set(a:T?):String{
        return a.toString()
    }
}

fun TestInvertvariantGeneric(){
    val animalTpl:SimpleGeneric3<Animal> = SimpleGeneric3()
    val dogTpl:SimpleGeneric3<Dog>

    //Dog是Animal的子类
    //当SimpleGeneric类型是逆变的时候,泛型只能在函数的in位置,而不是out位置的时候。
    //反过来,SimpleGeneric<Animal>是SimpleGeneric<Dog>的子类
    //所以,这句才会正确,编译不报错
    dogTpl = animalTpl
    println("animalTpl ${animalTpl}")

    //因为,能接收Dog参数的泛型的接口,肯定能接受,能接收Animal参数的泛型的实现
}


//协变,与部分指定的泛型
class SimpleGeneric4<out T>(val data:T?){
    //@UnsafeVariance是特殊做法,指定不参与协变运算,因为这个方法仅仅用来查询,而不是设置进去的
    fun contains(a: @UnsafeVariance T?):Boolean{
        return true
    }
    fun get():T?{
        return data
    }
}


fun TestCovariantGeneric2(){
    val animalTpl:SimpleGeneric4<Animal>
    val dogTpl:SimpleGeneric4<Dog> = SimpleGeneric4(Dog("dog1"))

    animalTpl = dogTpl
    println("animalTpl ${animalTpl}")
}

fun CovariantGenericTest_Go(){
    TestError()
    TestCovariantGeneric()
    TestInvertvariantGeneric()
    TestCovariantGeneric2()
}

泛型的协变与逆变,是一大难点了。首先,说明协变与逆变是什么,它是泛型的一种性质,有这种性质的泛型能得到一个额外的能力,就像胖和瘦是人的一个性质一样。

已知Student是Person的子类型,并且知道MyContainer<T>是一个泛型。那么,我们可以确定MyContainer<Student>与MyContainer<Person>的关系吗,哪个是子类型,哪个是父类型?答案是不可以,这取决于MyContainer的内部实现。Kotlin的方法是协助我们标注泛型性质,从而在编译时确定泛型的父子类型关系。

  • 当MyContainer<Student>是MyContainer<Person>的子类型的时候,我们称为MyContainer是协变的,这种协变类型的特点是,泛型参数T只能在out参数
  • 当MyContainer<Person>是MyContainer<Student>的子类型的时候,我们称为MyContainer是逆变的,这种协变类型的特点是,泛型参数T只能在in参数
  • 特殊情况下,我们需要用@UnsafeVariance关键字来绕过这个规定,但是运行时可能会报错,这需要开发者自己来确定。

最后,在Kotlin中,不可变的容器类型,都是泛型,并且是协变的。

这个问题并不容易理解,具体看《第一行Android代码》的P419,书上说得挺好的。

6 协程

Kotlin的协程是一个看家本领了,在UI开发中,协程的使用相当重要,避免了一大堆的闭包回调问题。与此同时,Kotlin的协程接口设计相对于js和golang要复杂不少,这是因为:

  • 需要协同现有的线程库,以及自定义协程在线程上的分配,协程在线程上的挂起然后回调等等。对于golang来说,开发者只能选择线程数量,对协程在哪个线程上运行,指定协程需要固定在特定线程上,等等这些功能上是无法实现的。对于js来说,协程(异步)更是更简化的实现,因为js的业务逻辑只有单线程,协程(异步)不允许被在多个线程中迁移。
  • 需要支持结构化并发,golang启动协程以后,父协程需要等待子协程的返回需要特定的channel来配合,当子协程抛出异常以后,整个进程都会崩掉。Kotlin对此不是这样的,父协程的生命周期默认就是其全部子协程的生命周期的最大值,子协程抛出异常的时候,父协程也会受到异常。
  • Kotlin的协程是一个库,而不是一个语言实现,这点略有遗憾。所以Kotlin的协程语法用起来并不那么顺手。

总而言之,Kotlin为了应对UI问题上的更复杂的异步问题(相比后端处理),提出了一个相对复杂的协程设计。

参考资料:

6.1 快速上手

因为协程的问题比较庞大,所以先来一个快速上手版本先,代码在这里

6.1.1 依赖

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

在app/build.gradle加入以上依赖,注意,版本号最好与Kotlin版本号一致

6.1.2 作用域

package com.example.myapplication.scope

import kotlinx.coroutines.*
import kotlin.time.Duration

fun runBlockingTest(){
    println("1")
    //runBlocking是全局方法,启动协程,并且会堵塞当前线程,直至协程作用域的结束
    runBlocking {
        println("2")
        delay(100)
        println("3")
    }
    println("4")

    //输出为1,2,3,4
}

fun globalScopeLaunchTest(){
    println("1")
    //GlobalScope是一个单例,也是一个协程作用域,launch是它的方法
    //GlobalScope启动后不堵塞当前线程,并且返回一个job方法,可以用来检查协程作用域是否已经结束
    val job = GlobalScope.launch {
        println("2")
        delay(100)
        println("3")
    }
    println("4")
    Thread.sleep(200)
    //等待协程作用域的结束,join方法
    //job.join()
    println("5")

    //输出为1,4,2,3,5
}

fun CoroutineScopeLaunchTest(){
    println("1")
    //先创建一个Job
    var job = Job()
    //CoroutineScope是一个类,也是一个协程作用域,launch是它的方法
    //CoroutineScope与GlobalScope的区别在于,它绑定到一个job上来,而不是返回一个新的job
    //因此可以将多个CoroutineScope绑定到同一个job上,我们可以同时用单个job等待或者检查多个CoroutineScope
    CoroutineScope(job).launch{
        println("2")
        delay(100)
        println("3")
    }
    println("4")
    Thread.sleep(200)
    //等待协程作用域的结束,join方法
    //job.join()
    println("5")

    //输出为1,4,2,3,5
}

fun ScopeTest_Go(){
    //runBlockingTest()
    //globalScopeLaunchTest()
    CoroutineScopeLaunchTest()
}

我们可以通过这几种方法创建作用域:

  • runBlocking,全局方法,默认会自动堵塞当前线程
  • GlobalScope.launch,单例下的方法,不堵塞线程,每次返回一个新的job
  • CoroutineScope(job).launch,类实例下的方法,不堵塞线程,可以复用同一个job

6.1.3 挂起方法

package com.example.myapplication.scope

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.lang.Exception
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

fun go1(){
    //这种方法会堵塞协程
    Thread.sleep(10)
}

//挂起方法的定义是在方法中加入suspend关键字
//对于js来说,相当于一个async方法
suspend fun go2(){
    //这种方法不堵塞协程
    delay(10)
}

class MyTimeout{
    fun wait(timeout:Long,callback:(target:Int)->Unit){
        Thread{
            Thread.sleep(timeout)
            callback(100)
        }.start()
    }
}

//suspendCoroutine是一个特殊的方法,可以将协程暂时切换到出去
//当数据有返回的时候,我们才触发continuation.resume来触发数据
//这是一个协程切换到普通线程,普通线程再切换回来的重要方法
//对于js来说,这个方法相当于用Promise包装一个async函数
suspend fun suspendCoroutine_1():Int{
    return suspendCoroutine{ continuation->
        MyTimeout().wait(100) { result ->
            continuation.resume(result)
        }
    }
}

suspend fun suspendCoroutine_2():Int{
    return suspendCoroutine{ continuation->
        MyTimeout().wait(100) { result ->
            continuation.resumeWithException(Exception("exception1"))
        }
    }
}


suspend fun suspendCoroutine_3():Int{
    return suspendCoroutine{ continuation->
        MyTimeout().wait(100) { result ->
            val returnResult:Result<Int> = Result.success(102)
            var returnException:Result<Int> = Result.failure(Exception("exception2"))
            if( (1..2).random() == 1 ){
                continuation.resumeWith(returnResult)
            }else{
                continuation.resumeWith(returnException)
            }
        }
    }
}

fun testBasic(){
    runBlocking {
        println("1")
        delay(100)
        println("2")
        //在协程里面,我们可以调用普通方法
        go1()
        println("3")

        //我们也可以调用特定的挂起方法
        //调用挂起方法的意义在于,我们可以高效的使用协程。
        //调用普通方法,遇到堵塞的时候不会让出时间片,但是挂起方法遇到堵塞的时候会自动让出时间片给其他的协程
        //注意,这里与js不同。js调用async方法以后,可以await,也可以不await。
        // 但是在Kotlin的协程里面,必须await,这种调用方式是隐式的,这种方法其实避免隐晦的bug,但是带来是不灵活的代价
        go2()
        println("4")

        val a = suspendCoroutine_1()
        println("a is ${a}")
        println("5")

        try{
            val b = suspendCoroutine_2()
        }catch(e:Exception){
            e.printStackTrace()
            println("6")
        }

        try{
            val c = suspendCoroutine_3()
            println("7 c is ${c}")
        }catch(e:Exception){
            e.printStackTrace()
            println("7")
        }

    }
}

fun Suspend_Test(){
    testBasic()
}

要点如下:

  • suspend方法,可挂起方法,在协程中尽可能只调用这类方法,当遇到堵塞的时候会自动让出时间片给其他协程(需要堵塞方法支持,例如用delay,而不是Thread.sleep)。
  • suspendCoroutine方法,在挂起方法中,调用普通作用域下的异步操作的方法,会自动让出时间片给其他协程。

6.1.4 创建协程

package com.example.myapplication.scope

import kotlinx.coroutines.*

fun MyLaunch_Go(){
    runBlocking {
        //launch其实是CoroutineScope的方法,外层必须有CoroutineScope才能调用
        println("1")
        //launch就是启动一个协程,不堵塞当前协程,无返回值
        // 但是launch的运行周期受子协程影响,结构性并发
        var job = launch{
            println("2")
            launch {
                println("3.1")
                delay(100)
                println("3.2")
            }
            launch {
                println("4.1")
                delay(200)
                println("4.2")
            }
            println("5")
        }
        println("6")
        job.join()
        println("7")
        //输出如下
        /*
        1
        6
        2
        5
        3.1
        4.1
        3.2
        4.2
        7
         */
    }
}

fun MyAsync_Go(){
    runBlocking {
        //async其实是CoroutineScope的方法,外层必须有CoroutineScope才能调用
        println("1")
        //async就是启动一个协程,不堵塞当前协程,有返回值
        // 但是async的运行周期受子协程影响,结构性并发
        var job = launch{
            println("2")
            var a1 = async {
                println("3.1")
                delay(100)
                println("3.2")
                "1"
            }
           var a2 = async {
                println("4.1")
                delay(200)
                println("4.2")
                "2"
            }
            println("5")
            //依然可以用join,但是没有返回值
            //a1.join()
            //对于async启动的块,我们可以用await获取返回值
            println("a1 is ${a1.await()}")
            println("a2 is ${a2.await()}")
        }
        println("6")
        job.join()
        println("7")
        //输出如下
        /*
        1
        6
        2
        5
        3.1
        4.1
        3.2
        a1 is 1
        4.2
        a2 is 2
        7
         */
    }
}

fun MyWithContext_Go(){
    runBlocking {
        //withContext其实是CoroutineScope的方法,外层必须有CoroutineScope才能调用
        println("1")
        //withContext就是启动一个协程,堵塞当前协程,有返回值
        //withContext与async很相似,不同之处在于,马上堵塞协程,且必须指定Dispatcher
        // 但是async的运行周期受子协程影响,结构性并发
        var job = launch{
            println("2")
            //参数指分派到IO线程池上执行
            var a1 = withContext(Dispatchers.IO) {
                println("3.1")
                delay(100)
                println("3.2")
                "1"
            }
            //和上面的写法是一致的
            var a2 = async(Dispatchers.IO)  {
                println("4.1")
                delay(200)
                println("4.2")
                "2"
            }.await()
            println("5")
            println("a1 is ${a1}")
            println("a2 is ${a2}")
        }
        println("6")
        job.join()
        println("7")
        //输出如下
        /*
        1
        6
        2
        3.1
        3.2
        4.1
        4.2
        5
        a1 is 1
        a2 is 2
        7
         */
    }
}

/*
// 这种是常见的写法错误,launch,async和withContext都是CoroutineScope的方法
// 在挂起函数中是没有CoroutineScope,所以这种方法是错误的
suspend fun mySuspend(){
    launch{
        println("cc")
    }
}
 */

//正确的方法是,先用coroutineScope获取当前的CoroutineScope
//然后再在CoroutineScope里面使用launch和async
suspend fun mySuspend2(){
    val job = coroutineScope {
        launch{
            println("3.1")
            delay(100)
            println("3.2")
        }
        async{
            println("4.1")
            delay(200)
            println("4.2")
            "HAHA"
        }
    }
    //coroutineScope的特点是隐式的join,无论你有没有调用,他都需要等待子协程结束才能返回
    //job.join()
}

fun MySuspendLanuch_Go(){
    runBlocking {
        println("1")
        //mySuspend()
        println("2")
        mySuspend2()
        println("5")
        /*
        输出结果:
        1
        2
        3.1
        4.1
        3.2
        4.2
        5
         */
    }
}

fun MySuspendLanuch2_Go(){
    runBlocking {
        println("1")
        //mySuspend()
        println("2")
        val job = coroutineScope {
            launch{
                println("3.1")
                delay(100)
                println("3.2")
            }
            async{
                println("4.1")
                delay(200)
                println("4.2")
                "HAHA"
            }
        }
        //coroutineScope的特点是隐式的join,无论你有没有调用,他都需要等待子协程结束才能返回
        //即使不在suspend方法中,它依然是这样
        //job.join()
        println("5")
        /*
        输出结果:
        1
        2
        3.1
        4.1
        3.2
        4.2
        3
         */
    }
}

suspend fun mySuspend3(){
    //supervisorScope与coroutineScope相似的工作方式,区别在于发生异常时的处理方式不同
    val job = supervisorScope {
        launch{
            println("3.1")
            delay(100)
            println("3.2")
        }
        async{
            println("4.1")
            delay(200)
            println("4.2")
            "HAHA"
        }
    }
    //supervisorScope的特点是隐式的join,无论你有没有调用,他都需要等待子协程结束才能返回
    //job.join()
}

fun MySuspendLanuch3_Go(){
    runBlocking {
        println("1")
        //mySuspend()
        println("2")
        mySuspend3()
        println("5")
        /*
        输出结果如下:
        1
        2
        3.1
        4.1
        3.2
        4.2
        5
         */
    }
}

fun LaunchTest_Go(){
    //MyLaunch_Go()
    //MyAsync_Go()
    //MyWithContext_Go()
    //MySuspendLanuch_Go()
    //MySuspendLanuch2_Go()
    MySuspendLanuch3_Go()
}

要点如下:

  • launch,启动协程,不堵塞当前协程,无返回值,join方法会等待对应协程结束
  • async,启动协程,不堵塞当前协程,有返回值,join或者await方法会等待对应协程结束
  • withContext,启动协程,堵塞协程,有返回值,默认join。

在suspend挂起方法里面,不能直接创建新协程,需要先获取作用域。

  • coroutineScope,获取当前的作用域,并堵塞当前协程,直至所有子协程结束,默认join。异常发生的时候一挂全挂。
  • supervisorScope,获取当前的作用域,并堵塞当前协程,直至所有子协程结束,默认join。异常发生的时候子挂父不挂。

6.1.5 小结

Kotlin协程的几个注意点:

  • 结构化并发,父协程的生命周期,取决于自身和子协程生命周期的最大值。
  • 默认堵塞当前线程,runBlocking
  • 默认堵塞当前协程,withContext,coroutineScope和supervisorScope

协程绑定的线程为,Dispatcher配置:

  • Unconfined,无线程池,每次都是新的线程
  • Default,线程池,适合CPU密集应用,线程数量较多。
  • IO,IO线程池,适合IO密集应用,线程数量较多。
  • Main,UI线程,适合操作UI操作

注意,IO与Default共用一部分的线程,所以两者可能会调度到同一条线程上。

6.2 协程作用域

后续的代码在这里

6.2.1 协程上下文

fun ScopeContextTest(){
    runBlocking {
        //coroutineContext就是一个Key/value的容器而已
        val a = GlobalScope.launch(CoroutineName("k1")) {
            //CoroutineName是类,同时它有一个伴随对象,也是同名的CoroutineName
            GlobalScope.launch(CoroutineName("k2")) {
                //获取当前Scope的Job,Job是Job类的伴随对象
                println(coroutineContext[Job])
                //等价于这种写法
                println(coroutineContext[Job.Key])
                //获取当前Scope的Name
                println(coroutineContext[CoroutineName])
            }
            println(coroutineContext[Job])
            println(coroutineContext[CoroutineName])
        }
        a.join()
    }
}

协程上下文是一个简单的Key/Value结构

6.2.2 协程调度器

package com.example.myapplication

import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.math.log

fun ScopeContextTest(){
    runBlocking {
        //coroutineContext就是一个Key/value的容器而已
        val a = GlobalScope.launch(CoroutineName("k1")) {
            //CoroutineName是类,同时它有一个伴随对象,也是同名的CoroutineName
            GlobalScope.launch(CoroutineName("k2")) {
                //获取当前Scope的Job,Job是Job类的伴随对象
                println(coroutineContext[Job])
                //等价于这种写法
                println(coroutineContext[Job.Key])
                //获取当前Scope的Name
                println(coroutineContext[CoroutineName])
            }
            println(coroutineContext[Job])
            println(coroutineContext[CoroutineName])
        }
        a.join()
    }
}

fun ScopeDispacherWithContext(){
    runBlocking {
        val go1 = {
            println("current Id ${Thread.currentThread().id}")
        }
        for (i in (1..10)) {
            launch {
                withContext(Dispatchers.IO) {
                    go1()
                }
            }
        }
    }
    /*
    输出结构如下:
    current Id 12
    current Id 13
    current Id 16
    current Id 14
    current Id 16
    current Id 17
    current Id 15
    current Id 18
    current Id 14
    current Id 17
     */
}

fun ScopeDispacherTest(){
    //创建一个单线程的调度器
    val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()
    runBlocking {
        //启动协程的时候指定调度器
        GlobalScope.launch(myDispatcher) {
            log(1)//MyThread线程
        }.join()
        log(2)//主线程
    }
    //用完要记得关闭调度器
    myDispatcher.close()
    /*
    输出结构如下:
    2021-12-16T08:42:19.643589Z [MyThread] 1
    2021-12-16T08:42:19.655755Z [main] 2
     */
}

fun ScopeDispatcherTest2(){
    runBlocking {
        Executors.newFixedThreadPool(10)
            .asCoroutineDispatcher().use { dispatcher ->
                GlobalScope.launch(dispatcher) {
                    //首次进入时调度1次
                    log(1)
                    //进入async调度1次
                    val job = async {
                        log(2)
                        delay(1000)
                        //delay返回来以后调度1次
                        log(3)
                        "Hello"
                    }
                    log(4)
                    val result = job.await()
                    //await 返回以后帝都1次
                    log("5. $result")
                }.join()
                log(6)
            }
    }
    /*
    输出结果如下:共产生4次调度,用了4个线程
    2021-12-16T08:40:47.317772Z [pool-1-thread-1] 1
    2021-12-16T08:40:47.333989Z [pool-1-thread-1] 4
    2021-12-16T08:40:47.334100Z [pool-1-thread-2] 2
    2021-12-16T08:40:48.340155Z [pool-1-thread-3] 3
    2021-12-16T08:40:48.341376Z [pool-1-thread-4] 5. Hello
     */
}

fun ScopeTest_Go(){
    //ScopeContextTest()
    ScopeDispacherWithContext()
    //ScopeDispacherTest()
    //ScopeDispatcherTest2()
}

我们除了可以使用IO,Main,Default这些调度器以外,我们还可以自定义调度器。注意,每一次withContext(IO)会生成新的线程调度,这最终会导致多线程的发生。这跟js不一样,js里面的业务实际都在单线程里面工作。

6.3 协程异常

6.3.1 未捕获异常传播

package com.example.myapplication

import kotlinx.coroutines.*

fun ExceptionCatch(){
    //一般情况下,异常抛出以后,其他协程不受影响,仅仅会在全局打印异常错误
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                launch {
                    println("4.1")
                    delay(30)
                    throw ArithmeticException("Hey!")
                    println("4.2")
                }
                launch {
                    //该协程受到影响,5.2无法输出
                    println("5.1")
                    delay(50)
                    println("5.2")
                }
                println("6")
                delay(100)
                //该协程受到影响,7无法输出
                println("7")
            }
            delay(100)
            //该协程受到影响,2无法输出
            println("2")
        }
        a.join()
        /*
        输出如下:
        1
        3
        4.1
        6
        5.1
         */
    }
}

fun ExceptionLaunchJoinCatch(){
    //作用域里面,使用async或者launch的join可以捕捉到异常,异常为
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                var job = launch {
                    throw ArithmeticException("Hey!")
                }
                delay(100)
                //不执行
                println("4")
            }
            delay(100)
            //不执行
            println("2")
        }
        a.join()
        /*
        输出如下:
        1
        3
        */
    }
}

fun ExceptionAsyncJoinCatch(){
    //作用域里面,使用async或者launch的join可以捕捉到异常,异常为
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                var job = async {
                    throw ArithmeticException("Hey!")
                }
                delay(100)
                //不执行
                println("4")
            }
            delay(100)
            //不执行
            println("2")
        }
        a.join()
        /*
        输出如下:
        1
        3
        */
    }
}

fun ExceptionAwaitCatch(){
    //作用域里面,使用async的await也可以捕捉到异常
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                var job = async {
                    throw ArithmeticException("Hey!")
                    "cc"
                }
                delay(100)
                //不执行
                println("4")
            }
            delay(100)
            //不执行
            println("2")
        }
        a.join()
        /*
        1
        3
         */
    }
}

fun ExceptionWithContextCatch(){
    //作用域里面,使用withContext
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                try{
                    println("3")
                    //withContext的异常能自动传递给上级,并且不会让其他协程自动退出
                    var job = withContext(Dispatchers.Default) {
                        throw ArithmeticException("Hey!")
                        "cc"
                    }
                    delay(100)
                    //这一句不执行
                    println("4")
                }catch(e: java.lang.Exception){
                    //注意,这一句能执行,捕获到原始异常
                    println("e ${e}")
                }
            }
            delay(100)
            //注意,这一句正常执行
            println("2")
        }
        a.join()
        /*
        1
        3
        e java.lang.ArithmeticException: Hey!
        2
         */
    }
}

fun ExceptionCoroutineScopeCatch(){
    //作用域里面,使用withContext
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                coroutineScope {
                    println(5)
                    launch {
                        println(7)
                        throw ArithmeticException("Hey!")
                        //不执行
                        println(8)
                    }
                    launch {
                        println(9)
                        delay(100)
                        //不执行
                        println(10)
                    }
                    delay(100)
                    //不执行
                    println(6)
                }
                delay(100)
                //不执行
                println("4")
            }
            delay(100)
            //不执行
            println("2")
        }
        a.join()
        /*
        1
        3
        5
        7
        9
        Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: Hey!
         */
    }
}

fun ExceptionSupervisorScopeCatch(){
    //作用域里面,使用withContext
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                supervisorScope {
                    println(5)
                    launch {
                        println(7)
                        throw ArithmeticException("Hey!")
                        //不执行
                        println(8)
                    }
                    launch {
                        println(9)
                        delay(100)
                        //执行
                        println(10)
                    }
                    delay(100)
                    //执行
                    println(6)
                }
                delay(100)
                //执行
                println("4")
            }
            delay(100)
            //执行
            println("2")
        }
        a.join()
        /*
        1
        3
        5
        7
        9
        Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: Hey!
         */
    }
}

fun ExceptionSpreadTest_Go(){
    //ExceptionCatch()
    //ExceptionLaunchJoinCatch()
    //ExceptionAsyncJoinCatch()
    //ExceptionAwaitCatch()
    ExceptionWithContextCatch()
    //ExceptionCoroutineScopeCatch()
    //ExceptionSupervisorScopeCatch()
}

未捕获异常的传播,总结如下:

  • 默认情况下,对于同一个作用域下用launch或者async子协程的未捕获异常,会停止同一个作用域下面其他协程的正常运行
  • 因为corountineScope沿用旧的作用域,所以corountineScope的子协程产生未捕获异常的时候,会连累原来的作用域一起崩掉
  • 因为withContext会产生新的作用域,所以这个作用域下的异常不会影响上级作用域的协程。另外,withContext产生的异常会自动传递给他的上级协程
  • 因为supervisorScope会产生新的作用域,所以这个作用域下的异常不会影响上级作用域的协程。另外,supervisorScope的各个子协程相互不受异常影响

6.3.2 未捕获异常默认捕捉

package com.example.myapplication.exception

import com.example.myapplication.log
import kotlinx.coroutines.*
import java.lang.Exception

fun ExceptionDefaultCatch(){
    runBlocking {
        //设置异常处理
        val exceptionHandler = CoroutineExceptionHandler() { coroutineContext, throwable ->
            log("Throws an exception with message: ${throwable.message}")
        }

        log(1)

        //启动协程的时候,可以指定默认的异常处理方式
        GlobalScope.launch(exceptionHandler) {
            throw ArithmeticException("Hey!")
        }.join()
        log(2)
    }
    /*
    1
    2
    3
    e java.lang.ArithmeticException: Hey!
    Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: Hey!
     */
}

fun ExceptionDefaultCatch_supervisorScope(){
    runBlocking {
        //设置异常处理
        val exceptionHandler = CoroutineExceptionHandler() { coroutineContext, throwable ->
            log("Throws an exception with message: ${throwable.message} ${coroutineContext[CoroutineName]}")
        }

        log(1)

        //一般情况下,由顶级的Scope来捕捉未捕获异常
        //当有supervisorScope的时候,由supervisorScope下面的首个顶级launch来捕获一会吃那个
        GlobalScope.launch(exceptionHandler+CoroutineName("1")) {
            log(3)
            launch {
                supervisorScope {
                    launch(exceptionHandler+CoroutineName("2")) {
                        launch( exceptionHandler+CoroutineName("3")) {
                            throw ArithmeticException("Hey!")
                        }
                    }
                }
            }

            log(4)
        }.join()
        log(2)
        /*
        输出结果如下:
        2021-12-16T11:46:56.713518Z [main] 1
        2021-12-16T11:46:56.737191Z [DefaultDispatcher-worker-1] 3
        2021-12-16T11:46:56.738443Z [DefaultDispatcher-worker-1] 4
        2021-12-16T11:46:56.744317Z [DefaultDispatcher-worker-2] Throws an exception with message: Hey! CoroutineName(2)
        2021-12-16T11:46:56.745125Z [main] 2
         */
    }
}

fun ExceptionDefaultCatch_coroutineScope(){
    runBlocking {
        //设置异常处理
        val exceptionHandler = CoroutineExceptionHandler() { coroutineContext, throwable ->
            log("Throws an exception with message: ${throwable.message} ${coroutineContext[CoroutineName]}")
        }

        log(1)

        //一般情况下,由顶级的Scope来捕捉未捕获异常
        //当有supervisorScope的时候,由supervisorScope下面的首个顶级launch来捕获一会吃那个
        GlobalScope.launch(exceptionHandler+CoroutineName("1")) {
            log(3)
            launch {
                coroutineScope {
                    launch(exceptionHandler+CoroutineName("2")) {
                        launch( exceptionHandler+CoroutineName("3")) {
                            throw ArithmeticException("Hey!")
                        }
                    }
                }
            }

            log(4)
        }.join()
        log(2)
        /*
        输出结果如下:
        2021-12-16T11:46:56.713518Z [main] 1
        2021-12-16T11:46:56.737191Z [DefaultDispatcher-worker-1] 3
        2021-12-16T11:46:56.738443Z [DefaultDispatcher-worker-1] 4
        2021-12-16T11:46:56.744317Z [DefaultDispatcher-worker-2] Throws an exception with message: Hey! CoroutineName(1)
        2021-12-16T11:46:56.745125Z [main] 2
         */
    }
}

fun ExceptionDefaultCatch_withContext(){
    runBlocking {
        //设置异常处理
        val exceptionHandler = CoroutineExceptionHandler() { coroutineContext, throwable ->
            log("Throws an exception with message: ${throwable.message} ${coroutineContext[CoroutineName]}")
        }

        log(1)

        //一般情况下,由顶级的Scope来捕捉未捕获异常
        //当有supervisorScope的时候,由supervisorScope下面的首个顶级launch来捕获一会吃那个
        GlobalScope.launch(exceptionHandler+CoroutineName("0")) {
            log(3)
            launch {
                withContext(Dispatchers.IO + exceptionHandler + CoroutineName("1")) {
                    launch(exceptionHandler + CoroutineName("2")) {
                        launch(exceptionHandler + CoroutineName("3")) {
                            throw ArithmeticException("Hey!")
                        }
                    }
                }
            }
            delay(100)
            log(4)
        }.join()
        delay(100)
        log(2)
        /*
        输出结果如下:
        2021-12-16T11:46:56.713518Z [main] 1
        2021-12-16T11:46:56.737191Z [DefaultDispatcher-worker-1] 3
        2021-12-16T11:46:56.738443Z [DefaultDispatcher-worker-1] 4
        2021-12-16T11:46:56.744317Z [DefaultDispatcher-worker-2] Throws an exception with message: Hey! CoroutineName(1)
        2021-12-16T11:46:56.745125Z [main] 2
         */
    }
}


fun ExceptionCatchTest_Go(){
    //ExceptionDefaultCatch()
    ExceptionDefaultCatch_supervisorScope()
    //ExceptionDefaultCatch_coroutineScope()
    //ExceptionDefaultCatch_withContext()
}

未捕获异常的捕捉,总结如下: * 默认情况下,异常总是冒泡到当前作用域顶部被捕捉到 * 使用supervisorScope的时候,异常最多只能被supervisorScope的最近launch或者async捕捉到 * 使用withContext但没有被捕捉的时候,依然被顶部作用域捕捉

6.4 协程取消

线程和协程取消的设计非常相似,都是有取消操作,没有停止操作,为什么?因为,立即停止协程,协程中的资源没有来得及得到释放。而取消协程,则是在协程自动恢复一个CancellingException的异常,通知协程进行取消操作。同理,停止线程也会产生资源泄漏的风险,线程取消的时候会触发InterruptedException的异常。

6.4.1 取消传播

package com.example.myapplication.cancel

import com.example.myapplication.ExceptionWithContextCatch
import kotlinx.coroutines.*

fun CancelCatch(){
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            val job = launch {
                println("3")
                launch {
                    println("4.1")
                    delay(100)
                    //该协程受到影响,4.2无法输出
                    println("4.2")
                }
                launch {
                    //该协程受到影响,5.2无法输出
                    println("5.1")
                    delay(100)
                    println("5.2")
                }
                println("6")
                delay(100)
                //该协程受到影响,7无法输出
                println("7")
            }
            delay(100)
            job.cancel()
            //该协程受到影响,2无法输出
            println("2")
        }
        delay(30)
        a.cancel()
        /*
        输出如下:
        1
        3
        4.1
        6
        5.1
         */
    }
}

fun CancelWithContextCatch(){
    //作用域里面,使用withContext
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                //withContext的异常能自动传递给上级,并且不会让其他协程自动退出
                var job = withContext(Dispatchers.Default) {
                    println(5)
                    delay(100)
                    //这一句不执行
                    println(6)
                }
                delay(100)
                //这一句不执行
                println("4")
            }
            delay(100)
            //这一句不执行
            println("2")
        }
        delay(30)
        a.cancel()
        /*
        1
        3
         */
    }
}

fun CancelCoroutineScopeCatch(){
    //作用域里面,使用withContext
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                coroutineScope {
                    println(5)
                    launch {
                        println(7)
                        delay(100)
                        //不执行
                        println(8)
                    }
                    launch {
                        println(9)
                        delay(100)
                        //不执行
                        println(10)
                    }
                    delay(100)
                    //不执行
                    println(6)
                }
                delay(100)
                //不执行
                println("4")
            }
            delay(100)
            //不执行
            println("2")
        }
        delay(30)
        a.cancel()
        /*
        1
        3
        5
        7
        9
         */
    }
}

fun CancelSupervisorScopeCatch(){
    //作用域里面,使用withContext
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                supervisorScope {
                    println(5)
                    launch {
                        println(7)
                        delay(100)
                        //不执行
                        println(8)
                    }
                    launch {
                        println(9)
                        delay(100)
                        //不执行
                        println(10)
                    }
                    delay(100)
                    //不执行
                    println(6)
                }
                delay(100)
                //不执行
                println("4")
            }
            delay(100)
            //不执行
            println("2")
        }
        delay(30)
        a.cancel()
        /*
        1
        3
        5
        7
        9
        */
    }
}

fun CancelSpreadTest_Go(){
    //CancelCatch()
    //CancelWithContextCatch()
    //CancelCoroutineScopeCatch()
    CancelSupervisorScopeCatch()
}

协程取消的逻辑更为简单直接,总结如下:

  • 按照协程结构块的规则,顶级协程cancel的时候,将下面所有的协程都cancel掉,无论是不是同一个作用域

6.4.2 取消通知

package com.example.myapplication.cancel

import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine


class MyTimeout{
    fun wait(timeout:Long,callback:(target:Int)->Unit):Thread{
        val thread = Thread{
            try{
                Thread.sleep(timeout)
                callback(100)
            }catch(e:InterruptedException){
                println("catch thread interrupt")
            }
        }
        thread.start()
        return thread
    }
}

suspend fun suspendCoroutine_1():Int{
    //注意改为了suspendCancellableCoroutine,而不是suspendCoroutine
    return suspendCancellableCoroutine{ continuation->
        val thread = MyTimeout().wait(100) { result ->
            continuation.resume(result)
        }
        //关键是这一句,invokeOnCancellation的意义是,当收到协程cancel的时候,得到额外的通知
        //这样可以快速停止普通作用域的操作,例如及时停止线程池中对网络请求的发送(OkHttp),释放回调避免对View的内存占用(AutoDispose),
        continuation.invokeOnCancellation {
            //通知线程interrupt
            println("thread cancel!!!")
            thread.interrupt()
        }

    }
}


fun CancelSuspend(){
    runBlocking {
        var a = GlobalScope.launch {
            println("1")
            launch {
                println("3")
                suspendCoroutine_1()
                //不执行
                println("4")
            }
            launch {
                println("5")
                delay(100)
                //不执行
                println("6")
            }
            delay(100)
            //不执行
            println("2")
        }
        delay(30)
        a.cancel()
        /*
        1
        3
        5
        thread cancel!!!
        catch interrupt
         */
    }
}

fun CancelSuspendTest_Go(){
    CancelSuspend()
}

取消通知的方式也比较简单,用suspendCancellableCoroutine,然后注册invokeOnCancellation回调就可以了。

协程的取消传播,与取消通知,容易被混淆,他们的区别在于:

  • 取消通知是,是通知普通作用域下的库释放资源,放弃回调。
  • 取消传播是,是触发协程作用域下的协程的CanellingException异常,触发提前结束协程。

6.5 结构化并发

从以上的例子中,我们可以看到Kotlin的协程遵循结构化并发的设计,包括有:

  • 父协程的join或者wait,需要等待所有子协程的结束
  • 任一子协程的未捕获异常,会导致同一个作用域下的父协程或者兄弟协程全部挂掉。要修改默认这个行为,需要配合使用withContext或者supvisorScope。
  • 父协程的cancel,会自动触发所有子协程的cancel,无论是否在同一个作用域下面。

6.6 安卓框架上的配合指引

UI框架上有几个问题是比较头痛的,我们需要配合Kotlin协程来解决。

  • 回调地狱,Kotlin协程的suspend工具能很好地避免回调地狱的问题
  • UI线程与IO线程切换,安卓上不允许在UI线程上进行网络IO操作,否则会报错,这种设计很有必要,毕竟没人愿意因为网络迟钝而导致页面无法响应。用Kotlin的withContext我们可以轻松做到随意切换线程,而且代码还是顺序式的
  • 并发IO请求,一个用户触发操作,需要并发执行多个网络请求,并且要求其中一个网络请求失败时,自动取消其他的网络请求。这用Kotlin的async/await以及coroutineScope就能实现了。
  • Activity提前Destroy的问题,看这个古老的问题,用户按下按钮,触发Network操作,当Network还没有获取完数据以前,用户按下Back键,系统销毁了Activity页面。这个时候,Network回调回来了,企图修改页面UI,然后App就崩溃了。有了Kotlin的协程cancel操作,这个问题变得相当轻松,我们将所有的回调触发操作都写在CoroutineScope的特定job上,当Activity进行Destroy回调的时候,触发job的cancel操作,提前让所有协程结束就可以了,优雅而且安全。
  • View销毁但是回调存在导致的内存泄漏问题,这个问题跟上一个问题相似,但是本质不太一样。RxJava中有对应的解决方案AutoDispose。想象一下,Activity没有Destroy,但是一个动态的ListView组件被删除了,而这个ListView组件的onClick操作早已经被触发了,对应的Network操作长时间没有得到返回,这会导致ListView组件由于Network回调操作而持有着,无法被GC释放掉,内存占用严重。解决方案是,使用Kotlin的suspendCancellableCoroutine,注册invokeOnCancellation回调就可以了。当View被Detach的时候,通知协程cancel,并通知Network库及时释放回调资源,看这里

试想一下,如果Kotlin的协程没有结构化并发的设计,能解决以上的这些问题吗。API的设计不仅仅看功能,还得看他需要解决什么问题,业务场景是什么。

还有看这里这个

最后,我们还有个注意的地方

  • withContext,会切换新的线程。并且两个不同的withContext,他们可能会在不同的线程中(6.2.2节的展示)。因为,我们需要保证withContext下的操作不会产生多线程冲突的问题。尽可能在withContext里面只做ajax,或者数据库的单一操作,尽可能不涉及到业务。

7 协程的流

Kotlin中协程的流,是指数据是多次返回,像js中onClickListener的哪种通知方式。

7.1 快速上手

为了对协程的流有一个感性的认识,我们先快速过一遍。代码在这里

7.1.1 flow

package com.example.myapplication.scope

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.lang.RuntimeException
import kotlin.system.measureTimeMillis

fun flowTest1(){
    //闭环的输入,产生被动的输出
    //输入是一个闭包中产生的,输出是被动触发的。
    runBlocking {
        flow{
            for(i in (1..10)){
                emit(i)
            }
        }.collect { v->
            println(v)
        }
    }
    /*
    输出如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     */
}

fun flowTest2(){
    runBlocking {
        var time = measureTimeMillis{
            flow{
                for(i in (1..3)){
                    delay(100)
                    emit(i)
                }
            }.collect { v->
                delay(200)
                println(v)
            }
        }
        //每个数据是100ms+200ms=300ms,共需要900ms的时间
        println("time ${time} ms")
    }
    /*
    输出如下:
    1
    2
    3
    time 931 ms
     */
}

fun flowTest3(){
    //放在闭环输入,和被动输出的好处是
    //当输入或者输出发生异常的时候,发射端和收集端都会自动退出,并清理相关资源
    runBlocking {
        try{
            flow{
                for(i in (1..3)){
                    emit(i)
                    if( i == 2 ){
                        throw RuntimeException("ee")
                    }
                }

            }.collect { v->
                delay(200)
                println(v)
            }
        }catch(e:Exception){
            e.printStackTrace()
        }
    }
    /*
    输出如下:
    1
    2
    java.lang.RuntimeException: ee
        at com.example.myapplication.scope.FlowTestKt$flowTest3$1$1.invokeSuspend(FlowTest.kt:72)
     */
}

fun flowTest4(){
    //放在闭环输入,和被动输出的另外一个好处是:
    //受到结构化并发的控制,能附带将整个发射端,和收集端都cancel掉
    runBlocking {
        val job = launch {
            flow {
                for (i in (1..3)) {
                    emit(i)
                    delay(100)
                }
            }.collect { v ->
                delay(200)
                println(v)
            }
        }
        delay(400)
        job.cancel()
    }
    /*
    输出如下:
    1
     */
}


fun FlowTest_Go(){
    //flowTest1()
    //flowTest2()
    //flowTest3()
    flowTest4()
}

flow要点如下:

  • 闭环输入,被动输出
  • 每个事件,发射端和收集端都处理完成以后,下一个事件才能emit
  • emit端结束,collect会自动退出
  • 在发射或者收集端任意一处异常,都会导致整个流发生异常。
  • 在发射或者收集端任意一处取消,都会导致整个流发生取消。

7.1.2 channel

package com.example.myapplication.scope

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import java.lang.RuntimeException
import kotlin.system.measureTimeMillis

fun channelTest1(){
    //Channel的区别是开环输入,主动输出
    //输入是允许在不同时机,不同闭包中产生的,输出是主动触发的。
    //发送和接收的地方可以在多个闭包里面,不同的协程里面都无所谓。
    //唯一的限制是,发送和接收都在线的时候,通信才会出现。任意一方缺失的时候,都会堵塞
    runBlocking {
        var chan = Channel<Int>()
        launch{
            for( i in (1..5)){
                chan.send(i)
            }
        }
        launch{
            for( i in (1..5)){
                chan.send(i+10)
            }
        }
        launch {
            repeat(2){
                repeat(5){
                    val data = chan.receive()
                    println(data)
                }
            }
        }
    }
    /*
    输出如下:
    1
    11
    2
    3
    12
    4
    5
    13
    14
    15
     */
}

fun channelTest2(){
    runBlocking {
        val time = measureTimeMillis {
            var chan = Channel<Int>()
            coroutineScope {
                launch{
                    for( i in (1..3)){
                        delay(100)
                        chan.send(i)
                    }
                }
                launch {
                    for( i in (1..3)){
                        delay(200)
                        val data = chan.receive()
                        println(data)
                    }
                }
            }
        }
        //总共用了200ms*3 = 600ms,注意与flowTest2的不同。
        //Flow,emit的返回需要等待collect的完成
        //Channel,send的返回只需要对方调用了receive就可以了,不需要等待对方完成receive的整个任务
        println("all time ${time} ms")
    }
    /*
    输出如下:
    1
    2
    3
    all time 622 ms
     */
}

fun channelTest3(){
    //开环输入,和主动输出的时候是
    //输入端的异常,不会让channel自动关闭,也不会让接收端知道异常而退出
    //Channel就像无法感知到异常发生了一样
    //两者独立运行
    runBlocking {
        val chann = Channel<Int>()
        supervisorScope {
            launch{
                for(i in (1..3)){
                    chann.send(i)
                    if( i == 2 ){
                        throw RuntimeException("ee")
                    }
                }
            }
            launch {
                    for (i in (1..3)) {
                        val v = chann.receive()
                        println(v)
                    }
                    println("finish success")
            }
            delay(300)
            //推送最后一个数据,通知接收端关闭
            chann.send(3)
        }
    }
    /*
    输出如下:
    1
    Exception in thread "main" java.lang.RuntimeException: ee
        at com.example.myapplication.scope.ChannelTestKt$channelTest3$1$1$1.invokeSuspend(ChannelTest.kt:97)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
        at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
        at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
        at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
        at com.example.myapplication.scope.ChannelTestKt.channelTest3(ChannelTest.kt:90)
        at com.example.myapplication.scope.ChannelTestKt.ChannelTest_Go(ChannelTest.kt:145)
        at com.example.myapplication.MainTestKt.main(MainTest.kt:7)
        at com.example.myapplication.MainTestKt.main(MainTest.kt)
    2
    3
    finish success
     */
}

fun channelTest4(){
    //开环输入,和主动输出的时候是
    //对发射端的cancel,并不会触发接收端的cancel
    //两者独立运行
    runBlocking {
        val chann = Channel<Int>()
        val job = launch{
            for(i in (1..3)){
                delay(100)
                chann.send(i)
            }
        }
        launch {
            for (i in (1..3)) {
                val v = chann.receive()
                println(v)
            }
            println("finish success")
        }
        delay(210)
        job.cancel()
        //推送最后一个数据,通知接收端关闭
        chann.send(3)
    }
    /*
    输出如下:
    1
    2
    3
    finish success
     */
}


fun ChannelTest_Go() {
    //channelTest1()
    //channelTest2()
    //channelTest3()
    channelTest4()
}

Channel的要点如下:

  • 开环输入,主动输出
  • 每个事件,发射端在send和收集端在receive,才能触发,否则会导致对端堵塞。相对flow的好处在于,发射端不需要等待收集端整个任务的完成。
  • send端的close,会触发recevie的退出通知
  • 两者独立,在发射或者收集端任意一处异常,不会导致整个流发生异常。
  • 两者独立,在发射或者收集端任意一处取消,不会导致整个流发生取消。

7.1.3 channel.recevieAsFlow

package com.example.myapplication.scope

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.receiveAsFlow
import java.lang.RuntimeException
import kotlin.system.measureTimeMillis

fun channelAsFlowTest1(){
    //ChannelAsFlow,recieveAsFlow,可以将flow的输入从闭环输入,改为开环输入
    //当Channel触发的时候,触发flow的emit,并触发flow的collect
    //receiveAsFlow转换为flow的时候,多个collect同时接收事件,每个事件仅被一个collect处理
    runBlocking {
        var chan = Channel<Int>()
        val receiveFlow = chan.receiveAsFlow()
        //没有collect的发送,发送会堵塞
        //chan.send(100)
        launch{
            for( i in (1..5)){
                chan.send(i)
            }
            //显式调用close,才能结束所有的collect
            chan.close()
        }
        launch{
            receiveFlow.collect { v->
                println("collect#1 ${v}")
            }
        }
        launch{
            receiveFlow.collect { v->
                println("collect#2 ${v}")
            }
        }
    }
    /*
    输出如下:
    collect#1 1
    collect#1 2
    collect#1 4
    collect#2 3
    collect#1 5
     */
}

fun channelAsFlowTest1_2(){
    //consumeAsFlow,与receiveAsFlow的区别在于,consumeAsFlow只支持一个collect来接收数据
    runBlocking {
        var chan = Channel<Int>()
        val receiveFlow = chan.consumeAsFlow()
        //没有collect的发送,发送会堵塞
        //chan.send(123)
        launch{
            for( i in (1..5)){
                chan.send(i)
            }
            chan.close()
        }
        launch{
            receiveFlow.collect { v->
                println("collect#1 ${v}")
            }
        }
        /*
        //第二次collect会抛出异常
        launch{
            receiveFlow.collect { v->
                println("collect#2 ${v}")
            }
        }
         */
    }
    /*
    输出如下:
    collect#1 1
    collect#1 2
    collect#1 3
    collect#1 4
    collect#1 5
     */
}

fun channelAsFlowTest2(){
    runBlocking {
        val time = measureTimeMillis {
            var chan = Channel<Int>()
            val flow = chan.receiveAsFlow()
            coroutineScope {
                launch{
                    for( i in (1..3)){
                        delay(100)
                        chan.send(i)
                    }
                    chan.close()
                }
                launch {
                    flow.collect { v->
                        delay(200)
                        println(v)
                    }
                }
            }
        }
        //总共用了200ms*3 = 600ms+100ms= 700ms,注意与channelTest2的不同
        //因为collect的触发,需要等待chann的第一次send的触发
        println("all time ${time} ms")
    }
    /*
    输出如下:
    1
    2
    3
    all time 736 ms
     */
}

fun channelAsFlowTest3(){
    runBlocking {
        val chann = Channel<Int>()
        val flow = chann.receiveAsFlow()
        //两者独立运行,send的崩溃,不影响collect的异常
        supervisorScope {
            launch{
                for(i in (1..3)){
                    chann.send(i)
                    if( i == 2 ){
                        throw RuntimeException("ee")
                    }
                }
            }
            val job = launch {
                flow.collect { v->
                    println(v)
                }
                println("finish success")
            }
            delay(300)
            job.cancel()
        }
    }
    /*
    输出如下:
    1
    Exception in thread "main" java.lang.RuntimeException: ee
        at com.example.myapplication.scope.ChannelAsFlowKt$channelAsFlowTest3$1$1$1.invokeSuspend(ChannelAsFlow.kt:126)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
        at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
        at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
        at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
        at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
        at com.example.myapplication.scope.ChannelAsFlowKt.channelAsFlowTest3(ChannelAsFlow.kt:117)
        at com.example.myapplication.scope.ChannelAsFlowKt.ChannelAsFlowTest_Go(ChannelAsFlow.kt:169)
        at com.example.myapplication.MainTestKt.main(MainTest.kt:8)
        at com.example.myapplication.MainTestKt.main(MainTest.kt)
    2
     */
}

fun channelAsFlowTest4(){
    //两者独立运行,send的cancel,不影响collect的cancel
    runBlocking {
        val chann = Channel<Int>()
        val flow = chann.receiveAsFlow()
        val job = launch{
            for(i in (1..3)){
                delay(100)
                chann.send(i)
            }
        }
        val job2 = launch {
            flow.collect { v->
                println(v)
            }
        }
        delay(210)
        job.cancel()
        delay(220)
        job2.cancel()
    }
    /*
    输出如下:
    1
    2
     */
}

fun ChannelAsFlowTest_Go(){
    //channelAsFlowTest1()
    channelAsFlowTest1_2()
    //channelAsFlowTest2()
    //channelAsFlowTest3()
    //channelAsFlowTest4()
}

Channel转换为flow的要点如下:

  • 使得flow从闭环输入,转换到了开环输入。
  • receiveAsFlow支持多个collect,consumeAsFlow只支持单个collect。每个事件仅能被消费一次,无论多少个collect
  • 每个事件,发射端在send和收集端在collect,才能触发,否则会导致发送端send堵塞。相对flow的好处在于,发射端不需要等待收集端整个任务的完成。
  • send端的close,会触发collect端的自动退出
  • 两者独立,在发射或者收集端任意一处异常,不会导致整个流发生异常。
  • 两者独立,在发射或者收集端任意一处取消,不会导致整个流发生取消。

7.1.4 flow.shareIn

package com.example.myapplication.scope

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import java.lang.RuntimeException
import kotlin.system.measureTimeMillis

fun flowAsSharedTest1(){
    //flow.shareIn,和stateIn的特点在于,将单被动输出,改为多被动输出
    //多个闭合输出之间,当flow发送某个数据的时候,所有collect都能收到同样的数据(订阅模式)
    //如果某个collect是延后才参与到flow的时候,它只会收到之后的数据(取决于relay参数)
    //如果某个collect是中途就退出到flow的时候,它就会丢掉部分数据,也不会堵塞flow的emit
    //这种模式下的collect端,不会随着flow的关闭而自动关闭
    runBlocking {
        val globalJob = launch {
            val myFlow = flow{
                for( i in (1..5)){
                    delay(100)
                    emit(i)
                }
            }
            //Lazily的方式是,当第一个collect的时候,才去触发flow的启动
            val sharedFlow = myFlow.shareIn(GlobalScope,SharingStarted.Lazily,0)
            launch{
                sharedFlow.collect { v->
                    println("collect#1 ${v}")
                }
            }

            launch{
                delay(200)
                //延迟启动
                sharedFlow.collect { v->
                    println("shared#2 ${v}")
                }
            }
            val job = launch{
                //提前结束
                sharedFlow.collect { v->
                    println("shared#3 ${v}")
                }
            }
            delay(300)
            job.cancel()
        }

        delay(700)
        globalJob.cancel()
    }
    /*
    输出如下:
    collect#1 1
    shared#3 1
    collect#1 2
    shared#3 2
    shared#2 2
    collect#1 3
    shared#2 3
    collect#1 4
    shared#2 4
    collect#1 5
    shared#2 5

    Process finished with exit code 0
     */
}

fun flowAsSharedTest1_2(){
    runBlocking {
        val myFlow = flow{
            for( i in (1..5)){
                delay(100)
                emit(i)
            }
        }
        //stateIn与sharedIn很相似,订阅模式,多闭合输出,可以延后进入,可以中途退出
        //stateIn的唯一区别是,它记录state的初始值,和最后一个值
        //对于首次进入的collect,它会先推送initialValue,然后推送flow的emit值
        //对于延后进入的collect,它会先推送flow的最后一个值,然后推送flow后续的emit值
        val stateFlow = myFlow.stateIn(GlobalScope,SharingStarted.Lazily,100)
        launch{
            stateFlow.collect { v->
                println("collect#1 ${v}")
            }
        }

        launch{
            delay(200)
            //延迟启动
            stateFlow.collect { v->
                println("shared#2 ${v}")
            }
        }
        val job = launch{
            //提前结束
            stateFlow.collect { v->
                println("shared#3 ${v}")
            }
        }
        delay(100)
        job.cancel()
    }
    /*
    输出如下:
    collect#1 100
    shared#3 100
    collect#1 1
    shared#2 1
    collect#1 2
    shared#2 2
    collect#1 3
    shared#2 3
    collect#1 4
    shared#2 4
    collect#1 5
    shared#2 5
     */
}

fun flowAsSharedTest2(){
    runBlocking {
        val time = measureTimeMillis {
            val myFlow = flow{
                for( i in (1..3)){
                    delay(100)
                    emit(i)
                }
            }
            val sharedFlow = myFlow.shareIn(GlobalScope, SharingStarted.Lazily,0)
            coroutineScope {
                val channel = Channel<Int>()
                val job = launch{
                    var i = 0
                    sharedFlow.collect {v->
                        delay(200)
                        println(v)
                        i++
                        if( i == 3 ){
                            channel.send(0)
                        }
                    }
                }
                channel.receive()
                job.cancel()
            }
        }
        //由于collect不会自动退出,所以要辅助cancel掉
        //总共用了200ms*3 = 600ms+100ms= 700ms,注意与channelTest2的不同
        println("all time ${time} ms")
    }
    /*
    输出如下:
    1
    2
    3
    all time 757 ms
     */
}

fun flowAsSharedTest3(){
    //Flow的发射在GlobalScope作用域,两者独立运行,它的崩溃与collect作用域无关
    runBlocking {
        val myFlow = flow{
            for( i in (1..3)){
                emit(i)
                if( i == 2 ){
                    throw RuntimeException("ee")
                }
            }
        }
        val sharedFlow = myFlow.shareIn(GlobalScope, SharingStarted.Lazily,0)
        launch{
            var i = 0
            sharedFlow.collect {v->
                println(v)
                i++
            }
        }
    }
    /*
    输出如下:
    1
    2
    Exception in thread "DefaultDispatcher-worker-1" java.lang.RuntimeException: ee
     */
}

fun flowAsSharedTest4(){
    //Flow的发射在CoroutineScope作用域,两者独立运行,它的cancel与collect作用域无关
    runBlocking {
        val job = Job()
        val myScope = CoroutineScope(job)
        val myFlow = flow{
            for( i in (1..3)){
                emit(i)
            }
        }
        val sharedFlow = myFlow.shareIn(myScope, SharingStarted.Lazily,0)
        launch{
            try {
                var i = 0
                sharedFlow.collect { v ->
                    println(v)
                    i++
                }
            }catch(e:Exception){
                e.printStackTrace()
            }
        }
        launch {
            job.cancel()
        }
    }
    /*
    输出如下
    1
     */
}

fun FlowAsSharedTest_Go() {
    //flowAsSharedTest1()
    //flowAsSharedTest1_2()
    //flowAsSharedTest2()
    //flowAsSharedTest3()
    flowAsSharedTest4()
}

Flow转换为shareFlow的要点如下:

  • 使得flow从单被动输出,转换到了多被动输出。相当于订阅模式。
  • shareIn,支持relay,延迟进入的collect端能得到最后的多个事件回放。stateIn是shareIn的派生版本,自带首次状态的通知,和最后一次状态的回放。
  • shareFlow的发射不需要等待collect,emit端总是不堵塞。emit端可以延后进入,或者中途退出,都不会影响emit端的运行。
  • emit端的结束,不会触发collect端的自动退出。(这点和之前的都不一样)
  • 两者独立,在发射或者收集端任意一处异常,不会导致整个流发生异常。
  • 两者独立,在发射或者收集端任意一处取消,不会导致整个流发生取消。

7.1.5 小结

用图形给与以上的一个感性认识:

  • Flow,相当于Button.addClickListener,发送与接收处理都是同步的。
  • Channel,相当于golang的channel。
  • Channel.receiveAsFlow,开环输入,被动输出,(生产消费模型)
  • Flow.shareIn,闭环输入,被动输出,(发布订阅模型)

7.2 创建流

后续的代码在这里

package com.example.myapplication.scope

import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun createFlow1(){
    runBlocking {
        //使用函数的方式创建流
        val myFlow = flow { // 流构建器
            for (i in 1..3) {
                delay(100) // 假装我们在这里做了一些有用的事情
                emit(i) // 发送下一个值
            }
        }
        launch {
            myFlow.collect { v->
                println(v)
            }
        }
    }
    /*
    输出如下:
    1
    2
    3
     */
}

fun createFlow2(){
    runBlocking {
        //使用asFlow创建流
        val myFlow = (1..3).asFlow()
        launch {
            myFlow.collect { v->
                println(v)
            }
        }
    }
}

fun myCallbackFlow() = callbackFlow<Int> {
    println("in thread")
    val thread = Thread{

        for(i in (1..3)){
            Thread.sleep(100)
            //往外吐出数据
            trySendBlocking(i)
                .onFailure {
                    println("oh fail")
                }

        }
        close()
    }
    thread.start()
    //awaitClose这一句会堵塞
    awaitClose {
        //收到flow的cancel,或者close消息
        thread.interrupt()
    }
    println("out thread")
}

fun createFlow3(){
    runBlocking {
        //使用callbackFlow创建流
        val myFlow = myCallbackFlow()
        launch {
            myFlow.collect { v->
                println(v)
            }
        }
    }
    /*
    输出如下:
    in thread
    1
    2
    3
     */
}

fun CreateFlowTest_Go() {
    //createFlow1()
    //createFlow2()
    createFlow3()
}

创建流的三种方式:

  • 在闭包中emit数据
  • 用asFlow将集合转换为流
  • callbackFlow,对非协程的代码转换为流

7.3 流转换

package com.example.myapplication.scope

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun flowMapAndFilter(){
    runBlocking {
        val myFlow = (1..10).asFlow()
        myFlow.map{v->v+10}
            .filter { v-> v%2 ==0 }
            .collect { v->
                println(v)
            }
    }
    /*
    输出如下:
    12
    14
    16
    18
    20
     */
}

fun flowTransform(){
    runBlocking {
        val myFlow = (1..3).asFlow()
        myFlow.transform{v->
                emit("first ${v+10}")
                delay(100)
                emit("second ${v+20}")
            }
            .collect { v->
                println(v)
            }
    }
    /*
    输出如下:
    first 11
    second 21
    first 12
    second 22
    first 13
    second 23
     */
}

fun flowFlatMerge(){
    runBlocking {
        val myFlow = (1..3).asFlow()
        myFlow.flatMapMerge{v->
                flow<String>{
                    emit("first ${v+10}")
                    delay(100)
                    emit("second ${v+20}")
                }
            }
            .collect { v->
                println(v)
            }
    }
    /*
    输出如下:
    first 11
    first 12
    first 13
    second 21
    second 22
    second 23
     */
}

fun FlowConvertTest_Go(){
    //flowMapAndFilter()
    //flowTransform()
    flowFlatMerge()
}

流转换,包括有:

  • map与filter,这两个没啥好说的
  • transform,传入流的每个事件,然后用emit产生新的事件。
  • flatMerge,传入流的每个事件,返回一个新的流,将新流的所有事件合并在一起重新emit

7.4 流收集

package com.example.myapplication.scope

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun flowCollect(){
    runBlocking {
        //使用collect收集,阻塞,并等待结束
        val myFlow = (1..3).asFlow()
        launch {
            myFlow.collect { v->
                println(v)
            }
        }
    }
    /*
    输出如下:
    1
    2
    3
     */
}

fun flowLaunch(){
    runBlocking {
        //使用launchIn收集,非阻塞,异步
        val myFlow = (1..3).asFlow()
        val job = myFlow
            .onEach { v->
                delay(100)
                println(v)
            }
            .launchIn(this)
        println("start")
        job.join()
        println("end")
    }
    /*
    输出如下:
    start
    1
    2
    3
    end
     */
}


fun flowToList(){
    runBlocking {
        //使用asFlow创建流
        val myFlow = (1..3).asFlow()
        launch {
            val data = myFlow.toList()
            println(data)
        }
    }
    /*
    输出如下
    [1, 2, 3]
     */
}

fun FlowCollectTest_Go() {
    //flowCollect()
    //flowLaunch()
    flowToList()
}

流收集

  • collect,阻塞式收集流事件
  • launchIn,非阻塞方式收集流事件,返回job,可随时cancel这个collect
  • toList,将事件转换为集合

7.5 上下文

package com.example.myapplication.scope

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking

fun flowOnTest(){
    runBlocking {
        val myFlow = flow{
            for( i in (1..3)){
                Thread.sleep(100)
                emit(i)
            }
        }
            //flowOn之前的运行在Default的协程
            .flowOn(Dispatchers.Default)
            //flowOn之后的还是运行在runBlocking{}的协程里面
            .collect { v->
                println(v)
            }
    }
    /*
    输出如下:
    1
    2
    3
     */
}
fun FlowContextTest_Go() {
    flowOnTest()
}

默认情况下,流的发射端和接收端,都发生在调用所在的协程上下文中。我们可以用flowOn来改变发射端的上下文。注意,flowOn,只改变之前操作符所在的上下文,不改变之后操作符的上下文。

7.6 背压

package com.example.myapplication.scope

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlin.system.measureTimeMillis

fun flowTestLongCollect(){
    //背压问题,发射与收集端的速率不一致,导致发射方需要停下来等待收集端处理完毕以后才能发射下一个
    runBlocking {
        val time = measureTimeMillis {
            flow{
                for( i in (0..3)){
                    delay(100)
                    emit(i)
                }
            }.collect {
                delay(200)
                println(it)
            }
        }
        //每个元素需要300ms,共4个元素,需要1200ms
        println("all Time ${time}")
    }
    /*
    输出如下:
    0
    1
    2
    3
    all Time 1245
     */
}

fun flowBuffer(){
    //从发射方解决背压问题,将发射方的数据缓存起来,然后马上发射下一个
    runBlocking {
        val time = measureTimeMillis {
            flow{
                for( i in (0..3)){
                    delay(100)
                    emit(i)
                }
            }.buffer()
                .collect {
                delay(200)
                println(it)
            }
        }
        //每个元素collect端需要200ms,共4个元素,需要800ms
        //另外首次发射需要100ms,所以共900ms
        println("all Time ${time}")
    }
    /*
    输出如下:
    0
    1
    2
    3
    all Time 967
     */
}

fun flowConflate(){
    //从发射方解决背压问题,将发射方数据尝试发射,如果收集端阻塞,就用最新的数据覆盖整个缓存区的数据
    //从而让收集端只处理最新的数据,中间的数据跳过去了
    runBlocking {
        val time = measureTimeMillis {
            flow{
                for( i in (0..3)){
                    delay(100)
                    emit(i)
                }
            }.conflate()
                .collect {
                    delay(200)
                    println(it)
                }
        }
        //只有3个元素实际被处理了,每个处理时间为200ms,共600ms
        //加入首次发射的100ms,共700ms
        println("all Time ${time}")
    }
    /*
    输出如下:
    0
    1
    3
    all Time 761
     */
}

fun flowLatest() {
    //collectLatest是从收集方解决问题,如果新数据到来了,但是收集方还没处理完上次的数据
    // 就直接cancel收集方,让收集方马上立即处理最新数据
    //注意collectLatest与conflate的不同
    runBlocking {
        val time = measureTimeMillis {
            flow{
                for( i in (0..3)){
                    delay(100)
                    emit(i)
                }
            }
                .collectLatest {
                    delay(200)
                    println(it)
                }
        }
        //发射元素为4个,每个100ms,共400ms
        //只处理了最后一个元素,处理时间为200ms,共600ms
        println("all Time ${time}")
    }
    /*
    输出如下:
    3
    all Time 657
     */
}

fun FlowBackPressureTest_Go() {
    //flowTestLongCollect()
    //flowBuffer()
    //flowConflate()
    flowLatest()
}

因为流的默认操作是,每个事件需要等待收集端完全处理完毕以后,才能emit下一个事件。所以,当收集端的速率很慢的时候,发射端的速率就会被迫下降,这称为背压现象。解决方法有:

  • buffer,发射端事件缓存在内存中,尽量避免堵塞发射端
  • conflate,发射端事件缓存在内存中,但是只缓存最新的未处理数据。
  • conflate,接收端遇到新数据的时候,立即cancel当前的事件,改为只处理最新数据。

7.7 异常

package com.example.myapplication.scope

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.lang.Exception
import java.lang.RuntimeException

fun testFlowCatchDefault(){
    runBlocking {
        flow{
            //看起来代码只是catch了发射方的异常
            //但是,实际运行,会连发射方的异常都收集了
            //根本原因是,flow是闭环输入,被动输出,emit里面会执行collect的代码
            try{
                for( i in (1..3)){
                    emit(i)
                }
            }catch(e:Exception){
                println("all catch")
                e.printStackTrace()
            }
        }.collect {
            println(it)
            if( it == 2 ){
                throw RuntimeException("ee")
            }
        }
    }
    /*
    输出如下:
    1
    2
    all catch
    java.lang.RuntimeException: ee
        at com.example.myapplication.scope.FlowCatchKt$testFlowCatchDefault$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:137)
        at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
        at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
     */
}

fun testFlowCatchOperation(){
    runBlocking {
        try{
            flow{
                for( i in (1..3)){
                    emit(i)
                }
                //catch操作符只会收集在操作符之前的异常的操作
                //因此不会收集collect里面的异常
            }.catch {e->
                println("emit catch")
                e.printStackTrace()
            }.collect {
                println(it)
                if( it == 2 ){
                    throw RuntimeException("ee")
                }
            }
            //collect的异常需要用外部的try-catch来捕捉
        }catch( e:Exception){
            println("collect catch")
            e.printStackTrace()
        }
    }
    /*
    输出如下:
    1
    2
    collect catch
    java.lang.RuntimeException: ee
        at com.example.myapplication.scope.FlowCatchKt$testFlowCatchOperation$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:137)
        at kotlinx.coroutines.flow.FlowKt__ErrorsKt$catchImpl$$inlined$collect$1.emit(Collect.kt:136)
        at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
        at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
        at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:77)
        at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:59)
     */
}

fun FlowCatchTest_Go() {
    testFlowCatchDefault()
    //testFlowCatchOperation()
}

Kotlin的异常处理与直观的想法并不一样,对发射端的catch会导致捕捉接收端的异常。解决方法是,使用声明式的异常捕捉,catch操作符。

7.8 Channel语法糖

package com.example.myapplication.scope

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ChannelResult
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun channelBasic(){
    runBlocking {
        val channel = Channel<Int>()
        launch {
            for( i in (1..3)){
                channel.send(i)
            }
            channel.close()
        }
        launch {
            while( true){
                val receiveResult = channel.receiveCatching()
                if( receiveResult.isClosed ){
                    break
                }
                println(receiveResult.getOrThrow())
            }
        }
    }
    /*
    输出如下:
    1
    2
    3
     */
}

fun channelForSugar(){
    runBlocking {
        val channel = Channel<Int>()
        launch {
            for( i in (1..3)){
                channel.send(i+1)
            }
            channel.close()
        }
        launch {
            for( data in channel ){
                println(data)
            }
        }
    }
    /*
    输出如下:
    2
    3
    4
     */
}

fun channelProduceProduce(){
    runBlocking {
        val channel = produce {
            for( i in (1..3)){
                send(i+2)
            }
        }
        launch {
            for( data in channel ){
                println(data)
            }
        }
    }
    /*
    输出如下:
    3
    4
    5
     */
}

fun ChannelSugarTest_Go() {
    //channelBasic()
    //channelForSugar()
    channelProduceProduce()
}

Channel的语法糖

  • produce,用闭包方式生成一个Channel发射流
  • for,从头到尾接收所有的Channel数据

7.9 Android配合指引

Android中使用到flow协程的例子有:

  • 数据库变更推送,使用flow创建一个数据库倾听任务,并不断接收变更消息。
  • 下载数据的进度推送,使用flow创建一个下载任务,并不断接收下载任务的进度更新信息。
  • 全局EventBus,EventBus显然是发布订阅模式。所以用flow.shareIn
  • ViewModel推送到View的数据更新,由于数据是带状态,而不是事件的,只需要记录最新状态,中途状态可以直接丢失。所以用flow.stateIn。
  • ViewModel推送到View的副作用,由于副作用是不能被合并的,必须被处理,所以用Channel。

参考资料:

8 总结

Kotlin的语法糖真的很多,也不知道好事还是坏事。

Kotlin比较大的改进是:

  • 内联和泛型实化,这点真的有必要
  • 协程,UI开发的利器。
  • 非空安全,这点很有必要

总体来说,为了解决UI开发中的各种边沿问题(内存泄漏,回调时机不一致),Kotlin协程的API设计异常复杂,也提出了额外的结构化并发来解决它。隐约觉得,这个设计过于臃肿,有被推倒重来的可能。

参考资料:

相关文章