Kotlin高级部分

27 阅读11分钟

一.Lambda知识点

1.函数声明、实现与调用

2.函数推断

3.函数参数

fun test1(){
    //1.函数声明、实现与调用
    var method:()->String /*函数声明*/
    method={
        println("this is method impl")
        "1" //最后一行为返回值
    }//函数实现
    method()//函数调用 运算符 重载 和下面invoke调用一致
    println(method.invoke())

    //2.函数推断,省去声明过程
    var method2={
        println("this is method impl")
        "1" //lambda最后一行为返回值 普通函数不是
    }//函数实现
    method()//函数调用 运算符 重载 和下面invoke调用一致
    println(method.invoke())

    //3.参数的设置
    var method3:(Int)->String={a:Int->a.toString()}
    //如果是单个参数可以在实现中不声明类型 但是要声明
    var method4:(Int)->String={a->a.toString()}
    //如果是单个参数可以在实现中不定义形参,默认是it
    var method5:(Int)->String={it.toString()}
    //如果参数实现中未使用到可以使用_来替代,它就不会定义一个临时变量
    var method6:(Int,String)->String={_,s->s}

}

    //注意下面lambda和普通的函数的区别
    fun method3(): () -> Boolean /*可省略*/= {
        true
    }
    fun method4():  Boolean {
      return  true
    }

    
     //函数中套函数 函数的返回的是一个函数
    fun k():(Int)->Unit={p:Int-> println(p)}

    @Test
    fun test3(){
        k()(1)
    }
  //返回的是Unit
    fun method() :Unit= run { println() }

    //返回的是lambda函数
    fun method2():()->Unit={
        
    }
     var method=::method4//将一个函数的引用赋值给变量
   //使用函数体返回
    var methodx:()->Boolean = fun ():Boolean{
        return true
    }

    private fun test(){
        methodx();
    }

4.搭配扩展函数

    @Test
    fun test2(){
        //为String添加一个扩展函数匿名函数
        var method:String.(p:Int)->Unit={

            //lambda内部持有的this指向当前string
            //it是传过来的参数
            println("$this,$it")
        }
        var s="1"
        s.method(1);
    }

    

二.高阶函数

高阶函数就是函数中的参数可以是一个函数。

  @Test
    fun test() {

        //如果最后一个参数是lambda可以后面根大括号
        val str=show(2) {
            println("par:$it")
            "1"
        }
        println("str:$str")


        //
        math(1,2,3){
            a, b, c ->
            a+b+c
        }
    }

    //函数中参数是函数
    private fun show(int: Int, block: (Int) -> String) :String{
        //调用函数 有点回调的意思
       return block(int)
    }

    //根据传入过来的函数来定义计算
    private fun math(a: Int,b: Int,c: Int,cal:(a: Int,b: Int,c: Int)->Int):Int{
        val result = cal(a, b, c)
        println("result:$result")
        return result
    }

高阶函数搭配扩展函数使用,注意 this和it,扩展函数持有的是this

    @Test
    fun test2() {

        var str = "false"
        println(str.run2 {
            //这里是this 获取的是当前对象
            str == "true"
        })
        var str2 = "false"

        str2.run3 {
            //可以获得的是it 获取的是参数
            true

        }
    }

    //高阶函数和扩展函数的配合
    //参数是一个泛型的匿名的扩展的lambda函数
    private fun <T> T.run2(pMethod: T.() -> Boolean) =
        pMethod()

    //高阶函数和扩展函数的配合
    //参数是一个匿名的lambda函数
    private fun <T> T.run3(pMethod: (Double) -> Boolean) =
        pMethod(1.0)

函数引用参数传递写法

    //传入函数引用
    @Test
    fun test3(){

        val a:Function1<Int,String> = ::blockImpl
        fun b():Function1<Int,String> = ::blockImpl
        run4(a)
        run4(b())
        run4 (::blockImpl)
        var c:(Int)->String = ::blockImpl
        var d:Int.()->String = ::blockImpl //Int.()和(Int)可以替换使用
    }
    fun blockImpl(value:Int):String{
        println("$value")
        return "$value"
    }
    private fun run4(block: (Int) -> String)=block(1)

函数声明返回匿名函数中包含匿名函数

 /*函数的返回是一个高阶函数 包含了三个参数,最后一个是匿名lambda函数*/
fun higherTest(): (Int, String, (Int, String) -> Unit) -> Unit   =
        { num: Int, str: String, lambda: (Int, String) -> Unit ->
            lambda(num, str)

        }


    @Test
    fun test4() {
        higherTest()/*返回函数*/(1,"2"){
            i,str->
            println("$i,$str")
        }
    }

搭配泛型使用转换

  //搭配泛型使用 将T 转成T1 R转成R1
    fun <T, T1, R, R1> covertHigher()/*: (T, T1, (T) -> R, (T1) -> R1) -> Unit*/ =
        { t: T, t1: T1, lambda: (T) -> R, lambda2: (T1) -> R1 ->
            println(lambda(t))
            println(lambda2(t1))
        }


    @Test
    fun test5() {
        covertHigher<String, Int, Int, String>()("1",1,{
            it.length
        },{
            it.toString()
        })

    }

