Kotlin 2. Kotlin Suspend 挂起函数

Kotlin挂起函数详解
本文介绍了Kotlin中的挂起函数概念及应用。挂起函数是协程的核心,用于执行耗时操作而不阻塞主线程。文章通过实例展示了如何利用挂起函数简化异步编程,并讨论了其背后的实现原理。

一起来学Kotlin:概念:2. Kotlin Suspend 挂起函数

这里介绍 Kotlin Suspend 挂起函数的使用。挂起(suspend)函数是所有协程的核心。 挂起函数可以执行长时间运行的操作并等待它完成而不会阻塞主线程。Kotlin 的 suspend 关键字可以帮助我们消除回调,用同步的写法写异步。



总结

挂起(suspend)函数是所有协程的核心。 挂起函数可以执行长时间运行的操作并等待它完成而不会阻塞主线程。

挂起函数的语法与常规函数的语法类似,不同之处在于添加了suspend关键字。 它可以接受一个参数并有一个返回类型。 但是,挂起函数只能由另一个挂起函数或在协程内调用。

suspend fun backgroundTask(param: Int): Int {
     // long running operation
}

在背后,编译器将挂起函数转换为另一个没有挂起关键字的函数,该函数接受一个类型为 Continuation<T> 的附加参数。 例如,上面的函数将由编译器转换为:

fun backgroundTask(param: Int, callback: Continuation<Int>): Int {
   // long running operation
}

本质

  • 挂起函数只能在协程或者其他挂起函数中调用。
  • 挂起的对象是协程:launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被挂起,即,从正在执行它的线程上脱离。就是说,当前线程跳过这个挂起函数,继续往下运行,但另一方面,线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程,执行完后,返回到之前挂起它的线程;
  • 简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作;
  • 挂起函数的特点是使用同步的方式完成异步任务。
  • withContext 的作用就是指定切换的线程,比如:suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO)

何时使用

如果你的某个函数比较耗时,也就是要等的操作,那就把它写成 suspend 函数。这就是原则。

耗时操作一般分为两类:I/O 操作和 CPU 计算工作。比如文件的读写、网络交互、图片的模糊处理,都是耗时的,通通可以把它们写进 suspend 函数里。

另外这个「耗时」还有一种特殊情况,就是这件事本身做起来并不慢,但它需要等待,比如 5 秒钟之后再做这个操作。这种也是 suspend 函数的应用场景。

消除回调

假设 postItem 由三个有依赖关系的异步子任务组成: requestTokencreatePostprocessPost,这三个函数都是基于回调的 API:

// 三个基于回调的 API
fun requestToken(block: (String) -> Unit)
fun createPost(
  token: String,
  item: Item,
  block: (Post) -> Unit)
)
fun processPost(post: Post)

fun postItem(item: Item) {
  requestToken { token ->
    createPost(token, item) { post ->
      processPost(post)
    }
  }
}

可以看到基于回调的 API 很容易造成大量缩进。如果代码中再加上一些条件、循环的逻辑,那么代码可读性会大大降低。Kotlin 的 suspend 关键字可以帮助我们消除回调,用同步的写法写异步

suspend fun requestToken(): String
suspend fun createPost(token: String, item: Item): Post
suspend fun processPost(post)

suspend fun postItem(item: Item) {
  val token = 🏹 requestToken()
  val post = 🏹 createPost(token, item)
  🏹 processPost(post)
}

由于 createPost 这些方法实际上是耗时的 IO 异步操作,需要等到拿到返回值才能执行后面的逻辑,但我们又不希望阻塞当前线程(通常是主线程),因此最终必须实现某种消息传递的机制,让后台线程做完耗时操作以后把结果传给主线程。

一些例子

一个基本的使用方式:

suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "BoyCoder"
}

在 Room 里面会经常用到:

@Dao
interface RegisterDatabaseDao {

    @Insert
    suspend fun insert(register: RegisterEntity)

    //@Delete
    //suspend  fun deleteSubscriber(register: RegisterEntity):Int

    @Query("SELECT * FROM Register_users_table ORDER BY userId DESC")
    fun getAllUsers(): LiveData<List<RegisterEntity>>

    @Query("DELETE FROM Register_users_table")
    suspend fun deleteAll(): Int

    @Query("SELECT * FROM Register_users_table WHERE user_name LIKE :userName")
    suspend fun getUsername(userName: String): RegisterEntity?

}

最后这个例子可以直接在 Kotlin Playground 上跑。

import kotlinx.coroutines.*
import java.util.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.Period
import java.text.SimpleDateFormat
import java.lang.Thread

