Kotlin

目录

文章主要参考链接

Kotlin系列教程——史上最全面、最详细的学习教程,持续更新中…

Android 进阶:基于 Kotlin 的 Android App 开发实践

Kotlin协程它不香吗?

Kotlin 的协程用力瞥一眼

var 与 val

// 定义一个不可为空的变量,用var修饰的变量可以被重新赋值,用val修饰的变量则不能,但是不能赋值为null
var a : Int = 12

/*
    定义可空类型的变量,即变量可以被赋值为null
    定义格式为: 修饰符 变量名 : 类型? = 值
*/
var nullA : Int? = 12
nullA = null

lateinit和lazy

lateinit 的使用

lateinitkotlin中的一个关键字,使用方法如下

    // 声明一个string变量
    lateinit var a1: String
    
    private fun test() {
        // 初始化
        a1 = "test1"
    }

var之前添加lateinit,然后选择你想要的时候,初始化,但是有以下需要注意的地方:

  • lateinit只能修饰变量var,不能修饰常量val
  • lateinit不能对可空类型使用
  • lateinit不能对java基本类型使用,例如:Double、Int、Long等
    在调用lateinit修饰的变量时,如果变量还没有初始化,则会抛出未初始化异常,报错

lazy 的使用

lazy也是kotlin中常用的一种延迟加载方式,使用方法如下:

val a2:String by lazy{
    println("开始初始化")
    // 初始化的值
    "sss"
}

使用时,在类型后面加by lazy{}即可,{}中的最后一行代码,需要返回初始化的结果,上述代码中,"sss"即为最后初始化的值。下面是lazy的一些注意点:

  • lazy只能对常量val使用,不能修饰变量var
  • lazy的加载时机为第一次调用常量的时候,且只会加载一次(毕竟是个常量,只能赋值一次)

复合符号( ‘?.’ ‘?:’ ‘!!’ ‘as?’ ‘?’ )

复合符号( ‘?.’ ‘?:’ ‘!!’ ‘as?’ ‘?’ )

Elvis操作符

  • ?.该符号的用法为:可空类型变量?.属性/方法。如果可空类型变量为null时,返回null
var str : String? = "123456"
str = null

println(str?.length)   // 当变量str为null时,会返回空(null)

  • ?: 这个操作符表示在判断一个可空类型时,会返回一个我们自己设定好的默认值.
val testStr : String? = null

var length = 0

// 例: 当testStr不为空时,输出其长度,反之输出-1

// ?: 写法
length = testStr?.length ?: -1

println(length)// -1

函数

示例

定义一个返回类型为Int的函数

fun returnFun() : Int{
    return 2
}

单表达式函数

当函数返回单个表达式时,可以省略函数体的花括号

fun sum(x: Int=0, y: Int): Int {
    return x + y
}

等价于 -->

fun sum(x: Int=0, y: Int): Int =  x + y

它还等价于 -->

fun sum(x: Int=0, y: Int) =  x + y //可以通过编译器来推断该函数的返回类型。

扩展函数

Kotlin系列之扩展函数

class Extension2 {
    fun test() = println("this is from test()")
}

fun Extension2.test() = println("this is from extension function") // 扩展函数 test

fun main(args: Array<String>) {
    var extension2 = Extension2()
    extension2.test()
}

输出

this is from test()

当扩展函数跟原先的函数重名,并且参数都相同时,扩展函数就会失效,调用的是原先类的函数

嵌套类与内部类

嵌套类(Nested Class)

Kotlin 的嵌套类是指定义在某一个类内部的类,嵌套类不能够访问外部类的成员。除非嵌套类变成内部类。

class Outter1 {

    val str:String = "this property is from outter1 class"

    class Nested {

        fun foo() = println("")
    }
}

fun main(args: Array<String>) {
    Outter1.Nested().foo()
}

内部类(Inner Class)

Kotlin 的内部类使用inner关键字标识,内部类能够访问外部类的成员。

class Outter2 {