三.手写Kotlin中的内置高阶函数

1.手写rx


    @Test
    fun testRx() {
        create { "1" }
            .mappper { 2 }
            .comsumer {
                println(it)
            }
    }

    //手写create map操作符 类似Rxjava 写法1
    //保存数据
    class Helper<T>(var item: T) {
        //将T转成R
        fun <R> mappper(action: (T) -> R): Helper<R> {
            return Helper(action(item))
        }

        fun comsumer(action: (T) -> Unit) {
            action(item)
        }
    }

    //返回Help用来保存创建的数据
    fun <R> create(action: () -> R): Helper<R> {
        return Helper(action())
    }
    @Test
    fun testRx2() {
        create2 { "1" }
            .mappper2 {
                println(length)
            }
            .comsumer2 {
                println(this)
            }
    }

    //手写create map操作符 类似Rxjava 写法2 使用扩展函数
    //保存数据
    class Helper2<T>(var item: T)

    //将T转成R
    fun <T, R> Helper2<T>.mappper2(action: T.() -> R) = Helper2(action(item))
    fun <T> Helper2<T>.comsumer2(action: (T) -> Unit) = action(item)
    //返回Help用来保存创建的数据
    fun <R> create2(action: () -> R): Helper2<R> = Helper2(action())

2.内置高阶函数

1.Apply

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

可以看到apply中lamba是一个扩展函数,并且返回的是this,也就是调用对象的本身,我们通常使用它给某个 对象设置属性,方法

data class User(var name: String, var email: String)

fun main() {
    val user = User("Bob", "bob@example.com").apply {
        name = "Alice"
        email = "alice@example.com"
    }
    println("Name: ${user.name}, Email: ${user.email}") 
}

2.also

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

also 函数会返回调用它的对象本身,这使得它适合用于在不改变对象本身的情况下,对对象执行一些额外的操作,比如日志记录、调试信息输出等。

  @Test
    fun testAlso() {
        val numbers = mutableListOf(1, 2, 3, 4, 5)
        val result = numbers
            .also { println("原始列表: $it") }
            .map { it * 2 }
            .also { println("乘以 2 后的列表: $it") }
            .filter { it > 5 }
            .also { println("过滤后大于 5 的列表: $it") }
        println("最终结果: $result")
    }

3.Run

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

可以看到run有两种使用方式。一种是作为对象的扩展函数调用;另一种是作为独立函数使用。

  • 当作为扩展函数时,它会将调用对象作为 this 上下文,在代码块中可以直接访问该对象的属性和方法。
  • 代码块的最后一行表达式的值会作为 run 函数的返回值。
    @Test
    fun runTest() {
        val person = Person("Alice", 25)
        // 作为扩展函数使用
        val result = person.run {
            age++
            "Name: $name, Age: $age"
        }
        println(result)

        // 作为独立函数使用
        val number = run {
            val a = 10
            val b = 20
            a + b
        }
        println(number)
    }

4.Let

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
  • let 函数将调用对象作为参数传递给代码块,在代码块中使用 it 来引用该对象。
  • 代码块的最后一行表达式的值会作为函数的返回值。
  • 常用于空安全处理,避免空指针异常。
     @Test
    fun testLet() {
        var message: String? = "Hello, Kotlin!"
        message?.let {
            println(it.length)
        }
    }

5.with

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}
  • with 函数接收一个对象作为参数,在代码块中,该对象作为 this 上下文,可直接访问其属性和方法。
  • 代码块的最后一行表达式的值会作为函数的返回值。
  data class Book(var title: String, var author: String)

    @Test
    fun testWith() {
        val book = Book("Kotlin in Action", "Dmitry Jemerov")
        val info = with(book) {
            "Title: $title, Author: $author"
        }
        println(info)
    }
