Kotlin协程场景化学习

2,328 阅读5分钟

何为Kotlin协程?

协程是一种并发设计模式,Kotlin协程是一个线程框架。

为什么需要Kotlin协程?

提供方便的线程操作API,编写逻辑清晰且简洁的线程代码。

协程是Google在 Android 上进行异步编程的推荐解决方案。具有如下特点:

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
  • 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

如何使用Kotlin协程

添加依赖

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

使用场景

启动协程

若我们需要执行一个简单的后台(或者前台)任务,通过GlobalScope.launch可以快速的启动一个协程来处理业务逻辑,同时,也可以通过Dispatchs来指定线程类型:Dispatchers.Default、Dispatchers.IO、Dispatchers.Main、Dispatchers.Unconfined

GlobalScope.launch(Dispatchers.IO) {
    delay(1000)
    Log.d(TAG, "processIO in ${Thread.currentThread().name}")
}

切换线程

主线程 => IO线程 => 主线程。这种场景开发过程中使用最多,比如后台获取一张照片,然后前台显示。

// 主线程内启动一个协程
GlobalScope.launch(Dispatchers.Main) {
    // 切换到IO线程
    withContext(Dispatchers.IO) {
        delay(1000)
        Log.d(TAG, "processIO in ${Thread.currentThread().name}")
    }
    // 自动切回主线程
    Log.d(TAG, "processUI in ${Thread.currentThread().name}")
}

运行结果:

2021-01-02 18:38:23.812 15506-15535/tech.kicky.coroutine D/Coroutine Sample: processIO in DefaultDispatcher-worker-1
2021-01-02 18:38:23.813 15506-15506/tech.kicky.coroutine D/Coroutine Sample: processUI in main

取消协程

private fun cancelCoroutine() {
    val job = GlobalScope.launch(Dispatchers.IO) {
        for (i in 0..10000) {
            delay(1)
            Log.d(TAG, "count = $i")
        }
    }
    Thread.sleep(30)
    job.cancel()
    Log.d(TAG, "Coroutine Cancel")
}

执行结果如下:

2021-01-02 18:53:37.680 23240-23279/tech.kicky.coroutine D/Coroutine Sample: count = 0
2021-01-02 18:53:37.682 23240-23278/tech.kicky.coroutine D/Coroutine Sample: count = 1
2021-01-02 18:53:37.685 23240-23280/tech.kicky.coroutine D/Coroutine Sample: count = 2
2021-01-02 18:53:37.687 23240-23280/tech.kicky.coroutine D/Coroutine Sample: count = 3
2021-01-02 18:53:37.689 23240-23280/tech.kicky.coroutine D/Coroutine Sample: count = 4
2021-01-02 18:53:37.690 23240-23280/tech.kicky.coroutine D/Coroutine Sample: count = 5
2021-01-02 18:53:37.693 23240-23280/tech.kicky.coroutine D/Coroutine Sample: count = 6
2021-01-02 18:53:37.696 23240-23240/tech.kicky.coroutine D/Coroutine Sample: Coroutine Cancel

LifecycleOwner配合使用

由于Kotlin协程主要作用是处理线程操作,若处理不当会出现内存泄露等问题,如Activity或者Fragment已销毁,但是界面内的协程却依旧在执行,就会产生内存泄露的问题。所以,我们在界面销毁时,必须取消界面内的协程操作。我们可以自己在界面销毁时调用cancel()方法,但是无良好的编程习惯就很容易被忽略。建议采用Google给我们提供的扩展方法

implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
private fun lifecycleCoroutine() {
    // 主线程内启动一个协程
    lifecycleScope.launch {
        // 切换到IO线程
        withContext(Dispatchers.IO) {
            delay(1000)
            Log.d(TAG, "processIO in ${Thread.currentThread().name}")
        }
        // 自动切回主线程
        Log.d(TAG, "processUI in ${Thread.currentThread().name}")
    }
}

注意:

1. lifecycleScope.launch()默认就是在主线程启动协程;

2. lifecycleScope内的协程在Lifecycle为destroyed状态时会自动取消。

