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 "com.android.tools.build:gradle:7.0.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
classpath
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
(type: Delete) {
task clean.buildDir
delete rootProject}
在项目级别的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;
= 5;
b }
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(){
(maxNumber(1,2));
println(maxNumber2(2,3));
println(minNumber(
println= 20,
right = 10
left ))
}
函数的写法也比较简单,注意对于单个语句的函数,可以省略括号,直接用等于号就可以了。
注意调用参数的时候,可以使用命令参数的传递方式,比较方便。这种命令的方式来传递参数也适用于构造函数。
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{
.startsWith("Fish_")->100
name//不需要用equals来比较
== "Jim"->200
name else->-1
}
}
fun ConditionTest_Go(){
(IfConfidition(1,2))
println(IfConfidition2(2,3))
println(IfConfidition3(3,4))
println
(whenCondition(2))
println(whenConditionForType(1.1))
println(whenConditionForExpression("Fish_gg"))
println}
重点如下:
- 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(){
(whileLoop())
println(forLoop())
println(forLoop2())
println(forLoop3())
println(forLoop4())
println}
重点如下:
- 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){
("person.name = ${a.name}")
println}
//参数类型为Person?的时候,才能包含有null的这种情况
fun go2(a:Person?){
//这种情况下,需要先判断null,再获取字段
if( a != null){
("person.name2 = ${a.name}")
println}
//kotlin有省事的?判断操作符
("person.name2 = ${a?.name}")
println}
fun go3(a:Person?){
//这种情况下,需要先判断null,再获取字段,否则取默认值
var result = "";
if( a != null){
= a.name;
result }else{
= "[null]";
result }
("person.name3 = ${result}")
println
//kotlin有省事的?判断操作符,再结合省事的?:二元操作符来表达
("person.name3 = ${a?.name?:"[null]"}")
println}
fun go4(a:Person?){
//!!是一个危险的操作,强行指定这个变量是非空的,出了问题由开发者自己兜底
("person.name4 = ${a!!.name}")
println}
fun OptionTest_Go(){
//编译不通过
//go1(null)
(Person("fish",123))
go1
(null)
go2(Person("fish",123))
go2
(null)
go3(Person("fish",123))
go3
(Person("fish",123))
go4(null)//这里能编译通过,但是运行时抛出NullPointerException
go4
}
非空安全性是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()
("hasInit ${person.isInit()}")
println.set("fish")
person(person.get())
println("hasInit ${person.isInit()}")
println
var person2 = MyPerson()
("hasInit ${person2.isInit()}")
println//这里运行时会报错,因为读取对象的时候,发现对象还没有被初始化
//lateinit property target has not been initialized
(person2.get())
println}
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->
("name ${input.name} ordinal ${input.ordinal}")
println}
(Color.RED)
output(Color.GREEN)
output(Color.BLUE)
output}
enum class Color2(val index:String,val aliasName:String) {
("#FF0000","红色"),
RED("#00FF00","绿色"),
GREEN("#0000FF","蓝色"),
BLUE}
fun enumTest2(){
val output = {input:Color2->
("name ${input.name} ordinal ${input.ordinal} index ${input.index} alias ${input.aliasName}")
println}
(Color2.RED)
output(Color2.GREEN)
output(Color2.BLUE)
output}
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(){
("color getter run")
printlnreturn field
}
set(value) {
("color setter run")
println= value
field }
fun eat(){
(name +" is eating. He is "+ age +" years old")
println}
fun setHeight(height:Float){
this.height = height
}
}
fun ClassTest_Go(){
val p = Person()
.age = 121
p.name = "Fish"
p//失败,因为setter被设置为private
//p.height = "123"
//必须通过指定方法
.setHeight(123f)
p.color = "blue"
p("age = ${p.age} and name = ${p.name} and height = ${p.height} and color = ${p.color}")
println.eat()
p}
基础用法,没啥好说的。
默认情况下,对于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//类实例时初始化的方法
= name.length;
nameLength }
fun show(){
("name ${name} nameLength ${nameLength}")
println}
}
fun ClassInitTest_Go(){
val animal = Animal("Dog");
.show()
animal}
声明类里面的括号,称为主构造函数,该构造函数里面的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(){
("name ${this.name} age ${this.age}")
println}
}
//Student有一个主构造函数,需要显式地调用父类的主构造函数
class Student3(val clazz:String, name2:String, age2:Int): Person3(name2,age2){
fun showClazz(){
("clazz ${this.clazz}");
println//没有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(){
("name ${this.name} age ${this.age}")
println}
}
//对于覆写的成员,也需要指定override关键字
class Student4(val clazz:String, override val name:String, override val age:Int): Person4(name,age){
fun showClazz(){
("clazz ${this.clazz} name: ${this.name} age: ${this.age}")
println}
}
//案例4
//Person5有主构造函数
open class Person5( val name:String, val age:Int){
fun show(){
("name ${this.name} age ${this.age}")
println}
}
//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(){
("clazz ${this.clazz} name: ${this.name} age: ${this.age}")
println}
}
//案例5
//Person6有主构造函数
open class Person6( val name:String, val age:Int){
fun show(){
("name ${this.name} age ${this.age}")
println}
}
//相当特殊的情况
//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(){
("clazz ${this.clazz} name ${this.name} age ${this.age}")
println}
}
//案例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(){
("name ${this.name} age ${this.age}")
println}
}
//相当特殊的情况
//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(){
("clazz ${this.clazz} name ${this.name} age ${this.age}")
println}
}
fun ExtendAndConstructorTest_Go(){
val student2 = Student2();
val student3 = Student3("student_clazz3","fish3",123);
.show();
student3.showClazz();
student3
val student4 = Student4("student_clazz4","fish4",124);
.show();
student4.showClazz();
student4
val student5 = Student5("fish5",125);
.show();
student5.showClazz();
student5
val student5_2 = Student5("fish5",25);
.show();
student5_2.showClazz();
student5_2
val student6 = Student6("student_clazz6",1);
.show();
student6.showClazz();
student6
val student7 = Student7("student_clazz7",1);
.show();
student7.showClazz();
student7}
总共有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(){
("brid fly")
println}
}
fun InterfaceTest_Go(){
val bird = Bird()
.fly()
bird}
接口也没啥好说,注意加上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()
("c = ${myClass.c} d= ${myClass.d}")
println}
权限也没啥好说的,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方法
("a == c 为 "+(a==c))
println("a == b 为 "+(a==b))
println
//data class默认有toString方法
("a.toString "+ a.toString())
println
//data class默认有hashCode方法
("a.hasCode "+ a.hashCode())
println
}
写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(){
("count ${Single.get()}")
println.inc()
Single("count ${Single.get()}")
println}
Java中单例类,Kotlin对应的就是一个简单的object关键字而已,不再需要考虑并发单例的各种问题。
2.8 companion类
package com.example.myapplication
class Util {
fun doAction1(){
("do action1")
println}
companion object{
//伴生对象,一个具体的实例,不是Java的static方法
fun doAction2(){
("do Action2")
println}
//与Java一样的static方法,以方便与Java对象进行交互
@JvmStatic
fun doAction3(){
("do Action3")
println}
}
}
fun CompaionClassTest_Go(){
var util = Util()
.doAction1()
util
.doAction2()
Util.doAction3()
Util}
对于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()
(result2)
showResult2
var result:Result = Success()
(result)
showResult}
密封类也是一种语法糖,强行要求所有实现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()
(car1.getGarageAddress())
println
//内部类无法在外部类之外创建,因为它需要获取外部类的引用
//var car1_out = Garage.Car("kk")
//内部类,需要用外部类引用才能创建,看一下和上一个语句区别
var car1_out = garage.Car("fish")
var garage2 = Garage2("b")
var car2 = garage2.createCar()
//无法获取address,因为是普通的嵌套类
(car2.getGarageAddress())
println
//嵌套类,可以仅仅通过外部类名称就能创建
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){
("animal call [${animal.call()}]")
println}
class MyDog:MyAnimal{
override fun call(): String {
return "dog call"
}
}
fun AnonymousClassTest_Go(){
//明确地创建一个类
(MyDog())
goAnimal
//匿名类
(object :MyAnimal{
goAnimaloverride 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(){
("call ${this.name}")
println}
fun walk(){
("walk")
println}
}
//QDog可以通过继承的方式,复用QAnimal的行为逻辑
class QDog:QAnimal("Dog"){
fun run(){
("Dog run")
println
//继承的问题在于,强约束
//子类可以修改父类的变量,而且每个类只能有一个父类
this.name = "xxx"
}
}
//QAnimal2是不可被继承的
class QAnimal2(var name:String){
fun callName(){
("call ${this.name}")
println}
fun walk(){
("walk")
println}
}
//QDog2复用QAnimal2的另外一种方式是,组合,创建一个内部的Animal类
//然后将QAnimal2的接口手动复制过来,转发给QAnimal2
class QDog2{
private val myAnimal:QAnimal2 = QAnimal2("Dog2")
fun callName(){
.callName()
myAnimal}
fun walk(){
.walk()
myAnimal}
fun run(){
("Dog run")
println
//组合更加灵活,我们可以只转发一部分接口
//另外,一个类允许组合多个类的行为,没有继承带来的强约束问题
//使用这种方法的麻烦在于,当QAnimal2后续新增了行为以后,QDog2都要手动添加对应接口来转发,比较麻烦
//this.name = "xxx"
}
}
interface QAnimal3Interface{
fun callName()
fun walk()
}
//QAnimal3是不可被继承的
class QAnimal3(var name:String):QAnimal3Interface{
override fun callName(){
("call ${this.name}")
println}
override fun walk(){
("walk")
println}
}
//QDog3使用委托机制来实现组合的灵活性,同时避免组合需要手动添加接口的麻烦。
class QDog3:QAnimal3Interface by QAnimal3("Dog3") {
fun run(){
("Dog run")
println
//这个时候,by委托当Dog的接口新增的时候,不需要更改。
}
}
fun ByClassTest_Go(){
val dog1 = QDog()
.walk()
dog1.run()
dog1.callName()
dog1
val dog2 = QDog2()
.walk()
dog2.run()
dog2.callName()
dog2
val dog3 = QDog3()
.walk()
dog3.run()
dog3.callName()
dog3}
在类上使用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){
("${value} has been assigned to '${property.name}' in ${thisRef?.javaClass}")
println}
}
//属性的by委托,就是将属性的读写操作委托到另外一个类去
class ByPropertyExample{
//val声明,只读
val str:String by ReadOnlyPropertyDelegate()
//var声明,可读写
var str2:String by WritePropertyDelegate()
}
fun ByPropertyTest_Go(){
val example = ByPropertyExample()
(example.str)
println(example.str2)
println.str2 = "789"
example}
委托属性,相当于Javascript的defineProperty,对类特定的属性的读写操作进行监控。
2.12.3 内置委托
package com.example.myapplication
import kotlin.properties.Delegates
class example2 {
//lazy,惰性获取变量,变量只能是只读的
//该方法是线程安全的
val no:Int by lazy {
("begin get no")
println200
}
//observable,可观察数据,首个参数的默认值,闭包是修改时候触发的
var name: String by Delegates.observable("wang", {
, oldName, newName ->
kProperty("kProperty:${kProperty.name} | oldName:$oldName | newName:$newName")
println})
//notNull,与lateinit的效果一样,只是支持基元类型
var instance:Int by Delegates.notNull()
}
fun LazyByTest(){
val a = example2()
("after init a")
println(a)
println("get a")
println(a.no)
println(a.no)
println}
fun ObservableByTest(){
val a = example2()
("after init a")
println(a)
println("get a")
println(a.name)
println.name = "bb"
a.name = "cc"
a}
fun NotNullByTest(){
val a = example2()
//这里会产生运行是报错
//println(a.instance)
.instance = 123
a(a.instance)
println}
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")
//读取下标
(a[0])
println
//遍历list,不需要声明single
for( single in a ){
(single)
println}
}
fun ListTest2(){
var b = mutableListOf("a","b")
//可修改集合
.add("c")
b
//读取下标
(b[0])
println
//遍历list
for( single in b ){
(single)
println}
}
fun MapTest(){
var a = mapOf("A" to 1,"B" to 2)
//不可变的Map,不能被修改
//a["B"] = 3;
//读取下标
(a["A"])
println(a["C"])//不存在的时候返回null
println
//遍历map
for( (key,value) in a ){
("key is ${key} value is ${value}")
println}
}
fun MapTest2(){
var a = mutableMapOf("A" to 1,"B" to 2)
//不变的Map,能被修改
["B"] = 3;
a
//读取下标
(a["A"])
println(a["C"])
println
//遍历map
for( (key,value) in a ){
("key is ${key} value is ${value}")
println}
}
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({
:String->fruit.length
fruit})
("r1 is ${r1}")
println
//lambda作为函数的最后一个参数的时候,可以移出到括号外面
val r2 = list.map(){
:String->fruit.length
fruit}
("r1 is ${r2}")
println
//map函数的参数为空的时候,可以省略括号
val r3 = list.map{
:String->fruit.length
fruit}
("r1 is ${r3}")
println
//可以省略lambda的参数类型,由kotlin来自己推导
val r4 = list.map{
->fruit.length
fruit}
("r1 is ${r4}")
println
//使用it关键字,来代表lambda的唯一参数的情况,这样能避免尖括号
val r5 = list.map{
.length
it}
("r1 is ${r5}")
println}
重点有:
- 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,只是需要明确指定一个参数可以
.let{
a->
person("age is ${person.getAge()}")
println("age is ${person.getAge()}")
println}
//等效于以上的效果,用it关键字能更省事一点
.let{
a("age is ${it.getAge()}")
println("age is ${it.getAge()}")
println}
}
fun with_Go(){
var a = Person("fish",123)
//let 关键字,与js的with,完全等价,可以直接调用对象的方法和变量。
// 另外,最后一个语句的值,作为整个with语句的结果。
var newAge = with(a){
()
incAge()
incAge()
getAge}
("new Age ${newAge}")
println}
fun run_Go(){
var a = Person("fish",123)
//与let关键字等价,语法不同而已
var newAge = a.run{
()
incAge()
incAge()
getAge}
("new Age ${newAge}")
println}
fun apply_Go(){
var a = Person("fish",123)
//与let关键字相似,返回值是对象自身,而不是最后一个语句的值
var newA = a.apply{
()
incAge()
incAge()
getAge}
("new Age ${newA}")
println}
fun use_Go(){
val fo = FileOutputStream("data")
val bfo = BufferedOutputStream(fo)
//外层流关闭的时候,内层流就会关闭
//use会在闭包结束的时候自动调用close
.use {
bfo.write("Hello".toByteArray())
bfo}
.close()
bfo}
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"
("lettersCount ${a.lettersCount()}")
println
var b = "b".with("c")
("b ${b}")
println
var c = testClass("fish",123).with("kk")
("c ${c}")
println}
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)
("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}")
println}
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()
.add(1)
list.add(3)
list.add(5)
list("list[1] ${list[1]}")
println[0] = 3
list("list[0] ${list[0]}")
println("3 in list is ${3 in list}")
println("2 in list is ${2 in list}")
println}
fun OperatorTest_Go(){
()
MoneyTest()
MyArrayList_Go
//返回值是IntRange类型
//左闭右闭区间
val target = (1..20)
("IntRange ${target}")
printlnval random = target.random()
("IntRange Random random ${random}")
printlnval newStr = "fish".repeat( random)
("newStr ${newStr}")
println}
要点如下:
- 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{
(num1,num2)
operatorion}
fun print(num1:Int,num2:Int){
("num1 ${num1} and num2 ${num2}")
println}
//一个特殊的闭包,类似apply方式的闭包,stringBuilder是该闭包的this参数
fun build(myBuilder:StringBuilder,block:StringBuilder.()->Unit):StringBuilder{
(myBuilder)
blockreturn myBuilder
}
fun HighOrderFunctionText_Go(){
val a = 10
var b = 20
//传入一个已存在的全局函数,用:;
(a,b,::print)
num1AndNum2//传入一个闭包
(a,b){c,d->
num1AndNum2("c = ${c} and d = ${d}")
println}
val c = StringBuilder()
val d = build(c){
//调用的都是StringBuilder里面的方法
(10)
append("cc")
append}
("d is ${d}")
println}
高阶函数在Javascript里面玩得比较多,也就不多说了。Kotlin另外还支持类似apply的闭包参数传递,这点要注意的。
4.4 内联函数
package com.example.myapplication
/*
inline的优势:
* 不需要进行函数的实际调用
* 附加可以提前整个函数中退出的功能,return
*/
inline fun printStringInline(str:String, block:(String)->Unit){
("printString begin")
println(str)
block("pringString end")
println}
fun printStringNoInline(str:String,block:(String)->Unit){
("printString begin")
println(str)
block("pringString end")
println}
inline fun printStringInline2(str:String, block:(String)->Unit){
("printString begin")
println(str)
block("pringString end")
println}
inline fun printStringInline3(str:String, block:(String)->Unit){
("printString begin")
println//inline的局限性一,inline函数中只能调用inline函数,不能调用noinline函数
// 解决方法是,用noinline关键字包装,例如printStringInline3_2,同时失去闭包的内联能力
//printStringNoInline(str,block)
(str,block)
printStringInline2("pringString end")
println}
inline fun printStringInline3_2(str:String, noinline block:(String)->Unit){
("printString begin")
println//inline的局限性
(str,block)
printStringNoInline("pringString end")
println}
inline fun printStringInline4(str:String, block:(String)->Unit){
("printString begin")
println//inline的局限性二,inline函数中不能将闭包block放在另外一个闭包runnable里面,因为没人清楚闭包runnable什么时候会触发
// 解决方法是,用crossinline关键字包装,例如printStringInline4_2,同时会失去提前return全局函数的能力
/*
var runnable = Runnable {
block(str)
}
*/
("pringString end")
println}
inline fun printStringInline4_2(str:String, crossinline block:(String)->Unit){
("printString begin")
printlnvar runnable = Runnable {
(str)
block}
("pringString end")
println}
fun inline_test1(){
var str = ""
("init test1 begin")
println(str){
printStringInline("lambda start")
printlnif( str.isEmpty()){
//这里的做法相当诡异,与普通语言不同
//在闭包中return不是结束闭包,而是结束整个inline_test1
return
}
("lambda end")
println}
("init test1 end")
println}
fun inline_test2(){
var str = ""
("init test2 begin")
println(str){
printStringInline("lambda start")
printlnif( str.isEmpty()){
//这里的return才是结束闭包
@printStringInline
return}
("lambda end")
println}
("init test2 end")
println}
fun no_inline_test3(){
var str = ""
("init test3 begin")
println(str){
printStringNoInline("lambda start")
printlnif( str.isEmpty()){
//非inline函数,不能实现提前结束no_inline_test3
//return
}
("lambda end")
println}
("init test3 end")
println}
fun no_inline_test4(){
var str = ""
("init test4 begin")
println(str){
printStringInline4_2("lambda start")
printlnif( str.isEmpty()){
//结束闭包
@printStringInline4_2
return//不能使用return,虽然是inline,因为有crossinline关键字
//return
}
("lambda end")
println}
("init test4 end")
println}
fun crossinline_test5(){
var str = ""
("init test4 begin")
println(str){
printStringNoInline("lambda start")
printlnif( str.isEmpty()){
//结束闭包
@printStringNoInline
return}
("lambda end")
println}
("init test4 end")
println}
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->{
("key ${key} is null")
println}
}
}
return result
}
fun VarargFunctionTest_Go(){
val result = cvOf("a" to 1,"b" to "fish","c" to 12,"d" to null)
(result)
println}
要点如下:
- 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"
("a beginsWithFish ${a beginsWithFish "cc"}")
println("b beginsWithFish ${b beginsWithFish "cc"}")
println}
infix 函数也是写法上的语法糖了,可以省略点号,和括号,限制在于:
- 必须是某个类的成员方法或者扩展方法
- 方法有且仅有一个参数
5 泛型
代码在这里
5.1 泛型基础
package com.example.myapplication
fun <T> printTemp(a:T){
(a.toString()+"_temp")
println}
//带泛型上界的声明
fun <T :Person > printPerson(a:T){
(a.name+","+a.age);
println}
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()
("re ${re}")
println}
fun BasicGenericTest_Go(){
("123Hello")
printTemp(78)
printTemp(Person("fish",123))
printTemp
(Person("cat",780))
printPerson(Student())
printPerson
val data1 = SimpleData<Int>()
.set(123)
data1("data1 ${data1.get()}")
println
var data2 = SimpleData<Person>()
.set(Person("cat",879))
data2("data2 ${data2.get()}")
println//这里会报错,因为类型不匹配
//data2.set(123)
//可以传入不同的泛型参数
(data1)
printAllSimpleData(data2)
printAllSimpleData}
泛型基础,这里与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(){
("class1 ${getClass(1)}")
println("class1 ${getClass("fish")}")
println
("class2 ${getClass2<Int>()}")
println("class2 ${getClass2<String>()}")
println}
因为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>的子类
//所以,这句才会正确,编译不报错
= dogTpl
animalTpl ("animalTpl ${animalTpl}")
println
//因为,能返回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>的子类
//所以,这句才会正确,编译不报错
= animalTpl
dogTpl ("animalTpl ${animalTpl}")
println
//因为,能接收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"))
= dogTpl
animalTpl ("animalTpl ${animalTpl}")
println}
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(){
("1")
println//runBlocking是全局方法,启动协程,并且会堵塞当前线程,直至协程作用域的结束
{
runBlocking ("2")
println(100)
delay("3")
println}
("4")
println
//输出为1,2,3,4
}
fun globalScopeLaunchTest(){
("1")
println//GlobalScope是一个单例,也是一个协程作用域,launch是它的方法
//GlobalScope启动后不堵塞当前线程,并且返回一个job方法,可以用来检查协程作用域是否已经结束
val job = GlobalScope.launch {
("2")
println(100)
delay("3")
println}
("4")
println.sleep(200)
Thread//等待协程作用域的结束,join方法
//job.join()
("5")
println
//输出为1,4,2,3,5
}
fun CoroutineScopeLaunchTest(){
("1")
println//先创建一个Job
var job = Job()
//CoroutineScope是一个类,也是一个协程作用域,launch是它的方法
//CoroutineScope与GlobalScope的区别在于,它绑定到一个job上来,而不是返回一个新的job
//因此可以将多个CoroutineScope绑定到同一个job上,我们可以同时用单个job等待或者检查多个CoroutineScope
(job).launch{
CoroutineScope("2")
println(100)
delay("3")
println}
("4")
println.sleep(200)
Thread//等待协程作用域的结束,join方法
//job.join()
("5")
println
//输出为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(){
//这种方法会堵塞协程
.sleep(10)
Thread}
//挂起方法的定义是在方法中加入suspend关键字
//对于js来说,相当于一个async方法
fun go2(){
suspend //这种方法不堵塞协程
(10)
delay}
class MyTimeout{
fun wait(timeout:Long,callback:(target:Int)->Unit){
{
Thread.sleep(timeout)
Thread(100)
callback}.start()
}
}
//suspendCoroutine是一个特殊的方法,可以将协程暂时切换到出去
//当数据有返回的时候,我们才触发continuation.resume来触发数据
//这是一个协程切换到普通线程,普通线程再切换回来的重要方法
//对于js来说,这个方法相当于用Promise包装一个async函数
fun suspendCoroutine_1():Int{
suspend return suspendCoroutine{ continuation->
().wait(100) { result ->
MyTimeout.resume(result)
continuation}
}
}
fun suspendCoroutine_2():Int{
suspend return suspendCoroutine{ continuation->
().wait(100) { result ->
MyTimeout.resumeWithException(Exception("exception1"))
continuation}
}
}
fun suspendCoroutine_3():Int{
suspend return suspendCoroutine{ continuation->
().wait(100) { result ->
MyTimeoutval returnResult:Result<Int> = Result.success(102)
var returnException:Result<Int> = Result.failure(Exception("exception2"))
if( (1..2).random() == 1 ){
.resumeWith(returnResult)
continuation}else{
.resumeWith(returnException)
continuation}
}
}
}
fun testBasic(){
{
runBlocking ("1")
println(100)
delay("2")
println//在协程里面,我们可以调用普通方法
()
go1("3")
println
//我们也可以调用特定的挂起方法
//调用挂起方法的意义在于,我们可以高效的使用协程。
//调用普通方法,遇到堵塞的时候不会让出时间片,但是挂起方法遇到堵塞的时候会自动让出时间片给其他的协程
//注意,这里与js不同。js调用async方法以后,可以await,也可以不await。
// 但是在Kotlin的协程里面,必须await,这种调用方式是隐式的,这种方法其实避免隐晦的bug,但是带来是不灵活的代价
()
go2("4")
println
val a = suspendCoroutine_1()
("a is ${a}")
println("5")
println
try{
val b = suspendCoroutine_2()
}catch(e:Exception){
.printStackTrace()
e("6")
println}
try{
val c = suspendCoroutine_3()
("7 c is ${c}")
println}catch(e:Exception){
.printStackTrace()
e("7")
println}
}
}
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才能调用
("1")
println//launch就是启动一个协程,不堵塞当前协程,无返回值
// 但是launch的运行周期受子协程影响,结构性并发
var job = launch{
("2")
println{
launch ("3.1")
println(100)
delay("3.2")
println}
{
launch ("4.1")
println(200)
delay("4.2")
println}
("5")
println}
("6")
println.join()
job("7")
println//输出如下
/*
1
6
2
5
3.1
4.1
3.2
4.2
7
*/
}
}
fun MyAsync_Go(){
{
runBlocking //async其实是CoroutineScope的方法,外层必须有CoroutineScope才能调用
("1")
println//async就是启动一个协程,不堵塞当前协程,有返回值
// 但是async的运行周期受子协程影响,结构性并发
var job = launch{
("2")
printlnvar a1 = async {
("3.1")
println(100)
delay("3.2")
println"1"
}
var a2 = async {
("4.1")
println(200)
delay("4.2")
println"2"
}
("5")
println//依然可以用join,但是没有返回值
//a1.join()
//对于async启动的块,我们可以用await获取返回值
("a1 is ${a1.await()}")
println("a2 is ${a2.await()}")
println}
("6")
println.join()
job("7")
println//输出如下
/*
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才能调用
("1")
println//withContext就是启动一个协程,堵塞当前协程,有返回值
//withContext与async很相似,不同之处在于,马上堵塞协程,且必须指定Dispatcher
// 但是async的运行周期受子协程影响,结构性并发
var job = launch{
("2")
println//参数指分派到IO线程池上执行
var a1 = withContext(Dispatchers.IO) {
("3.1")
println(100)
delay("3.2")
println"1"
}
//和上面的写法是一致的
var a2 = async(Dispatchers.IO) {
("4.1")
println(200)
delay("4.2")
println"2"
}.await()
("5")
println("a1 is ${a1}")
println("a2 is ${a2}")
println}
("6")
println.join()
job("7")
println//输出如下
/*
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
fun mySuspend2(){
suspend val job = coroutineScope {
{
launch("3.1")
println(100)
delay("3.2")
println}
{
async("4.1")
println(200)
delay("4.2")
println"HAHA"
}
}
//coroutineScope的特点是隐式的join,无论你有没有调用,他都需要等待子协程结束才能返回
//job.join()
}
fun MySuspendLanuch_Go(){
{
runBlocking ("1")
println//mySuspend()
("2")
println()
mySuspend2("5")
println/*
输出结果:
1
2
3.1
4.1
3.2
4.2
5
*/
}
}
fun MySuspendLanuch2_Go(){
{
runBlocking ("1")
println//mySuspend()
("2")
printlnval job = coroutineScope {
{
launch("3.1")
println(100)
delay("3.2")
println}
{
async("4.1")
println(200)
delay("4.2")
println"HAHA"
}
}
//coroutineScope的特点是隐式的join,无论你有没有调用,他都需要等待子协程结束才能返回
//即使不在suspend方法中,它依然是这样
//job.join()
("5")
println/*
输出结果:
1
2
3.1
4.1
3.2
4.2
3
*/
}
}
fun mySuspend3(){
suspend //supervisorScope与coroutineScope相似的工作方式,区别在于发生异常时的处理方式不同
val job = supervisorScope {
{
launch("3.1")
println(100)
delay("3.2")
println}
{
async("4.1")
println(200)
delay("4.2")
println"HAHA"
}
}
//supervisorScope的特点是隐式的join,无论你有没有调用,他都需要等待子协程结束才能返回
//job.join()
}
fun MySuspendLanuch3_Go(){
{
runBlocking ("1")
println//mySuspend()
("2")
println()
mySuspend3("5")
println/*
输出结果如下:
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
.launch(CoroutineName("k2")) {
GlobalScope//获取当前Scope的Job,Job是Job类的伴随对象
(coroutineContext[Job])
println//等价于这种写法
(coroutineContext[Job.Key])
println//获取当前Scope的Name
(coroutineContext[CoroutineName])
println}
(coroutineContext[Job])
println(coroutineContext[CoroutineName])
println}
.join()
a}
}
协程上下文是一个简单的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
.launch(CoroutineName("k2")) {
GlobalScope//获取当前Scope的Job,Job是Job类的伴随对象
(coroutineContext[Job])
println//等价于这种写法
(coroutineContext[Job.Key])
println//获取当前Scope的Name
(coroutineContext[CoroutineName])
println}
(coroutineContext[Job])
println(coroutineContext[CoroutineName])
println}
.join()
a}
}
fun ScopeDispacherWithContext(){
{
runBlocking val go1 = {
("current Id ${Thread.currentThread().id}")
println}
for (i in (1..10)) {
{
launch (Dispatchers.IO) {
withContext()
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 //启动协程的时候指定调度器
.launch(myDispatcher) {
GlobalScope(1)//MyThread线程
log}.join()
(2)//主线程
log}
//用完要记得关闭调度器
.close()
myDispatcher/*
输出结构如下:
2021-12-16T08:42:19.643589Z [MyThread] 1
2021-12-16T08:42:19.655755Z [main] 2
*/
}
fun ScopeDispatcherTest2(){
{
runBlocking .newFixedThreadPool(10)
Executors.asCoroutineDispatcher().use { dispatcher ->
.launch(dispatcher) {
GlobalScope//首次进入时调度1次
(1)
log//进入async调度1次
val job = async {
(2)
log(1000)
delay//delay返回来以后调度1次
(3)
log"Hello"
}
(4)
logval result = job.await()
//await 返回以后帝都1次
("5. $result")
log}.join()
(6)
log}
}
/*
输出结果如下:共产生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 {
("1")
println{
launch ("3")
println{
launch ("4.1")
println(30)
delaythrow ArithmeticException("Hey!")
("4.2")
println}
{
launch //该协程受到影响,5.2无法输出
("5.1")
println(50)
delay("5.2")
println}
("6")
println(100)
delay//该协程受到影响,7无法输出
("7")
println}
(100)
delay//该协程受到影响,2无法输出
("2")
println}
.join()
a/*
输出如下:
1
3
4.1
6
5.1
*/
}
}
fun ExceptionLaunchJoinCatch(){
//作用域里面,使用async或者launch的join可以捕捉到异常,异常为
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
printlnvar job = launch {
throw ArithmeticException("Hey!")
}
(100)
delay//不执行
("4")
println}
(100)
delay//不执行
("2")
println}
.join()
a/*
输出如下:
1
3
*/
}
}
fun ExceptionAsyncJoinCatch(){
//作用域里面,使用async或者launch的join可以捕捉到异常,异常为
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
printlnvar job = async {
throw ArithmeticException("Hey!")
}
(100)
delay//不执行
("4")
println}
(100)
delay//不执行
("2")
println}
.join()
a/*
输出如下:
1
3
*/
}
}
fun ExceptionAwaitCatch(){
//作用域里面,使用async的await也可以捕捉到异常
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
printlnvar job = async {
throw ArithmeticException("Hey!")
"cc"
}
(100)
delay//不执行
("4")
println}
(100)
delay//不执行
("2")
println}
.join()
a/*
1
3
*/
}
}
fun ExceptionWithContextCatch(){
//作用域里面,使用withContext
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch try{
("3")
println//withContext的异常能自动传递给上级,并且不会让其他协程自动退出
var job = withContext(Dispatchers.Default) {
throw ArithmeticException("Hey!")
"cc"
}
(100)
delay//这一句不执行
("4")
println}catch(e: java.lang.Exception){
//注意,这一句能执行,捕获到原始异常
("e ${e}")
println}
}
(100)
delay//注意,这一句正常执行
("2")
println}
.join()
a/*
1
3
e java.lang.ArithmeticException: Hey!
2
*/
}
}
fun ExceptionCoroutineScopeCatch(){
//作用域里面,使用withContext
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
println{
coroutineScope (5)
println{
launch (7)
printlnthrow ArithmeticException("Hey!")
//不执行
(8)
println}
{
launch (9)
println(100)
delay//不执行
(10)
println}
(100)
delay//不执行
(6)
println}
(100)
delay//不执行
("4")
println}
(100)
delay//不执行
("2")
println}
.join()
a/*
1
3
5
7
9
Exception in thread "DefaultDispatcher-worker-2" java.lang.ArithmeticException: Hey!
*/
}
}
fun ExceptionSupervisorScopeCatch(){
//作用域里面,使用withContext
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
println{
supervisorScope (5)
println{
launch (7)
printlnthrow ArithmeticException("Hey!")
//不执行
(8)
println}
{
launch (9)
println(100)
delay//执行
(10)
println}
(100)
delay//执行
(6)
println}
(100)
delay//执行
("4")
println}
(100)
delay//执行
("2")
println}
.join()
a/*
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 ->
("Throws an exception with message: ${throwable.message}")
log}
(1)
log
//启动协程的时候,可以指定默认的异常处理方式
.launch(exceptionHandler) {
GlobalScopethrow ArithmeticException("Hey!")
}.join()
(2)
log}
/*
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 ->
("Throws an exception with message: ${throwable.message} ${coroutineContext[CoroutineName]}")
log}
(1)
log
//一般情况下,由顶级的Scope来捕捉未捕获异常
//当有supervisorScope的时候,由supervisorScope下面的首个顶级launch来捕获一会吃那个
.launch(exceptionHandler+CoroutineName("1")) {
GlobalScope(3)
log{
launch {
supervisorScope (exceptionHandler+CoroutineName("2")) {
launch( exceptionHandler+CoroutineName("3")) {
launchthrow ArithmeticException("Hey!")
}
}
}
}
(4)
log}.join()
(2)
log/*
输出结果如下:
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 ->
("Throws an exception with message: ${throwable.message} ${coroutineContext[CoroutineName]}")
log}
(1)
log
//一般情况下,由顶级的Scope来捕捉未捕获异常
//当有supervisorScope的时候,由supervisorScope下面的首个顶级launch来捕获一会吃那个
.launch(exceptionHandler+CoroutineName("1")) {
GlobalScope(3)
log{
launch {
coroutineScope (exceptionHandler+CoroutineName("2")) {
launch( exceptionHandler+CoroutineName("3")) {
launchthrow ArithmeticException("Hey!")
}
}
}
}
(4)
log}.join()
(2)
log/*
输出结果如下:
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 ->
("Throws an exception with message: ${throwable.message} ${coroutineContext[CoroutineName]}")
log}
(1)
log
//一般情况下,由顶级的Scope来捕捉未捕获异常
//当有supervisorScope的时候,由supervisorScope下面的首个顶级launch来捕获一会吃那个
.launch(exceptionHandler+CoroutineName("0")) {
GlobalScope(3)
log{
launch (Dispatchers.IO + exceptionHandler + CoroutineName("1")) {
withContext(exceptionHandler + CoroutineName("2")) {
launch(exceptionHandler + CoroutineName("3")) {
launchthrow ArithmeticException("Hey!")
}
}
}
}
(100)
delay(4)
log}.join()
(100)
delay(2)
log/*
输出结果如下:
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 {
("1")
printlnval job = launch {
("3")
println{
launch ("4.1")
println(100)
delay//该协程受到影响,4.2无法输出
("4.2")
println}
{
launch //该协程受到影响,5.2无法输出
("5.1")
println(100)
delay("5.2")
println}
("6")
println(100)
delay//该协程受到影响,7无法输出
("7")
println}
(100)
delay.cancel()
job//该协程受到影响,2无法输出
("2")
println}
(30)
delay.cancel()
a/*
输出如下:
1
3
4.1
6
5.1
*/
}
}
fun CancelWithContextCatch(){
//作用域里面,使用withContext
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
println//withContext的异常能自动传递给上级,并且不会让其他协程自动退出
var job = withContext(Dispatchers.Default) {
(5)
println(100)
delay//这一句不执行
(6)
println}
(100)
delay//这一句不执行
("4")
println}
(100)
delay//这一句不执行
("2")
println}
(30)
delay.cancel()
a/*
1
3
*/
}
}
fun CancelCoroutineScopeCatch(){
//作用域里面,使用withContext
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
println{
coroutineScope (5)
println{
launch (7)
println(100)
delay//不执行
(8)
println}
{
launch (9)
println(100)
delay//不执行
(10)
println}
(100)
delay//不执行
(6)
println}
(100)
delay//不执行
("4")
println}
(100)
delay//不执行
("2")
println}
(30)
delay.cancel()
a/*
1
3
5
7
9
*/
}
}
fun CancelSupervisorScopeCatch(){
//作用域里面,使用withContext
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
println{
supervisorScope (5)
println{
launch (7)
println(100)
delay//不执行
(8)
println}
{
launch (9)
println(100)
delay//不执行
(10)
println}
(100)
delay//不执行
(6)
println}
(100)
delay//不执行
("4")
println}
(100)
delay//不执行
("2")
println}
(30)
delay.cancel()
a/*
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{
.sleep(timeout)
Thread(100)
callback}catch(e:InterruptedException){
("catch thread interrupt")
println}
}
.start()
threadreturn thread
}
}
fun suspendCoroutine_1():Int{
suspend //注意改为了suspendCancellableCoroutine,而不是suspendCoroutine
return suspendCancellableCoroutine{ continuation->
val thread = MyTimeout().wait(100) { result ->
.resume(result)
continuation}
//关键是这一句,invokeOnCancellation的意义是,当收到协程cancel的时候,得到额外的通知
//这样可以快速停止普通作用域的操作,例如及时停止线程池中对网络请求的发送(OkHttp),释放回调避免对View的内存占用(AutoDispose),
.invokeOnCancellation {
continuation//通知线程interrupt
("thread cancel!!!")
println.interrupt()
thread}
}
}
fun CancelSuspend(){
{
runBlocking var a = GlobalScope.launch {
("1")
println{
launch ("3")
println()
suspendCoroutine_1//不执行
("4")
println}
{
launch ("5")
println(100)
delay//不执行
("6")
println}
(100)
delay//不执行
("2")
println}
(30)
delay.cancel()
a/*
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 {
flowfor(i in (1..10)){
(i)
emit}
}.collect { v->
(v)
println}
}
/*
输出如下:
1
2
3
4
5
6
7
8
9
10
*/
}
fun flowTest2(){
{
runBlocking var time = measureTimeMillis{
{
flowfor(i in (1..3)){
(100)
delay(i)
emit}
}.collect { v->
(200)
delay(v)
println}
}
//每个数据是100ms+200ms=300ms,共需要900ms的时间
("time ${time} ms")
println}
/*
输出如下:
1
2
3
time 931 ms
*/
}
fun flowTest3(){
//放在闭环输入,和被动输出的好处是
//当输入或者输出发生异常的时候,发射端和收集端都会自动退出,并清理相关资源
{
runBlocking try{
{
flowfor(i in (1..3)){
(i)
emitif( i == 2 ){
throw RuntimeException("ee")
}
}
}.collect { v->
(200)
delay(v)
println}
}catch(e:Exception){
.printStackTrace()
e}
}
/*
输出如下:
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)) {
(i)
emit(100)
delay}
}.collect { v ->
(200)
delay(v)
println}
}
(400)
delay.cancel()
job}
/*
输出如下:
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>()
{
launchfor( i in (1..5)){
.send(i)
chan}
}
{
launchfor( i in (1..5)){
.send(i+10)
chan}
}
{
launch (2){
repeat(5){
repeatval data = chan.receive()
(data)
println}
}
}
}
/*
输出如下:
1
11
2
3
12
4
5
13
14
15
*/
}
fun channelTest2(){
{
runBlocking val time = measureTimeMillis {
var chan = Channel<Int>()
{
coroutineScope {
launchfor( i in (1..3)){
(100)
delay.send(i)
chan}
}
{
launch for( i in (1..3)){
(200)
delayval data = chan.receive()
(data)
println}
}
}
}
//总共用了200ms*3 = 600ms,注意与flowTest2的不同。
//Flow,emit的返回需要等待collect的完成
//Channel,send的返回只需要对方调用了receive就可以了,不需要等待对方完成receive的整个任务
("all time ${time} ms")
println}
/*
输出如下:
1
2
3
all time 622 ms
*/
}
fun channelTest3(){
//开环输入,和主动输出的时候是
//输入端的异常,不会让channel自动关闭,也不会让接收端知道异常而退出
//Channel就像无法感知到异常发生了一样
//两者独立运行
{
runBlocking val chann = Channel<Int>()
{
supervisorScope {
launchfor(i in (1..3)){
.send(i)
channif( i == 2 ){
throw RuntimeException("ee")
}
}
}
{
launch for (i in (1..3)) {
val v = chann.receive()
(v)
println}
("finish success")
println}
(300)
delay//推送最后一个数据,通知接收端关闭
.send(3)
chann}
}
/*
输出如下:
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)){
(100)
delay.send(i)
chann}
}
{
launch for (i in (1..3)) {
val v = chann.receive()
(v)
println}
("finish success")
println}
(210)
delay.cancel()
job//推送最后一个数据,通知接收端关闭
.send(3)
chann}
/*
输出如下:
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)
{
launchfor( i in (1..5)){
.send(i)
chan}
//显式调用close,才能结束所有的collect
.close()
chan}
{
launch.collect { v->
receiveFlow("collect#1 ${v}")
println}
}
{
launch.collect { v->
receiveFlow("collect#2 ${v}")
println}
}
}
/*
输出如下:
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)
{
launchfor( i in (1..5)){
.send(i)
chan}
.close()
chan}
{
launch.collect { v->
receiveFlow("collect#1 ${v}")
println}
}
/*
//第二次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 {
launchfor( i in (1..3)){
(100)
delay.send(i)
chan}
.close()
chan}
{
launch .collect { v->
flow(200)
delay(v)
println}
}
}
}
//总共用了200ms*3 = 600ms+100ms= 700ms,注意与channelTest2的不同
//因为collect的触发,需要等待chann的第一次send的触发
("all time ${time} ms")
println}
/*
输出如下:
1
2
3
all time 736 ms
*/
}
fun channelAsFlowTest3(){
{
runBlocking val chann = Channel<Int>()
val flow = chann.receiveAsFlow()
//两者独立运行,send的崩溃,不影响collect的异常
{
supervisorScope {
launchfor(i in (1..3)){
.send(i)
channif( i == 2 ){
throw RuntimeException("ee")
}
}
}
val job = launch {
.collect { v->
flow(v)
println}
("finish success")
println}
(300)
delay.cancel()
job}
}
/*
输出如下:
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)){
(100)
delay.send(i)
chann}
}
val job2 = launch {
.collect { v->
flow(v)
println}
}
(210)
delay.cancel()
job(220)
delay.cancel()
job2}
/*
输出如下:
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)){
(100)
delay(i)
emit}
}
//Lazily的方式是,当第一个collect的时候,才去触发flow的启动
val sharedFlow = myFlow.shareIn(GlobalScope,SharingStarted.Lazily,0)
{
launch.collect { v->
sharedFlow("collect#1 ${v}")
println}
}
{
launch(200)
delay//延迟启动
.collect { v->
sharedFlow("shared#2 ${v}")
println}
}
val job = launch{
//提前结束
.collect { v->
sharedFlow("shared#3 ${v}")
println}
}
(300)
delay.cancel()
job}
(700)
delay.cancel()
globalJob}
/*
输出如下:
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)){
(100)
delay(i)
emit}
}
//stateIn与sharedIn很相似,订阅模式,多闭合输出,可以延后进入,可以中途退出
//stateIn的唯一区别是,它记录state的初始值,和最后一个值
//对于首次进入的collect,它会先推送initialValue,然后推送flow的emit值
//对于延后进入的collect,它会先推送flow的最后一个值,然后推送flow后续的emit值
val stateFlow = myFlow.stateIn(GlobalScope,SharingStarted.Lazily,100)
{
launch.collect { v->
stateFlow("collect#1 ${v}")
println}
}
{
launch(200)
delay//延迟启动
.collect { v->
stateFlow("shared#2 ${v}")
println}
}
val job = launch{
//提前结束
.collect { v->
stateFlow("shared#3 ${v}")
println}
}
(100)
delay.cancel()
job}
/*
输出如下:
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)){
(100)
delay(i)
emit}
}
val sharedFlow = myFlow.shareIn(GlobalScope, SharingStarted.Lazily,0)
{
coroutineScope val channel = Channel<Int>()
val job = launch{
var i = 0
.collect {v->
sharedFlow(200)
delay(v)
println++
iif( i == 3 ){
.send(0)
channel}
}
}
.receive()
channel.cancel()
job}
}
//由于collect不会自动退出,所以要辅助cancel掉
//总共用了200ms*3 = 600ms+100ms= 700ms,注意与channelTest2的不同
("all time ${time} ms")
println}
/*
输出如下:
1
2
3
all time 757 ms
*/
}
fun flowAsSharedTest3(){
//Flow的发射在GlobalScope作用域,两者独立运行,它的崩溃与collect作用域无关
{
runBlocking val myFlow = flow{
for( i in (1..3)){
(i)
emitif( i == 2 ){
throw RuntimeException("ee")
}
}
}
val sharedFlow = myFlow.shareIn(GlobalScope, SharingStarted.Lazily,0)
{
launchvar i = 0
.collect {v->
sharedFlow(v)
println++
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)){
(i)
emit}
}
val sharedFlow = myFlow.shareIn(myScope, SharingStarted.Lazily,0)
{
launchtry {
var i = 0
.collect { v ->
sharedFlow(v)
println++
i}
}catch(e:Exception){
.printStackTrace()
e}
}
{
launch .cancel()
job}
}
/*
输出如下
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) {
(100) // 假装我们在这里做了一些有用的事情
delay(i) // 发送下一个值
emit}
}
{
launch .collect { v->
myFlow(v)
println}
}
}
/*
输出如下:
1
2
3
*/
}
fun createFlow2(){
{
runBlocking //使用asFlow创建流
val myFlow = (1..3).asFlow()
{
launch .collect { v->
myFlow(v)
println}
}
}
}
fun myCallbackFlow() = callbackFlow<Int> {
("in thread")
printlnval thread = Thread{
for(i in (1..3)){
.sleep(100)
Thread//往外吐出数据
(i)
trySendBlocking.onFailure {
("oh fail")
println}
}
()
close}
.start()
thread//awaitClose这一句会堵塞
{
awaitClose //收到flow的cancel,或者close消息
.interrupt()
thread}
("out thread")
println}
fun createFlow3(){
{
runBlocking //使用callbackFlow创建流
val myFlow = myCallbackFlow()
{
launch .collect { v->
myFlow(v)
println}
}
}
/*
输出如下:
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()
.map{v->v+10}
myFlow.filter { v-> v%2 ==0 }
.collect { v->
(v)
println}
}
/*
输出如下:
12
14
16
18
20
*/
}
fun flowTransform(){
{
runBlocking val myFlow = (1..3).asFlow()
.transform{v->
myFlow("first ${v+10}")
emit(100)
delay("second ${v+20}")
emit}
.collect { v->
(v)
println}
}
/*
输出如下:
first 11
second 21
first 12
second 22
first 13
second 23
*/
}
fun flowFlatMerge(){
{
runBlocking val myFlow = (1..3).asFlow()
.flatMapMerge{v->
myFlow<String>{
flow("first ${v+10}")
emit(100)
delay("second ${v+20}")
emit}
}
.collect { v->
(v)
println}
}
/*
输出如下:
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 .collect { v->
myFlow(v)
println}
}
}
/*
输出如下:
1
2
3
*/
}
fun flowLaunch(){
{
runBlocking //使用launchIn收集,非阻塞,异步
val myFlow = (1..3).asFlow()
val job = myFlow
.onEach { v->
(100)
delay(v)
println}
.launchIn(this)
("start")
println.join()
job("end")
println}
/*
输出如下:
start
1
2
3
end
*/
}
fun flowToList(){
{
runBlocking //使用asFlow创建流
val myFlow = (1..3).asFlow()
{
launch val data = myFlow.toList()
(data)
println}
}
/*
输出如下
[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)){
.sleep(100)
Thread(i)
emit}
}
//flowOn之前的运行在Default的协程
.flowOn(Dispatchers.Default)
//flowOn之后的还是运行在runBlocking{}的协程里面
.collect { v->
(v)
println}
}
/*
输出如下:
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 {
{
flowfor( i in (0..3)){
(100)
delay(i)
emit}
}.collect {
(200)
delay(it)
println}
}
//每个元素需要300ms,共4个元素,需要1200ms
("all Time ${time}")
println}
/*
输出如下:
0
1
2
3
all Time 1245
*/
}
fun flowBuffer(){
//从发射方解决背压问题,将发射方的数据缓存起来,然后马上发射下一个
{
runBlocking val time = measureTimeMillis {
{
flowfor( i in (0..3)){
(100)
delay(i)
emit}
}.buffer()
.collect {
(200)
delay(it)
println}
}
//每个元素collect端需要200ms,共4个元素,需要800ms
//另外首次发射需要100ms,所以共900ms
("all Time ${time}")
println}
/*
输出如下:
0
1
2
3
all Time 967
*/
}
fun flowConflate(){
//从发射方解决背压问题,将发射方数据尝试发射,如果收集端阻塞,就用最新的数据覆盖整个缓存区的数据
//从而让收集端只处理最新的数据,中间的数据跳过去了
{
runBlocking val time = measureTimeMillis {
{
flowfor( i in (0..3)){
(100)
delay(i)
emit}
}.conflate()
.collect {
(200)
delay(it)
println}
}
//只有3个元素实际被处理了,每个处理时间为200ms,共600ms
//加入首次发射的100ms,共700ms
("all Time ${time}")
println}
/*
输出如下:
0
1
3
all Time 761
*/
}
fun flowLatest() {
//collectLatest是从收集方解决问题,如果新数据到来了,但是收集方还没处理完上次的数据
// 就直接cancel收集方,让收集方马上立即处理最新数据
//注意collectLatest与conflate的不同
{
runBlocking val time = measureTimeMillis {
{
flowfor( i in (0..3)){
(100)
delay(i)
emit}
}
.collectLatest {
(200)
delay(it)
println}
}
//发射元素为4个,每个100ms,共400ms
//只处理了最后一个元素,处理时间为200ms,共600ms
("all Time ${time}")
println}
/*
输出如下:
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)){
(i)
emit}
}catch(e:Exception){
("all catch")
println.printStackTrace()
e}
}.collect {
(it)
printlnif( 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{
{
flowfor( i in (1..3)){
(i)
emit}
//catch操作符只会收集在操作符之前的异常的操作
//因此不会收集collect里面的异常
}.catch {e->
("emit catch")
println.printStackTrace()
e}.collect {
(it)
printlnif( it == 2 ){
throw RuntimeException("ee")
}
}
//collect的异常需要用外部的try-catch来捕捉
}catch( e:Exception){
("collect catch")
println.printStackTrace()
e}
}
/*
输出如下:
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)){
.send(i)
channel}
.close()
channel}
{
launch while( true){
val receiveResult = channel.receiveCatching()
if( receiveResult.isClosed ){
break
}
(receiveResult.getOrThrow())
println}
}
}
/*
输出如下:
1
2
3
*/
}
fun channelForSugar(){
{
runBlocking val channel = Channel<Int>()
{
launch for( i in (1..3)){
.send(i+1)
channel}
.close()
channel}
{
launch for( data in channel ){
(data)
println}
}
}
/*
输出如下:
2
3
4
*/
}
fun channelProduceProduce(){
{
runBlocking val channel = produce {
for( i in (1..3)){
(i+2)
send}
}
{
launch for( data in channel ){
(data)
println}
}
}
/*
输出如下:
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设计异常复杂,也提出了额外的结构化并发来解决它。隐约觉得,这个设计过于臃肿,有被推倒重来的可能。
参考资料:
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!