函数名上下文引用返回值常见使用场景
alsoit调用对象本身额外操作(如日志记录
applythis调用对象本身对象初始化和配置
withthis代码块最后一个表达式的值对对象进行一系列操作并获取结果
letit代码块最后一个表达式的值空安全处理、链式调用中的转换
runthis(扩展函数),无(独立函数)代码块最后一个表达式的值对象初始化后计算结果、独立代码块执行

四.委托机制

什么是委托?委托就是通过组合的形式,传入对象/属性,让其他类帮你实现对象或者属性的实例化。

1.类委托

假设我们定义一个Dao接口,它有两个实现,我们有个仓库需要调用它获取实例,我们可以通过组合属性的方式将其实例化,我们可以使用依赖注入的方式,同时我们也可以使用委托,在Kotlin中,我们的委托帮助我们省略了不少代码,我们看下 它是如何实现的:

interface Dao {

    fun getUser(): String
}

/**
 * rom数据库实现
 */
class DaoImplRom : Dao {
    override fun getUser() =
        "return Rom User"
}

/**
 * litePal数据库实现
 */
class DaoImplLitePal : Dao {
    override fun getUser() =
        "return litePal User"
}

/**
 * 委托实现
 * 1.参数 使用接口
 * 2.实现功能接口,但是当前类无需实现接口的方法
 * 3.by + 功能
 * 4.内部即可调用接口中的方法 节省代码
 * 5.外部调用时指定传入实现即可
 *
 * @constructor
 */
class UserRepository(dao: Dao) :Dao by dao{

    fun getDaoUser():String{
        return getUser()
    }
}

    @Test
    fun testDelegateClz() {
        println(UserRepository(DaoImplRom()).getDaoUser())
    }

可以看到,dao以组合形式和仓库组合,并且它实现了Dao接口,但是我们并没有重写它的方法就获得了它的功能,我们只需要在外部传入具体实现即可。

2.属性委托

1.一般属性委托:将类中的属性委托给另外一个类实现;

2.可观察属性委托:属性改变时委托给observable观察变化

3.vetoable委托:和可观察差不多,但是可以阻止其变化

4.属性委托存储在map中。

// 1.定义一个属性委托类
class NameDelegate {
    private var value: String = ""

    //1.操作符 对象持有
    operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): String {
        println("getValue:$this,$property")
        return value
    }

    operator fun setValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>, newValue: String) {
        println("setValue:$this,$property,$newValue")
        value = newValue
    }
}

// 2.定义一个属性委托类 实现Kotlin帮我们提供的接口
class AgeDelegate : ReadWriteProperty<UserDelegate, Int> {
    private var age: Int = 0
    override fun getValue(thisRef: UserDelegate, property: KProperty<*>): Int {
        println("age getValue:$thisRef,$property")
        return age
    }

    override fun setValue(thisRef: UserDelegate, property: KProperty<*>, value: Int) {
        age = value
        println("age setValue:$thisRef,$property,$age")
    }

}

class UserDelegate {
    //属性委托给UserNameDelegate实现
    var name: String by NameDelegate()
    var age: Int by AgeDelegate()

    //3.可观察的属性委托:即属性变化时,我们在委托中会观察到它的变化
    var pressTime: Long by Delegates.observable(System.currentTimeMillis()) { _, oldValue, newValue ->

        println("pressTime oldValue:$oldValue,newValue:$newValue")
        //2次的时间间隔小于2秒就退出了
        if (newValue - oldValue < 2000) {
            println("退出")
        } else {
            println("请再次按返回键退出")
        }

    }
    //4.vetoable
    var num: Int by Delegates.vetoable(1) { _, oldValue, newValue ->
        println("newValue:$newValue")
        newValue > 2 //大于2时才容许更改
    }



}

//5. map属性委托构造方法中传入ma,然后属性可以对其赋值
class MapDelegate(map:Map<String,Any>){
    val name:String by map
    val age:Int by map
}
    @Test
    fun testDelegateClz() {
        UserRepository(DaoImplRom()).getDaoUser().also {
            println(it)
        }
    }


    @Test
    fun testDelegateProperty() {
        UserDelegate().apply {
            name = "zhangsan"
            age = 14
            println(name)
            println(age)
        }
    }


    @Test
    fun testDelegateObservableProperty() {
        UserDelegate().apply {
            pressTime = System.currentTimeMillis()
        }
    }

    @Test
    fun testDelegateVoProperty() {
        UserDelegate().apply {
            num = 2 //不容许修改
            println(num)
            num = 0 //容许修改
            println(num)
        }
    }

    @Test
    fun testDelegateMap() {
        val map = mapOf("name" to "zhangsan", "age" to 15)
            .onEach { (t, u) ->
                println("key:$t")
                println("value:$u")
            }

        MapDelegate(map)
            .also {
                println(it.name)
                println(it.age)
            }
    }

五.协程

1.协程的定义

协程可以理解为轻量级的线程,它是一种可以暂停和恢复执行的程序组件。与线程相比,协程的创建和销毁开销极小,一个线程中可以同时存在成千上万个协程。可以帮助我们使用同步的写法写出异步的效果;可以合并执行结果等,代码示例如下:

  //1.协程的好处,
        // 1.可以使用同步的方式写异步的代码

        lifecycleScope.launch {
            delay(1000)//模拟网路请求在非主线程执行
            //直接更新UI会切换到主线程
        }
        //2.合并两个请求的结果
        //如果是Rx可以用到zip操作符 或者使用ContDownLatch来获取

        lifecycleScope.launch {
            delay(1000)//模拟网路请求在非主线程执行

            //任务1
            val ret1=async {
                1
            }
            //任务2
            val ret2=async { 2 }
            //任务1执行+任务2执行 得到合并后结果
            val i = ret1.await() + ret2.await()

        }

2.协程的启动方式

1.runblocking

它会帮助我们开启一个协程,并且会在其执行完之后才结束当前线程,主要用于测试。

   @Test
    fun testRunBlocking() = runBlocking {
        // 这是一种阻塞式的协程启动方式,会阻塞当前线程直到协程体执行完毕。
        // 主要用于测试、main 函数中或桥接阻塞代码与非阻塞代码。禁止在 UI 线程中使用,
        //否则会导致应用无响应(ANR)
        println("run blocking test...")

    }