var dateTimeNow = ""

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking{
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("code start: ${dateTimeNow}")
    
    launch { 
        dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    	println("1 code start: ${dateTimeNow}")
        delay(2000L)
        dateTimeNow = dateAsString(Calendar.getInstance().time.time)
        println("2 Task from runBlocking: ${dateTimeNow}")
    }
    
    coroutineScope { // Creates a new coroutine scope
        dateTimeNow = dateAsString(Calendar.getInstance().time.time)
        println("3 coroutineScope created: ${dateTimeNow}")
        val job = launch {
            dateTimeNow = dateAsString(Calendar.getInstance().time.time)
            println("4 coroutineScope job starts: ${dateTimeNow}")
            val one = doSomethingUsefulOne()
        	  val two = doSomethingUsefulTwo()
            dateTimeNow = dateAsString(Calendar.getInstance().time.time)
            println("5 coroutineScope job ends: ${dateTimeNow}")
        }
        
        val job2 = launch {
            dateTimeNow = dateAsString(Calendar.getInstance().time.time)
            println("11 coroutineScope job2 starts: ${dateTimeNow}")
            
        }
    
        delay(1000L)
        dateTimeNow = dateAsString(Calendar.getInstance().time.time)
        println("6 Task from first coroutine scope: ${dateTimeNow}") // Printed before initial launch
        //job.cancel() // This cancels nested launch's execution
    }
    
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("code end: ${dateTimeNow}")
}

fun dateAsString(
    dateInMillis: Long,
    format: String = "yyyyMMdd HH:mm:ss",
    locale: Locale = Locale.getDefault()
): String {
    val date = Date(dateInMillis)
    val formatter = SimpleDateFormat(format, locale)
    return formatter.format(date)
}

suspend fun doSomethingUsefulOne(): Int {
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("7 第一个挂起函数开始: ${dateTimeNow}")
    delay(1000L) // 假设我们在这里做了某些有用的工作
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("8 第一个挂起函数结束: ${dateTimeNow}")
    return 1
}

suspend fun doSomethingUsefulTwo(): Int {
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("9 第二个挂起函数开始: ${dateTimeNow}")
    delay(2000L) // 假设我们在这里也做了某些有用的工作
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("10 第二个挂起函数结束: ${dateTimeNow}")
    
    coroutineScope {
        val job = launch {
            doSomethingUsefulThree()
            doSomethingUsefulFour()
        }
    }
    return 2
}

suspend fun doSomethingUsefulThree(): Int {
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("9 第三个挂起函数开始: ${dateTimeNow}")
    delay(3000L) // 假设我们在这里也做了某些有用的工作
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("10 第三个挂起函数结束: ${dateTimeNow}")
    return 3
}

suspend fun doSomethingUsefulFour(): Int {
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("9 第四个挂起函数开始: ${dateTimeNow}")
    delay(3000L) // 假设我们在这里也做了某些有用的工作
    dateTimeNow = dateAsString(Calendar.getInstance().time.time)
    println("10 第四个挂起函数结束: ${dateTimeNow}")
    return 4
}

打印的结果如下:

code start: 20221009 03:15:55
3 coroutineScope created: 20221009 03:15:55
1 code start: 20221009 03:15:55
4 coroutineScope job starts: 20221009 03:15:55
11 coroutineScope job2 starts: 20221009 03:15:55
7 第一个挂起函数开始: 20221009 03:15:55
6 Task from first coroutine scope: 20221009 03:15:56
8 第一个挂起函数结束: 20221009 03:15:56
9 第二个挂起函数开始: 20221009 03:15:56
2 Task from runBlocking: 20221009 03:15:57
10 第二个挂起函数结束: 20221009 03:15:58
9 第三个挂起函数开始: 20221009 03:15:58
10 第三个挂起函数结束: 20221009 03:16:01
9 第四个挂起函数开始: 20221009 03:16:01
10 第四个挂起函数结束: 20221009 03:16:04
5 coroutineScope job ends: 20221009 03:16:04
code end: 20221009 03:16:04

有几点需要说明:

  • launch 是 CoroutineScope 的一个扩展函数,该方法在不阻塞当前线程的情况下启动新的协程,launch 里面的代码虽然有挂起函数,但还是会按顺序运行(注意,这里的挂起函数并没有用withContext选择去指定切换的线程);
  • coroutineScope 本身就是一个挂起函数,会挂起当前的协程。coroutineScope 里面的代码除了 launch,其他按照顺序运行,而 coroutineScope 里面可以 launch 多个 job,这多个 job 是并行的;
  • suspend 挂起函数里面的挂起函数是(默认)串行的(即,用同步的方式实现异步)。