    val str:String = "this property is from outter2 class"

    inner class Inner {

        fun foo() = println("$str")
    }
}

fun main(args: Array<String>) {
    Outter2().Inner().foo()
}

小结一下嵌套类和内部类

  • 默认的是嵌套类
  • 嵌套类不持有外部类的引用,内部类持有外部类的引用
  • 嵌套类的创建方式:外部类.嵌套类()
  • 内部类的创建方式:外部类().内部类()

类的构造函数

  • 在Kotlin中,允许有一个主构造函数和多个二级构造函数(辅助构造函数、次构造函数)。其中主构造函数是类头的一部分。
  • 关键字或者构造函数名:constructor(参数)

主构造函数

主构造函数是类头的一部分,类名的后面跟上构造函数的关键字以及类型参数。

class Test constructor(num : Int){
     ...
}
或者
/*
     因为是默认的可见性修饰符且不存在任何的注释符
     故而主构造函数constructor关键字可以省略
*/
class Test(num: Int){
      ...
}

构造函数中的初始化代码块init
init{…}中能使用构造函数中的参数

fun main(args: Array<String>) {
    // 类的实例化,会在下面讲解到,这里只是作为例子讲解打印结果
    var test = Test(1)
}

class Test constructor(var num : Int){
    init {
        num = 5
        println("num = $num")// 5
    }
}

二级构造函数

Kotlin中支持二级构造函数。它们以constructor关键字作为前缀。

class Test{
    constructor(参数列表){

    }
}

同时存在主构造函数和二级构造函数时的情况

如果类具有主构造函数,则每个辅助构造函数需要通过另一个辅助构造函数直接或间接地委派给主构造函数。 使用this关键字对同一类的另一个构造函数进行委派:

fun main(args: Array<String>) {
    var test1 = Test(1)
    var test2 = Test(1,2)
}

// 这里是为了代码清晰,故而没有隐藏constructor关键字
class Test constructor(num: Int){

    init {
        println("num = $num")
    }

    constructor(num : Int, num2: Int) : this(num) {
        println(num + num2)
    }
}

说明:二级构造函数中的参数1(num),是委托了主构造函数的参数num。

可以看出,当实例化类的时候只传1个参数的时候,只会执行init代码块中的代码。当传2个参数的时候,除了执行了init代码块中代码外,还执行了二级构造函数中的代码。

输出结果为:

num = 1
num = 1
3

继承类的基础使用

定义继承类的关键字为:open。不管是类、还是成员都需要使用open关键字。


open class 类名{
     ...
     open var/val 属性名 = 属性值
     ...
     open fun 函数名()
     ...
 }

这里定义一个继承类Demo,并实现两个属性与方法,并且定义一个DemoTest去继承自Demo

open class Demo{

    open var num = 3

    open fun foo() = "foo"

    open fun bar() = "bar"

}

class DemoTest : Demo(){
    // 这里值得注意的是:Kotlin使用继承是使用`:`符号,而Java是使用extends关键字
}

fun main(args: Array<String>) {

    println(DemoTest().num)
    DemoTest().foo()
    DemoTest().bar()

}

输出

3
foo
bar

继承类的构造函数

无主构造函数

当实现类无主构造函数时,则每个二级构造函数必须使用super关键字初始化基类型,或者委托给另一个构造函数。 请注意,在这种情况下,不同的辅助构造函数可以调用基类型的不同构造函数

例:这里举例在Android中常见的自定义View实现,我们熟知,当我们指定一个
组件是,一般实现继承类(基类型)的三个构造函数。

class MyView : View(){

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
        : super(context, attrs, defStyleAttr)
}
存在主构造函数

当存在主构造函数时,主构造函数一般实现基类型中参数最多的构造函数,参数少的构造函数则用this关键字引用即可了

// 同样以自定义组件为例子

class MyView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
    : View(context, attrs, defStyleAttr) {

    constructor(context: Context?) : this(context,null,0)
    
    constructor(context: Context?,attrs: AttributeSet?) : this(context,attrs,0)
}
重写