2.launch

用于启动一个不需要返回结果的协程。它返回一个 Job 对象,可以用来控制协程的生命周期,例如取消协程( job.cancel() )或等待其完成( job.join() )。这种方式适用于“发后即忘”(fire-and-forget)的后台任务,如日志上传或缓存清理。 job的有几个点需要注意下:

launch执行流程

1.外层协程启动:当调用外层的 launch 函数时,外层协程开始执行。

2.遇到内层 launch :在外层协程的执行过程中,如果遇到内层的 launch 函数,会立即启动内 层协程,而外层协程不会等待内层协程执行完毕,会继续执行自身后续的代码。

3.内外层协程并发执行:内层协程和外层协程会并发执行(具体取决于调度器的配置),它们的执行顺序是不确定的,可能交替执行。

1. Main: runBlocking 开始
8. Main: 外层协程已启动,主线程继续执行
2. Parent: 外层协程开始
5. Parent: 内层协程已启动,外层协程继续执行
3. Child: 内层协程开始
6. Parent: 外层协程部分任务执行完毕
4. Child: 内层协程执行完毕
7. Parent: 显式等待内层协程完成,外层协程结束
9. Main: 外层协程执行完毕,runBlocking 结束

   @Test
    fun test2() = runBlocking {
        println("1. Main: runBlocking 开始") // 主线程打印

        val parentJob = launch {
            println("2. Parent: 外层协程开始")

            val childJob = launch {
                println("3. Child: 内层协程开始")
                delay(1000) // 模拟耗时操作,挂起内层协程
                println("4. Child: 内层协程执行完毕")
            } // childJob 创建并立即返回

            println("5. Parent: 内层协程已启动,外层协程继续执行")
            delay(500)
            println("6. Parent: 外层协程部分任务执行完毕")

            // 等待子协程完成 (可选,因为结构化并发已隐式等待)
            childJob.join()
            println("7. Parent: 显式等待内层协程完成,外层协程结束")
        } // parentJob 创建并立即返回

        println("8. Main: 外层协程已启动,主线程继续执行")

        // 等待外层协程完成
        parentJob.join()
        println("9. Main: 外层协程执行完毕,runBlocking 结束")
    }

job的取消流程

1.如果一个 Job 有子 Job ,取消父 Job 会递归地取消所有子 Job

2.取消一个 Job 不会影响其父 Job 或兄弟 Job

3.一旦 Job 被取消,它就处于“Cancelling”然后是“Cancelled”状态,不能再被用作新 Job 的父级

   @Test
    fun testLaunch()= runBlocking {

        val job = launch { // 启动一个新协程并保持对它的引用

            //子job  如果父job
            launch {
                while (true){
                    delay(1000) //如果被取消 可能会抛出异常
                    println("job running inner")
                }
            }

            launch {
                while (isActive/* 最好是根据isActive*/){

                    try {
                        delay(1000)
                    } catch (e: CancellationException) {
                        println("协程被取消,捕获到 CancellationException: ${e.message}")
                    }
                    println("job running inner2")
                }
            }

            while (isActive){ //判断是否取消 如果一个 Job 有子 Job,取消父 Job 会递归地取消所有子 Job
                delay(1000)
                println("job running")
            }

        }

        launch {
            println("Kotlin ")
            delay(3000)
            println("cancel job")
            job.cancel()//取消job协程 它是协作时的,所以在协程中可以使用isActive来判断它的运行状态
            job.join()//挂起,直到取消操作完成
        }
        println("Hello,")

    }

3.asyn

async 是一个用于启动协程的函数,它与 launch 类似,但主要区别在于 async 会返回一个 Deferred对象,该对象可以在协程执行完成后获取其结果。

启动流程:

1.启动协程:调用 async 函数时,协程立即开始执行,async 函数会立即返回一个 Deferred 对象,而 不会等待协程执行完成。

2.并发执行:协程在后台并发执行(具体取决于调度器的配置),主线程可以继续执行后续代码。

3.获取结果:当调用 deferred.await() 时,如果协程还未执行完成,当前协程会被挂起,直到协程执 行完成并返回结果。

4.继续执行:获取到结果后,当前协程会继续执行后续代码。

  @Test
    fun testAsyn() =
        //用于启动一个需要返回结果的协程。它返回一个 Deferred<T> 对象(继承自 Job),通过调用 await()
        // 方法可以获取协程的执行结果,该操作会挂起当前协程直到结果可用。async 常用于并行计算任务
        runBlocking {



            //1.启动协程:调用 async 函数时,协程立即开始执行,async 函数会立即返回一个 Deferred 对象,而不会等待协程执行完成。
            //2.并发执行:协程在后台并发执行(具体取决于调度器的配置),主线程可以继续执行后续代码。
            //3.获取结果:当调用 deferred.await() 时,如果协程还未执行完成,当前协程会被挂起,直到协程执行完成并返回结果。
            //4.继续执行:获取到结果后,当前协程会继续执行后续代码。
            val deferred1 = async {
                delay(1000)
                println("deferred1")
                10
            }
            val deferred2 = async {
                delay(1500)
                println("deferred2")
                20
            }
            launch {
                while (isActive){
                    println("--->>>")
                    delay(500)
                }
            }
            // 等待两个协程都执行完成并获取结果
            val result1 = deferred1.await()
            val result2 = deferred2.await()

            println("结果1: $result1, 结果2: $result2")
        }