<think>我们正在讨论的是在Spring框架中,与Kotlin反射相关的几个导入语句的位置。具体来说,用户询问的是`import kotlin.Unit;`, `import kotlin.reflect.KFunction;`, `import kotlin.reflect.KParameter;`, 和 `import kotlin.reflect.jvm.ReflectJvmMapping;` 在Spring项目中的使用位置。 在Spring框架中,我们可能会在以下场景中使用这些导入: 1. **处理Kotlin挂起函数suspending functions)**:Spring MVC和WebFlux支持Kotlin协程。当控制器方法是一个挂起函数时,它的实际返回类型是`Object`(因为挂起函数在编译后会有一个续体参数),但期望的返回类型可能是`Unit`(对应Void)或其他类型。因此,在处理返回值时,可能会用到`Unit`。 2. **反射操作**:如果我们在Spring应用中需要动态获取函数信息(如函数参数、返回值等),可能会使用到`KFunction`和`KParameter`。例如,在自定义注解处理器或者动态代理中。 3. **与Java反射互操作**:`ReflectJvmMapping`用于在Kotlin反射和Java反射之间进行映射。比如,将Kotlin的`KFunction`转换为Java的`Method`,这在需要与Java反射API交互的场景中非常有用。 具体位置可能出现在: - 自定义的`HandlerMethodArgumentResolver`中,解析控制器方法的参数。 - 自定义的`ReturnValueHandler`中,处理控制器方法的返回值,特别是对挂起函数的返回值进行处理(因为挂起函数在字节码层面返回的是`Object`,但逻辑上可能是`Unit`或具体类型)。 - 在AOP切面中,通过反射获取方法信息进行增强。 - 在Spring Data或Spring Security等扩展中,处理Kotlin特定的反射逻辑。 例如,在Spring WebFlux中,有一个`CoroutinesUtils`类(位于`org.springframework.core`包中)会使用到`KFunction`和`ReflectJvmMapping`来判断一个方法是否为挂起函数,并获取其返回类型。同时,`Unit`类型也经常用于判断挂起函数的返回类型是否是`Unit`(即没有返回值)。 另外,在Spring对Kotlin的支持中,如`KotlinDetector`、`KotlinDelegate`等工具类中,也可能使用到这些反射相关的导入。 因此,这些导入通常出现在Spring框架中与Kotlin集成相关的模块,或者开发者在编写需要处理Kotlin特有特性的Spring组件时。 §§相关问题§§ 1. Spring框架中哪些模块或类会使用到Kotlin反射API? 2. 在Spring MVC中如何处理Kotlin挂起函数的返回值? 3. 如何使用ReflectJvmMapping在Spring项目中实现Kotlin反射与Java反射的互操作? 4. 在自定义Spring组件中,如何利用KFunction和KParameter实现动态方法调用? 5. 为什么在Spring中使用Kotlin协程时需要特别处理Unit类型? </think>在Spring框架中使用Kotlin反射相关的导入(`kotlin.Unit`、`kotlin.reflect.KFunction`、`kotlin.reflect.KParameter`、`kotlin.reflect.jvm.ReflectJvmMapping`)主要出现在以下场景: 1. **Kotlin与Spring AOP集成** 在实现自定义切面(Aspect)时,通过Kotlin反射解析方法签名,获取参数注解或返回值类型。例如: ```kotlin @Aspect class CustomAspect { @Before("@annotation(org.springframework.transaction.annotation.Transactional)") fun logTransaction(kp: KParameter) { // 使用KParameter解析事务方法参数 } } ``` 2. **动态代理与Bean增强** 使用`ReflectJvmMapping`将Java反射对象转为Kotlin反射对象,实现基于Kotlin特性的动态代理: ```kotlin val javaMethod = MyService::class.java.getMethod("execute") val kFunction: KFunction<*> = ReflectJvmMapping.getKotlinFunction(javaMethod)!! ``` 3. **挂起函数(Coroutines)处理** 在Spring WebFlux或协程支持中,通过`Unit`和`KFunction`识别挂起函数的返回类型: ```kotlin suspend fun fetchData(): Unit { ... } // Unit表示挂起函数无实际返回 ``` 4. **自定义注解处理器** 结合Spring的`BeanPostProcessor`,用Kotlin反射解析带注解的方法: ```kotlin override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any { bean::class.members.filterIsInstance<KFunction<*>>() .forEach { kFunction -> kFunction.findAnnotation<MyCustomAnnotation>()?.let { ... } } return bean } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破浪会有时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值