当基类中的函数,没有用open修饰符修饰的时候,实现类中出现的函数的函数名不能与基类中没有用open修饰符修饰的函数的函数名相同,不管实现类中的该函数有无override修饰符修饰。

读着有点绕,直接看例子你就明白了。

open class Demo{
    fun test(){}   // 注意,这个函数没有用open修饰符修饰
}

class DemoTest : Demo(){
    
    // 这里声明一个和基类型无open修饰符修饰的函数,且函数名一致的函数
    // fun test(){}   编辑器直接报红,根本无法运行程序
    // override fun test(){}   同样报红
}

方法重载

在文章的开头提到了多态这个特性,方法的重载其实主要体现在这个地方。即函数
名相同,函数的参数不同的情况。这一点和Java是相同的

open class Demo{
    open fun foo() = "foo"
}

class DemoTest : Demo(){

    fun foo(str: String) : String{
        return str
    }

    override fun foo(): String {
        return super.foo()
    }
}    

fun main(args: Array<String>) {
    println(DemoTest().foo())
    DemoTest().foo("foo的重载函数")
}

输出:

foo
foo的重载函数

覆盖规则

这里的覆盖规则,是指实现类继承了一个基类,并且实现了一个接口类,当我的基类中的方法、属性和接口类中的函数重名的情况下,怎样去区分实现类到底实现哪一个中的属性或属性。 这一点和一个类同时实现两个接口类,而两个接口都用同样的属性或者函数的时候是一样的。

open class A{
    open fun test1(){ println("基类A中的函数test1()") }

    open fun test2(){println("基类A中的函数test2()")}
}

interface B{
    fun test1(){ println("接口类B中的函数test1()") }

    fun test2(){println("接口类B中的函数test2()")}
}

class C : A(),B{
    override fun test1() {
        super<A>.test1()
        super<B>.test1()
    }

    override fun test2() {
        super<A>.test2()
        super<B>.test2()
    }
}

接口

fun main(args: Array<String>) {

   // 类的初始化
   var demo = Demo1()

   demo.fun1()
}

/**
 * 我定义的接口
 */
interface Demo1Interface{

    // 定义的方法
    fun fun1()
}

/**
 * 接口的实现类
 */
class Demo1 : Demo1Interface{
    override fun fun1() {
        println("我是接口中的fun1方法")
    }
}

数据类

在Java中,或者在我们平时的Android开发中,为了解析后台人员给我们提供的接口返回的Json字符串,我们会根据这个字符串去创建一个类或者实例对象,在这个类中,只包含了一些我们需要的数据,以及为了处理这些数据而所编写的方法。这样的类,在Kotlin中就被称为数据类。

  • data为声明数据类的关键字,必须书写在class关键字之前。
  • 在没有结构体的时候,大括号{}可省略。
  • 构造函数中必须存在至少一个参数,并且必须使用val或var修饰。这一点在下面数据类特性中会详细讲解。
  • 参数的默认值可有可无。(若要实例一个无参数的数据类,则就要用到默认值)

使用数据类

声明数据类的关键字为:data

data class 类名(var param1 :数据类型,...){}
或者
data class 类名 可见性修饰符 constructor(var param1 : 数据类型 = 默认值,...)

// 定义一个名为User的数据类
data class User(val name : String, val pwd : String)

修改数据类属性

Koltin要修改数据类的属性,则使用其独有的copy()函数。其作用就是:修改部分属性,但是保持其他不变

val mUser = User("kotlin","123456")
println(mUser)
val mNewUser = mUser.copy(name = "new Kotlin")
println(mNewUser)

输出结果

User(name=kotlin, pwd=123456)
User(name=new Kotlin, pwd=123456)

解构声明

val mUser = User("kotlin","123456")
val (name,pwd) = mUser
println("name = $name\tpwd = $pwd")

输出结果

name = kotlin	pwd = 123456

抽象类

使用抽象类