4.suspend关键字

suspend 关键字的主要作用是标记一个函数为挂起函数,挂起函数可以在执行过程中暂停(挂起)当前协程的执行,将控制权交还给调用者,并且在合适的时候恢复执行。这种特性使得协程可以在不阻塞线程的情况下处理异步操作。挂起函数可以用于协程之间的协作,例如一个协程等待另一个协程的结果。 delay 函数就是一个典型的挂起函数,用于暂停协程的执行一段时间。它具有以下三个特点:

    1. **只能在协程或其他挂起函数中调用
      **由于挂起函数的执行可能会挂起当前协程,因此它只能在协程或者其他挂起函数内部被调用。如果在普通函数中调用挂起函数,会导致编译错误。
    2. **挂起和恢复机制
      **当调用挂起函数时,当前协程会被挂起,线程可以继续执行其他任务。当挂起函数的条件满足(如异步操作完成)时,协程会恢复执行。
    3. **异常处理
      **挂起函数的异常处理和普通函数类似,可以使用 try-catch 块来捕获和处理异常。

5.withcontext

在 Kotlin 协程里,执行上下文( CoroutineContext )定义了协程执行的环境,包含了调度器( CoroutineDispatcher )、异常处理器( CoroutineExceptionHandler )等元素。 withContext 函数的主要作用就是在不同的执行上下文之间进行切换,让协程可以在合适的线程环境中执行特定的任务。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

可以看到,它的参数是一个协程的上下文和一个协程作用域扩展的挂起函数。

  • context :指定要切换到的目标执行上下文。常见的有 Dispatchers.Main (用于 Android 主线程)、 Dispatchers.IO (用于 I/O 操作)、 Dispatchers.Default (用于 CPU 密集型操作)等。
  • block :这是一个挂起函数类型的 lambda 表达式,代表要在新的执行上下文里执行的代码块。该代码块的返回值会作为 withContext 函数的返回值。

常见用法:

1.调度器的切换

2.线程的切换

  @Test
    fun testWith1() = runBlocking {
        println("当前线程: ${Thread.currentThread().name}")

        // 切换到 IO 调度器执行
        val result = withContext(Dispatchers.IO) {
            println("切换到 IO 线程: ${Thread.currentThread().name}")
            // 模拟一个耗时的 I/O 操作
            delay(1000)
            "IO 操作结果"
        }

        println("回到原线程: ${Thread.currentThread().name},结果: $result")
    }

    @Test
    fun testWith2() = runBlocking {
        // 模拟一个异步任务
        GlobalScope.launch(Dispatchers.IO) {
            // 模拟耗时操作
            delay(2000)
            val data = "从网络获取的数据"

            // 切换到主线程更新 UI
            withContext(Dispatchers.Main) {
                // 更新 UI 的操作
                // 例如 textView.text = data
                println("update UI $data  ${Thread.currentThread().name}")
            }
        }
    }

6.delay函数

delay 函数用于让当前协程挂起指定的时间,在挂起期间,它不会阻塞线程,而是会让出线程的控制权,使得其他协程或任务可以在该线程上继续执行。当指定的时间过去后,协程会恢复执行。

suspend fun delay(timeMillis: Long)

通过下面的例子可以看出,使用sleep会阻塞协程1,但是delay则不会。

 @Test
    fun testKDelay(): Unit =
        runBlocking {
            // 使用 delay 的协程
            launch {
                println("协程 1 开始执行")
                delay(1000)
                println("协程 1 暂停 1 秒后继续执行")
            }
            // 使用 Thread.sleep 的线程阻塞
            launch {
                println("协程 2 开始执行")
                Thread.sleep(3000) //会阻塞其他协程
                //delay(3000) //不会阻塞其他协程
                println("协程 2 阻塞 3 秒后继续执行")
            }
        }

delay的可取消性:

当协程被取消时,如果正在执行 delay 函数, delay 会抛出 Cancellat ionException 来响应取 消操作。

    val job=launch {
                try {
                    println("协程开始执行")
                    delay(2 * 1000)
                    println("协程执行完毕")
                } catch (call: CancellationException) {
                    println(call.message)
                    call.printStackTrace()
                }
            }
            delay(100)
            job.cancel()

3.协程的结构化编程

1.协程的作用域