3.lifecycleScope还有一些其他的扩展方法,如launchWhenCreated、launchWhenStarted、launchWhenResumed等,用法从方法名上看很明显

ViewModel配合使用

协程与LifecycleOwner配合使用解决的是界面生命周期变化过程中协程的处理问题。但是针对屏幕旋转这种界面重建的场景,ViewModel对象的存在时间比LifecycleOwner要持久。虽然界面需要重建,但是协程不一定要被取消,这个需要结合具体需求考虑。

fun viewModelCoroutine() {
    viewModelScope.launch {
        Log.d("Coroutine Sample", Thread.currentThread().name)
    }
}

注意:

1. viewModelScope.launch()默认也是在主线程启动协程;

2. viewModelScope内的协程在ViewModel将被onCleared时会自动取消。

Retrofit真香组合

说重点,Retrofit 2.6之后的版本支持使用Kotlin的协程。那么,具体如何支持?

  • 添加依赖
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
  • 添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
  • Retrofit
object Retrofitance {
    private val client: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .build()
    }

    private val retrofitance: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl("https://www.wanandroid.com")
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
    }

    val wanAndroidApi: WanAndroidApi by lazy {
        retrofitance.create(WanAndroidApi::class.java)
    }
}
  • API
interface WanAndroidApi {

    @GET("/banner/json")
    suspend fun banners(): WanAndroidRoot<Banner>
}

重点关注API里的suspend关键字。suspend是挂起的意思,提醒开发者此方法为耗时方法。

  • 执行网络请求
class MainViewModel : ViewModel() {

    val banners = MutableLiveData<List<Banner>>()

    fun viewModelCoroutine() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = Retrofitance.wanAndroidApi.banners()
            banners.postValue(result.data)
        }
    }
}
private val viewModel by viewModels<MainViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
    viewModel.banners.observe(this, {
        val content: List<String> = it.map { banner ->
            banner.title
        }
        binding.text.text = content.toTypedArray().contentToString()
    })
    viewModel.viewModelCoroutine()
}

Retrofit请求依赖

针对存在依赖关系的网络请求,未使用协程之前,我们需要再回调中处理,一层两层尚可,层次多了就容易凌乱。使用协程,按照顺序编写代码,简洁清晰。

fun viewModelSequenceRequest() {
    viewModelScope.launch(Dispatchers.IO) {
        val start = System.currentTimeMillis()
        // 先请求首页Banners
        val result = Retrofitance.wanAndroidApi.banners()
        banners.postValue(result.data)
        // 再请求热键,只要是顺序执行即可且上一次的请求结果已拿到即可满足我们的使用场景。
        val keys = Retrofitance.wanAndroidApi.hotKeys()
        hotKeys.postValue(keys.data)
        Log.d("Coroutine Sample", (System.currentTimeMillis() - start).toString())
    }
}

Retrofit并发结果合并

针对多并发执行,结果统一处理,然后再执行其他内容的场景。未使用协程之前,我们可以采用RxJava的zip操作符处理。协程async/await轻松胜任。

fun viewModelAsync() {
    viewModelScope.launch(Dispatchers.IO) {
        val start = System.currentTimeMillis()
        val result = async { Retrofitance.wanAndroidApi.banners() }
        val keys = async { Retrofitance.wanAndroidApi.hotKeys() }
        Log.d(
                "Coroutine Sample",
                (result.await().data.size + keys.await().data.size).toString()
            )
        Log.d("Coroutine Sample", (System.currentTimeMillis() - start).toString())
    }
}

和上一个例子的代码相同,但是执行时间却会少很多,因为这个不是顺序执行而是并发执行。

总结

个人认为Kotlin协程的基本使用重点关注如下三个方面:

  • 线程切换
  • 如何避免内存泄露(与LifecycleOwner、ViewModel等配合使用)
  • 搭配Retrofit

当然,Kotlin Coroutine库里面还有很多操作符和方法有待探索,即学即用。

源码

github.com/onlyloveyd/…

Kotlin Couroutine源码

关注我

关注公众号【OpenCV or Android】

回复【计算机视觉】【Android】【Flutter】【OpenCV】白嫖学习资料。