关键字:abstract

abstract class Lanauage{
    val TAG = this.javaClass.simpleName  // 自身的属性
    
    // 自身的函数
    fun test() : Unit{
        // exp
    }
    abstract var name : String           // 抽象属性
    abstract fun init()                  // 抽象方法
}

/**
 * 抽象类Lanauage的实现类TestAbstarctA
 */
class TestAbstarctA : Lanauage(){

    override var name: String
        get() = "Kotlin"
        set(value) {}

    override fun init() {
        println("我是$name")
    }
}

/**
 * 抽象类Lanauage的实现类TestAbstarctB
 */
class TestAbstarctB : Lanauage(){
    override var name: String
        get() = "Java"
        set(value) {}

    override fun init() {
        println("我是$name")
    }
}

fun main(args: Array<String>) {
    
    // val lanauage = Lanauage() 是错误的,因为抽象类不能直接被实例化
    
    val mTestAbstarctA = TestAbstarctA()
    val mTestAbstarctB = TestAbstarctB()

    println(mTestAbstarctA.name)
    mTestAbstarctA.init()

    println(mTestAbstarctB.name)
    mTestAbstarctB.init()
}

Lambda

Lambda介绍

无参数的情况

val/var 变量名 = { 操作的代码 }

// 源代码
fun test(){ println("无参数") }
  
// lambda代码
val test = { println("无参数") }

// 调用
test() 
结果为:无参数

有参数的情况

val/var 变量名 : (参数的类型,参数类型,…) -> 返回值类型 = {参数1,参数2,… -> 操作参数的代码 }
等价于👇
// 此种写法:即表达式的返回值类型会根据操作的代码自推导出来。
val/var 变量名 = { 参数1 : 类型,参数2 : 类型, … -> 操作参数的代码 }

// 源代码
fun test(a : Int , b : Int) : Int{
    return a + b
}

// lambda
val test : (Int , Int) -> Int = {a , b -> a + b}
// 或者
val test = {a : Int , b : Int -> a + b}

// 调用
test(3,5) => 结果为:8

lambda表达式作为函数中的参数的时候

//定义一个所谓的高级函数 函数参数block是两个参数为Int类型的函数变量,返回值是Int
fun exampleFun(a: Int, b: Int, block: (Int, Int) -> Int): Int {
    return block(a, b)
}

//定义两个返回类型一样 但执行逻辑不一样的函数
fun aFun(n:Int,m:Int):Int{
    return m*n
}

fun bFun(n:Int,m:Int):Int{
    return m+n
}

//调用
fun main() {
    val aValue = exampleFun(2,3,::aFun)
    println("aFun作为参数的返回值:$aValue")
    val bValue = exampleFun(2,3,::bFun)
    println("bFun作为参数的返回值:$bValue")
}

//输出结果
aFun作为参数的返回:6
bFun作为参数的返回:5
  • 这里的调用使用两个冒号调用,具体的调用既可以调用本类中的方法,也可以调用某个成员变量类的方法
  • 调用本类方法直接两个冒号加函数名或者this::函数名 即可
  • 调用成员实例类的方法就是成员实例名::类函数名 即可

如果 Lambda 是函数的最后一个参数,你可以把 Lambda 写在括号的外面

view.setOnClickListener({ v: View ->
  switchToNextPage()
})

// 等价于
view.setOnClickListener() { v: View ->
  switchToNextPage()
}
// 而如果 Lambda 是函数唯一的参数,你还可以直接把括号去了:
view.setOnClickListener { v: View ->
  switchToNextPage()
}
// 另外,如果这个 Lambda 是单参数的,它的这个参数也省略掉不写:
view.setOnClickListener {
  switchToNextPage()
}
/*哎,不错,单参数的时候只要不用这个参数就可以直接不写了。
其实就算用,也可以不写,
因为 Kotlin 的 Lambda 对于省略的唯一参数有默认的名字:it
*/


view.setOnClickListener {
  switchToNextPage()
  it.setVisibility(GONE)
}