Kotlin 协程的作用域( CoroutineScope )是管理协程生命周期的核心概念。它是一个轻量级的上下文,用于确保协程在特定的生命周期内运行,并在该生命周期结束时自动取消所有相关的协程,从而避免内存泄漏和资源浪费。它有以下三个核心功能。

    1. 结构化并发:作用域强制实现了结构化并发的原则。在一个作用域内启动的所有协程都会被这个作用域所管理。当作用域本身被取消( scope.cancel() ),其下所有的子协程也会被递归地取消。
    2. 生命周期关联:在 Android 开发等场景中,可以将协程作用域与组件的生命周期(如 Activity Fragment ViewModel )绑定。例如,使用 viewModelScope 可以确保在 ViewModel 被清除时,所有仍在运行的协程会自动被取消****
    3. 提供执行上下文:每个作用域都包含一个 CoroutineContext ,其中至少有一个 Job 和一个 CoroutineDispatcher (调度器)。这决定了协程的执行环境和线程。
  • 常见的作用域
1.GloableScope

GlobalScope 是一个全局作用域,它没有父级 Job ,因此不受任何外部生命周期的限制。在其内部启动的协程是“顶级”协程,必须手动管理它们的生命周期。强烈不推荐在有明确生命周期的环境中使用 GlobalScope ,因为它启动的协程不会随着组件的销毁而自动取消,极易导致内存泄漏。

// 不推荐的做法
GlobalScope.launch {
    delay(1000)
    println("This will run even if the Activity is destroyed")
}
2.ViewModelScope

这是 ViewModel 的扩展属性,提供了一个与 ViewModel 生命周期绑定的作用域。当 ViewModel onCleared() 方法被调用时, viewModelScope 会自动取消其内的所有协程。这是在 ViewModel 中启动协程的推荐方式

class MyViewModel : ViewModel() {
    fun fetchData() {
        // 推荐做法
        viewModelScope.launch {
            try {
                val result = repository.getData()
                _uiState.value = UiState.Success(result)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e)
            }
        }
    }
}
3.LifeCycleScope

这是一个 LifecycleOwner (如 Activity Fragment )的扩展属性。它提供的作用域与 Lifecycle 绑定。当 Lifecycle 进入 DESTROYED 状态时,作用域会被取消

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 在 Fragment 的 STARTED 状态下安全地观察数据流
                flow.collect { data ->
                    textView.text = data
                }
            }
        }
    }
}
4.courtineScope

coroutineScope 是 Kotlin 协程中一个关键的挂起函数,它在实现结构化并发方面起着重要作用。

coroutineScope 函数的主要功能是创建一个新的协程作用域,该作用域会继承外部协程的上下文(如调度器、异常处理器等)。 它会等待其内部启动的所有子协程都执行完毕后,才会让当前协程继续往下执行,从而确保了协程执行的结构化和有序性

suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

特点

  1. **结构化并发
    **coroutineScope 严格遵循结构化并发原则。在其作用域内启动的所有子协程形成一个“工作单元”。如果其中一个子协程抛出未捕获的异常 ,整个 coroutineScope 作用域会被取消,其他子协程也会被终止, 这样可以避免出现“孤儿协程”,即那些在父协程结束后仍在运行的协程,从而提高代码的健壮性和可维护性
  2. **挂起特性
    **由于 coroutineScope 是挂起函数,它不会阻塞线程,而是挂起当前协程的执行。在等待子协程完成的过程中,线程可以去执行其他任务,提高了线程的利用率。
  3. **上下文继承
    **coroutineScope 会继 承其父协程的上下文 。例如,如果父协程使用 Dispatchers.IO 调度器,那么 coroutineScope 内的子协程默认也会在 Dispatchers.IO 调度器指定的线程上执行。不过,在 coroutineScope 内部可以使用 withContext 等方式切换上下文。

使用场景

  1. **分组执行任务
    **当需要同时执行多个相关的协程任务,并确保所有任务都完成后再继续后续操作时,可以使用 coroutineScope。比如,从多个数据源获取数据,然后对这些数据进行汇总处理。
  2. **异常统一处理
    **coroutineScope 可以将一组相关的协程任务作为一个整体进行异常处理。当其中一个子协程抛出异常时,可以统一捕获和处理,避免异常在代码中分散处理导致的复杂性。
import kotlinx.coroutines.*

suspend fun fetchData() = coroutineScope {
    // 启动第一个子协程获取数据
    val data1 = async {
        delay(1000)
        "Data from source 1"
    }
    // 启动第二个子协程获取数据
    val data2 = async {
        delay(1500)
        "Data from source 2"
    }
    // 汇总数据
    val combinedData = listOf(data1.await(), data2.await())
    combinedData
}

fun main() = runBlocking {
    val result = fetchData()
    println("汇总数据: $result")
}

//子协程 2 抛出了一个异常,coroutineScope 会捕获这个异常并取消整个作用域,
//其他子协程也会被终止。通过 try-catch 块可以捕获并处理这个异常
import kotlinx.coroutines.*