it

  • it并不是Kotlin中的一个关键字。
  • it是在当一个高阶函数中Lambda表达式的参数只有一个的时候可以使用it来使用此参数。it可表示为单个参数的隐式名称,是Kotlin语言约定的。
// 这里举例一个语言自带的一个高阶函数filter,此函数的作用是过滤掉不满足条件的值。
val arr = arrayOf(1,3,5,7,9)
// 过滤掉数组中元素小于2的元素,取其第一个打印。这里的it就表示每一个元素。
println(arr.filter { it < 5 }.component1())   

Kotlin的inline内联函数

当一个函数被内联 inline 标注后,在调用它的地方,会把这个函数方法体中的所以代码移动到调用的地方,而不是通过方法间压栈进栈的方式。
Kotlin的inline内联函数

Kotlin系列之let、with、run、apply、also函数的使用

Kotlin系列之let、with、run、apply、also函数的使用

函数名定义inline的结构函数体内使用的对象返回值是否是扩展函数适用的场景
letfun <T, R> T.let(block: (T) -> R): R = block(this)it指代当前对象闭包形式返回适用于处理不为null的操作场景
withfun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()this指代当前对象或者省略闭包形式返回适用于调用同一个类的多个方法时,可以省去类名重复,直接调用类的方法即可,经常用于Android中RecyclerView中onBinderViewHolder中,数据model的属性映射到UI上
runfun <T, R> T.run(block: T.() -> R): R = block()this指代当前对象或者省略闭包形式返回适用于let,with函数任何场景。
applyfun T.apply(block: T.() -> Unit): T { block(); return this }this指代当前对象或者省略返回this1、适用于run函数的任何场景,一般用于初始化一个对象实例的时候,操作对象属性,并最终返回这个对象。
2、动态inflate出一个XML的View的时候需要给View绑定数据也会用到.
3、一般可用于多个扩展函数链式调用4、数据model多层级包裹判空处理的问题
alsofun T.also(block: (T) -> Unit): T { block(this); return this }it指代当前对象返回this适用于let函数的任何场景,一般可用于多个扩展函数链式调用

集合

List

  • 声明并初始化List的集合:使用listOf(…)函数
  • 声明并初始化MutableList的集合:使用mutableListOf(…)函数

Set

Set类型集合会把重复的元素去除掉。

  • 声明并初始化Set的集合:使用setOf(…)函数
  • 声明并初始化MutableSet的集合:使用mutableSetOf(…)函数

Map

当我们的键存在重复时,集合会过滤掉之前重复的元素。

  • 不可变的Map类型集合的初始化使用:mapOf()函数
  • 可变的Map类型集合的初始化使用:mutableMapOf()函数

对象表达式与对象声明

创建一个对当前类有一点小修改的对象,但不想重新声明一个子类。Java匿名内部类实现,kotlin使用对象表达式和对象声明实现

对象表达式

要创建一个对象表达式,一般是创建一个继承自某一类的匿名类的对象,使用关键字object,如果超类有构造函数,则必须传递适当的构造参数,多个超类用逗号隔开。

open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*……*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的 返回类型或者用作公有属性的类型,那么该函数或属性的实际类型 会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象 中添加的成员将无法访问

class C {
    // 私有函数,所以其返回类型是匿名对象类型
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函数,所以其返回类型是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // 没问题
        val x2 = publicFoo().x  // 错误:未能解析的引用“x”
    }
}

对象声明

所谓的对象声明,我们可以理解为 java 中的单例模式。
单例与伴随对象

日常开发中写一个单例类是很常见的行为,Kotlin中直接将这种设计模式提升到语言级别,使用关键词object定义单例类。这里需要注意,是全小写。Kotlin中区分大小写,Java中原本指所有类的父类Object已弃用。单例类访问直接使用类名,无构造函数。

object Shop(name: String) {
    fun buySomething() {
        println("Bought it")
    }
}
Shop.buysomething()

伴生对象

Java中使用static标识一个类里的静态属性或方法,可以被这个类的所以实现使用。Kotlin改为使用伴随对象,用companion修饰单例类object,来实现静态属性或方法功能。

class Mall(name: String) {
    companion object Shop {
        val SHOP_NAME: String = "McDonald" // 等同于Java中写public static String
        fun buySomething() { // 等同于Java中写public static void
            println("Bought it")
        }
    }
}
Mall.buySomething()

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

This 表达式 (Expression)

inner 关键字定义内部类
在内部类当中访问外部类,需要显示使用 this@OutterClass.fun() 的语法

class A {

    fun testA(){	}

    inner class B { // 在 class A 定义内部类 B

        fun testB(){	}

        fun foo() {
            this.testB() // ok
            this.testA() // 编译错误
            this@A.testA() // ok
            this@B.testB() // ok
        }
    }
}

Kotlin 委托

类的委托

// 创建接口
interface Base {
    fun print()
}

// 实现此接口的被委托的类
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通过关键字 by 完成委托,Derived 相当于代理类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val base = BaseImpl(10)
    Derived(base).print() // 输出 10
}

Derived 类的构造函数的参数 b 是一个 Base 对象,通过关键字by完成委托。Derived 类无需再实现 print() 方法。

由此可见,委托模式是替代继承很好的一个选择。

再举一个例子,定义两个接口 Marks 和 Totals,再分别定义它们的实现类分别是 StdMarks、ExcelMarks 和 StdTotals、ExcelTotals。

最后定义一个 Student 类,它和 Marks 、 Totals 建立委托的关系,Student 没有使用任何继承。

interface Marks {

    fun printMarks()
}

class StdMarks : Marks {

    override fun printMarks() = println("printed marks")
}

class ExcelMarks : Marks {

    override fun printMarks() = println("printed marks and export to excel")
}

interface Totals {

    fun printTotals()
}

class StdTotals : Totals {

    override fun printTotals() = println("calculated and printed totals")
}

class ExcelTotals : Totals {

    override fun printTotals() = println("calculated and printed totals and export to excel")
}

class Student(studentId: Int, marks: Marks, totals: Totals)
    : Marks by marks, Totals by totals

fun main(args: Array<String>) {

    val student1 = Student(1, StdMarks(), StdTotals()) // StdMarks、StdTotals 为 Student 被委托的类
    student1.printMarks()
    student1.printTotals()

    println("---------------------------")

    val student2 = Student(2, ExcelMarks(), ExcelTotals()) // ExcelMarks、ExcelTotals 为 Student 被委托的类
    student2.printMarks()
    student2.printTotals()
}

输出

printed marks
calculated and printed totals
---------------------------
printed marks and export to excel
calculated and printed totals and export to excel

属性委托

Kotlin 委托

kotlin 协程

参考链接

【码上开学】Kotlin 的协程用力瞥一眼

【码上开学】Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了

【码上开学】到底什么是「非阻塞式」挂起?协程真的更轻量级吗?

Kotlin协程它不香吗?

Kotlin之witchContext与launch以及async的区别
Kotlin 协程源码解析

Kotlin协程是什么:

Kotlin协程就是 Kotlin 提供的一套线程封装的 API,它可以用看起来同步的方式写出异步的代码

基本使用

launch 函数不是顶层函数,是不能直接用的,可以使用下面三种方法来创建协程:

🏝️
// 方法一,使用 runBlocking 顶层函数
runBlocking {
    getImage(imageId)
}

// 方法二,使用 GlobalScope 单例对象
//            👇 可以直接调用 launch 开启协程
GlobalScope.launch {
    getImage(imageId)
}

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//                                    👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}

  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

  • 方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。

  • 方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

使用协程

如果只是使用 launch 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。

🏝️
launch(Dispachers.Main) {              // 👈 在 UI 线程开始
    val image = getImage(imageId)
    avatarIv.setImageBitmap(image)     // 👈 执行结束后,自动切换回 UI 线程
}