suspend fun performTasks() = coroutineScope {
    // 启动第一个子协程
    val job1 = launch {
        delay(1000)
        println("子协程 1 执行完毕")
    }
    // 启动第二个子协程,抛出异常
    val job2 = launch {
        delay(500)
        throw RuntimeException("子协程 2 抛出异常")
    }
    try {
        // 等待所有子协程完成
        job1.join()
        job2.join()
    } catch (e: Exception) {
        println("捕获到异常: ${e.message}")
    }
}

fun main() = runBlocking {
    performTasks()
    println("主协程继续执行")
}
5.自定义作用域

你可以创建自己的 CoroutineScope 实例,通常与类的实例绑定

lass MyManager {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    fun doWork() {
        scope.launch {
            // 执行工作
        }
    }

    fun cleanup() {
        scope.cancel() // 取消所有在该作用域内启动的协程
    }
}

正确使用协程作用域是编写健壮、无内存泄漏代码的关键。基本原则是:

  • 永远不要使用 ****GlobalScope
  • 根据生命周期选择作用域 :在 ****ViewModel ****中用 ****viewModelScope ,在 ****Activity / Fragment ****中用 ****lifecycleScope
  • 自定义类应持有自己的 CoroutineScope ,并在不再需要时显式取消它

2.协程的上下文

协程的上下文( CoroutineContext )是一个非常重要的概念,它定义了协程执行的环境和规则。下面为你详细介绍协程上下文的组成、作用、使用方式以及一些常见的上下文元素。

1.job

代表协程的生命周期,用于控制协程的启动、取消和等待完成。

Job的分类:

1.job接口

Job 是一个最基础的接口,它代表协程的生命周期,可以用来控制协程的启动、取消和等待完成。 Job 接口定义了协程的基本操作和状态,主要方法和属性包括:

    • start() :启动协程,如果协程还未启动的话。
    • cancel(cause: CancellationException? = null) :取消协程,可以传入一个可选的取消原因。
    • join() :挂起当前协程,直到目标协程完成。
    • isActive :判断协程是否处于活跃状态。
    • isCompleted :判断协程是否已经完成。
    • isCancelled :判断协程是否已经被取消。
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        delay(1000)
        println("协程执行完毕")
    }
    println("协程是否活跃: ${job.isActive}")
    job.cancel()
    println("协程是否被取消: ${job.isCancelled}")
    job.join()
}
2.Deferred<T> 接口

Deferred<T> **** ****Job ****的子接口,它在 ****Job ****的基础上增加了获取协程执行结果的功能。 Deferred<T> ****通常由 ****async ****函数返回,代表一个异步计算操作,最终会返回一个类型为 ****T ****的结果。主要方法是 ****await()

  • await() :这是一个挂起函数,用于等待异步计算完成并返回结果。如果计算过程中抛出异常, await() 会重新抛出该异常。
import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred: Deferred<Int> = async {
        delay(1000)
        42
    }
    val result = deferred.await()
    println("协程执行结果: $result")
}
3.SupervisorJob

SupervisorJob Job 的一种特殊实现,它与普通 Job 的主要区别在于异常处理和取消传播机制。在普通 Job 中,如果一个子协程抛出异常,会导致整个父协程及其所有子协程被取消;而在 SupervisorJob 中,一个子协程的异常不会影响其他子协程,只会取消抛出异常的子协程本身。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisorJob = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + supervisorJob)

    scope.launch {
        try {
            delay(1000)
            throw RuntimeException("子协程 1 抛出异常")
        } catch (e: Exception) {
            println("捕获到子协程 1 的异常: ${e.message}")
        }
    }

    scope.launch {
        delay(2000)
        println("子协程 2 正常执行完毕")
    }

    supervisorJob.join()
}
4.CompletableJob

CompletableJob ****也是 ****Job ****的一种特殊实现,它提供了额外的方法来手动完成或取消 ****Job 。主要方法包括:

  • complete() :手动将 ****Job ****标记为完成状态。
  • completeExceptionally(exception: Throwable) :手动将 Job 标记为因异常而完成
import kotlinx.coroutines.*

fun main() = runBlocking {
    val completableJob = Job() as CompletableJob
    launch {
        delay(1000)
        completableJob.complete()//通过 CompletableJob 的 complete() 
            //方法手动将 Job 标记为完成状态
    }
    completableJob.join() 
    println("Job 已完成")
}
2.CoroutineDispatcher

决定协程在哪个线程或线程池上执行。 常见的调度器有:

  • Dispatchers.Main :用于 Android 主线程,只能在 Android 环境中使用。
  • Dispatchers.IO :用于处理阻塞式 I/O 操作,如文件读写、数据库查询、网络请求等。
  • Dispatchers.Default :用于 CPU 密集型操作,背后是一个固定大小的线程池。
  • Dispatchers.Unconfined :不指定执行线程,协程会在调用它的线程上开始执行,直到遇到第一个挂起函数,之后由恢复它的线程继续执行。
3.CoroutineName

为协程指定一个名称,方便调试和日志记录。

4.CoroutineExceptionHandler

可以捕获协程中未处理的异常,避免程序崩溃。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("捕获到异常: ${exception.message}")
    }
    val job = launch(exceptionHandler) {
        throw RuntimeException("模拟异常")
    }
    job.join()
}
5.使用方式