// suspend:👈 挂起      
// withContext(Dispatchers.IO): 👈 切换到 IO 线程,并在执行完成后切回 UI 线程                    
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {// 👈 getImage代码将会运行在 IO 线程
    ...
}

注意点:
协程的挂起主要是切线程,只是它能在挂起之后自动的切回来。

suspend 的意义

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?

它其实是一个提醒
函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。
为什么 suspend 关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来?
因为它本来就不是用来操作挂起的。
挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字。
supend它并没有做挂起操作的功能,真正做挂起的是这个函数里面挂起函数,比如我们这里用的withContext这个自带的挂起函数

所以supend这个关键字,只是一个提醒

🏝️
// 👇 redundant suspend modifier
suspend fun suspendingPrint() {
  println("Thread: ${Thread.currentThread().name}")
}

如果你创建一个 suspend 函数但它内部不包含真正的挂起逻辑,编译器会给你一个提醒:redundant suspend modifier,告诉你这个 suspend 是多余的。
因为你这个函数实质上并没有发生挂起,那你这个 suspend 关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。

  • 自定义 suspend 函数

给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。

suspend fun getImage(imageId: Int) = 
       withContext(Dispatchers.IO) {
  }
常用的 Dispatchers
  • Dispatchers.Main:Android 中的主线程
  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算

如果使用协程,可以直接把两个并行请求写成上下两行,最后再把结果进行合并即可:

🏝️
coroutineScope.launch(Dispatchers.Main) {
    //            👇  async 函数之后再讲
    val avatar = async { api.getAvatar(user) }    // 获取用户头像
    val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
    val merged = suspendingMerge(avatar, logo)    // 合并结果
    //                  👆
    show(merged) // 更新 UI
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码。需要注意的是 suspendingMerge 并不是协程 API 中提供的方法,而是我们自定义的一个可「挂起」的结果合并方法.

协程作用域CoroutineScope

协程作用域、上下文与调度
Kotlin Coroutines(协程) 完全解析(一)

在 Android 环境中,通常每个界面(Activity、Fragment 等)启动的 Coroutine 只在该界面有意义,如果用户在等待 Coroutine 执行的时候退出了这个界面,则再继续执行这个 Coroutine 可能是没必要的。另外 Coroutine 也需要在适当的 context 中执行,否则会出现错误,比如在非 UI 线程去访问 View。 所以 Coroutine 在设计的时候,要求在一个范围(Scope)内执行,这样当这个 Scope 取消的时候,里面所有的子 Coroutine 也自动取消。所以要使用 Coroutine 必须要先创建一个对应的 CoroutineScope。

CoroutineScope 接口

CoroutineScope 是一个接口,要是查看这个接口的源代码的话就发现这个接口里面只定义了一个属性 CoroutineContext:

public interface CoroutineScope {
    // Scope 的 Context
    public val coroutineContext: CoroutineContext
}

所以 CoroutineScope 只是定义了一个新 Coroutine 的执行 Scope,每个协程coroutine builder(launchasync等) 都是 CoroutineScope 的扩展方法,并且自动的继承了当前 Scope 的 coroutineContext 和取消操作。

launch函数

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
   ......
}
CoroutineScope 和 CoroutineContext
  • CoroutineScope: 可以理解为协程本身,包含了CoroutineContext。
  • CoroutineContext: 协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。
  • EmptyCoroutineContext: 表示一个空的协程上下文。
CoroutineDispatcher
  • CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有三种标准实现Dispatchers.Default、Dispatchers.IO,Dispatchers.Main和Dispatchers.Unconfined,Unconfined 就是不指定线程。
Job & Deferred
  • Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期。

Coroutine builders

  • GlobalScope.launch函数属于协程构建器 Coroutine builders,Kotlin 中还有其他几种 Builders,负责创建协程。
runBlocking {}
  • runBlocking并非挂起函数;runBlocking {}是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的。
withContext {}
  • withContext {}不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。
async {}
  • CoroutineScope.async {}可以实现与 launch builder 一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为CoroutineScope.async {}返回的是 Deferred 类型。
  • 获取CoroutineScope.async {}的返回值需要通过await()函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。
fun main(args: Array<String>) = runBlocking { // start main coroutine
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }  // start async one coroutine without suspend main coroutine
        val two = async { doSomethingUsefulTwo() }  // start async two coroutine without suspend main coroutine
        println("The answer is ${one.await() + two.await()}") // suspend main coroutine for waiting two async coroutines to finish
    }
    println("Completed in $time ms")
}

与生命周期绑定的作用域

一般而言,在应用中具有生命周期的组件应该实现 CoroutineScope 接口,并负责该组件内 Coroutine 的创建和管理。例如对于 Android 应用来说,可以在 Activity 中实现 CoroutineScope 接口, 例如:

class ScopedActivity : Activity(), CoroutineScope {
    lateinit var job: Job
    // CoroutineScope 的实现
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()

    /*
     * 注意 coroutine builder 的 scope, 如果 activity 被销毁了或者该函数内创建的 Coroutine
     * 抛出异常了,则所有子 Coroutines 都会被自动取消。不需要手工去取消。
     */
      launch { // <- 自动继承当前 activity 的 scope context,所以在 UI 线程执行
          val ioData = async(Dispatchers.IO) { // <- launch scope 的扩展函数,指定了 IO dispatcher,所以在 IO 线程运行
            // 在这里执行阻塞的 I/O 耗时操作
          }
        // 和上面的并非 I/O 同时执行的其他操作
          val data = ioData.await() // 等待阻塞 I/O 操作的返回结果
          draw(data) // 在 UI 线程显示执行的结果
      }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 当 Activity 销毁的时候取消该 Scope 管理的 job。
        // 这样在该 Scope 内创建的子 Coroutine 都会被自动的取消。
        job.cancel()
    }


}

在mvvm模式使用作用域

class ViewModelOne : ViewModel() {

    private val viewModelJob = SupervisorJob()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

    val mMessage: MutableLiveData<String> = MutableLiveData()

    fun getMessage(message: String) {
        uiScope.launch {
            val deferred = async(Dispatchers.IO) {
                delay(2000)
                "post $message"
            }
            mMessage.value = deferred.await()
        }
    }

    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
}
ViewModelScope 方式

AndroidX Lifecycle v2.1.0 在 ViewModel 中引入 viewModelScope,当 ViewModel 被销毁时它会自动取消协程任务,这个特性真的好用。viewModelScope 管理协程的方式与我们在 ViewModel 引入协程的方式一样,代码实现如下:

class MyViewModel : ViewModel() {
  
    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }
  
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

Kotlin之Koin

Kotlin之Koin

协程中的取消和异常 | 异常处理详解

官方讲解

在 Android 开发中使用协程 | 背景介绍

在 Android 开发中使用协程 | 上手指南

在 Android 开发中使用协程 | 代码实战

协程中的取消和异常 | 核心概念介绍

协程中的取消和异常 | 取消操作详解

协程中的取消和异常 | 异常处理详解

对标 RxJava,你好 Flow,协程实战篇

supervisorScope 和 coroutineScope,它们的主要区别是当出现任何一个子 scope 失败的情况,coroutineScope 将会被取消。如果一个网络请求失败了,所有其他的请求都将被立即取消,这种需求选择 coroutineScope。相反,如果您希望即使一个请求失败了其他的请求也要继续,则可以使用 supervisorScope,当一个协程失败了,supervisorScope 是不会取消剩余子协程的。

作用域取消时,它内部所有的协程也会被取消;
suspend 函数返回时,意味着它的所有任务都已完成;

第三方博客

coroutineScope , supervisorScope 和 launch ,async 的混合异常处理

如何优雅的处理协程的异常?

DSL

如何在 kotlin 优雅的封装匿名内部类(DSL、高阶函数)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值