1. 创建协程时指定上下文

import kotlinx.coroutines.*

fun main() = runBlocking {
    // 指定协程的调度器为 Dispatchers.Default
    val job = launch(Dispatchers.Default) {
        println("协程在 Default 调度器上执行")
    }
    job.join()
}

2.合并上下文元素

可以使用 + 运算符将多个上下文元素合并成一个新的上下文。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val context = Dispatchers.Default + CoroutineName("myCoroutine")
    val job = launch(context) {
        println("协程名称: ${coroutineContext[CoroutineName]?.name}")
    }
    job.join()
}

3. 使用 withContext 切换上下文

withContext 是一个挂起函数,用于在协程执行过程中切换上下文。

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("当前线程: ${Thread.currentThread().name}")
    // 切换到 IO 调度器执行
    withContext(Dispatchers.IO) {
        println("切换到 IO 线程: ${Thread.currentThread().name}")
    }
    println("回到原线程: ${Thread.currentThread().name}")
}

使用注意事项:

  • 上下文合并规则:当使用 + 运算符合并上下文元素时,如果有相同类型的元素,后面的元素会覆盖前面的元素。
  • 异常处理 :不同的上下文元素对异常的处理方式不同,需要根据具体情况选择合适的异常处理机制。
  • 线程安全:在切换上下文时,要注意线程安全问题,避免出现数据竞争和并发问题。
6.作用域和上下文的关系

K otlin 协程中,上下文( CoroutineContext )和作用域( CoroutineScope )是两个核心概念,它们紧密相关,共同协作来管理协程的执行和生命周期。

  • 协程上下文( CoroutineContext ):它是一个元素的集合,这些元素定义了协程执行的环境和规则。常见的上下文元素包括 Job (管理协程的生命周期)、 CoroutineDispatcher (决定协程在哪个线程或线程池上执行)、 CoroutineName (为协程指定名称)、 CoroutineExceptionHandler (处理协程中未捕获的异常)等。
  • 协程作用域( CoroutineScope ):它用于管理协程的生命周期,确保协程在特定的生命周期内运行,并在该生命周期结束时自动取消所有相关的协程,避免内存泄漏和资源浪费。每个协程都必须在一个作用域内启动。
  1. 作用域包含上下文
    每个 CoroutineScope 都包含一个 CoroutineContext,作用域通过这个上下文来确定协程的执行环境和规则。当在作用域内启动协程时,协程会继承该作用域的上下文。
import kotlinx.coroutines.*

//customScope 包含了一个上下文,其中指定了调度器为 Dispatchers.Default 
//和协程名称为 "CustomScope"。
//在 customScope 内启动的协程会继承这个上下文,从而在指定的调度器上执行,并拥有指定的协程名称
fun main() = runBlocking {
    // 创建一个自定义作用域,指定上下文
    val customScope = CoroutineScope(Dispatchers.Default + CoroutineName("CustomScope"))
    customScope.launch {
        println("协程名称: ${coroutineContext[CoroutineName]?.name}")
        println("调度器: ${coroutineContext[CoroutineDispatcher]}")
    }
    delay(1000)
}

2.上下文影响作用域内协程的行为。

上下文的不同元素会影响作用域内协程的行为。例如, Job 元素决定了协程的生命周期管理方式, CoroutineDispatcher 决定了协程在哪个线程或线程池上执行。

import kotlinx.coroutines.*

//scope 的上下文包含了一个 Job,当取消这个 Job 时,作用域内的所有协程都会被取消
fun main() = runBlocking {
    val job = Job()
    val scope = CoroutineScope(job + Dispatchers.Default)
    val childJob = scope.launch {
        delay(1000)
        println("子协程执行完毕")
    }
    // 取消作用域的 Job,会导致作用域内的协程被取消
    job.cancel()
    childJob.join()
}

3.作用域决定上下文的生命周期

作用域的生命周期会影响其包含的上下文的生命周期。当作用域被取消时,其上下文也会被取消,从而导致作用域内的所有协程被取消。例如,在 Android 中使用 viewModelScope ,当 ViewModel 被清除时, viewModelScope 会被取消,其上下文也会被取消,所有在 viewModelScope 内启动的协程都会被自动取消。

4.可以在作用域内修改上下文

在作用域内启动协程时,可以通过 withContext 等方式修改协程的上下文。

import kotlinx.coroutines.*

//scope 的默认上下文是 Dispatchers.Default,但在协程内部通过 withContext(Dispatchers.IO) 
//切换到了 Dispatchers.IO 上下文执行任务
fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        // 在协程内部切换上下文
        withContext(Dispatchers.IO) {
            println("在 IO 调度器上执行")
        }
    }
    delay(1000)
}

上下文和作用域是 Kotlin 协程中相辅相成的两个概念。上下文为协程提供了执行的环境和规则,而作用域则负责管理协程的生命周期。它们共同确保了协程能够高效、安全地执行。

3.执行原理

基于状态机执行。