协程启动简单总结

2022-10-31  本文已影响0人  梧叶已秋声

先来看看调度器Dispatchers,里面有4种CoroutineDispatcher。

// kotlinx.coroutines.Dispatchers
public actual object Dispatchers {
    @JvmStatic
    public actual val Default: CoroutineDispatcher = DefaultScheduler
    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultIoScheduler
}

一、协程启动

1.GlobalScope

来看看使用 GlobalScope 的情况下,使用不同Dispatchers下,协程运行的情况。

// GlobalScope 使用
private const val TAG = "GlobalScope"

// 不传入 CoroutineDispatcher
fun globalScopeTest() = GlobalScope.launch {
    //DefaultDispatcher-worker-1
    Log.d(TAG, "inner globalScopeTest, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.Main
fun globalScopeTest1() = GlobalScope.launch(Dispatchers.Main) {
    // main
    Log.d(TAG, "inner globalScopeTest1, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.IO
fun globalScopeTest2() = GlobalScope.launch(Dispatchers.IO) {
    // DefaultDispatcher-worker-1
    Log.d(TAG, "inner globalScopeTest2, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.Default
fun globalScopeTest3() = GlobalScope.launch(Dispatchers.Default) {
    // DefaultDispatcher-worker-2
    Log.d(TAG, "inner globalScopeTest3, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.Unconfined
fun globalScopeTest4() = GlobalScope.launch(Dispatchers.Unconfined) {
    delay(1000)
    //如果不加delay, 当前线程 = main
    //使用delay后, 当前线程 = kotlinx.coroutines.DefaultExecutor
    Log.d(TAG, "inner globalScopeTest4, 当前线程 = ${Thread.currentThread().name}")
}

调用。

class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        globalScopeTest()
        globalScopeTest1()
        globalScopeTest2()
        globalScopeTest3()
        globalScopeTest4()
    }
}

结果如下:


image.png

简单总结:调用GlobalScope 启动协程运行的线程名有3种情况

DefaultDispatcher-worker-x (使用Dispatchers.Default和Dispatchers.IO,GlobalScope不传入 CoroutineDispatcher)
main(使用Dispatchers.Main,使用Dispatchers.Unconfined不调用delay(1000))
kotlinx.coroutines.DefaultExecutor(使用Dispatchers.Unconfined并调用delay(1000)后)

传入Dispatchers.Main则运行在main线程,传入Dispatchers.Default和Dispatchers.IO则运行在DefaultDispatcher-worker-x。传入Dispatchers.Unconfined则是调用者的线程,但是当在coroutine中第一个挂起之后,后面所在的线程将完全取决于调用挂起方法的线程。

要尽量避免使用GlobalScope,因为GlobalScope是生命周期是process级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。

2.GlobalScope

// CoroutineScope 使用
private const val TAG = "CoroutineScope"
// Dispatchers.Main
fun coroutineScopeTest() = CoroutineScope(Dispatchers.Main).launch{
    // main
    Log.d(TAG, "inner coroutineScopeTest, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.IO
fun coroutineScopeTest1() = CoroutineScope(Dispatchers.IO).launch{
    // DefaultDispatcher-worker-3
    Log.d(TAG, "inner coroutineScopeTest1, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.Default
fun coroutineScopeTest2() = CoroutineScope(Dispatchers.Default).launch{
    // DefaultDispatcher-worker-1
    Log.d(TAG, "inner coroutineScopeTest2, 当前线程 = ${Thread.currentThread().name}")
}

// Dispatchers.Unconfined
fun coroutineScopeTest3() = CoroutineScope(Dispatchers.Unconfined).launch{
    // main
    Log.d(TAG, "inner coroutineScopeTest3, 当前线程 = ${Thread.currentThread().name}")
}

简单调用

class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        coroutineScopeTest()
        coroutineScopeTest1()
        coroutineScopeTest2()
        coroutineScopeTest3()
    }
}

3.lifecycleScope

添加依赖

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'

在MainActivity 中使用

private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity(){
    fun lifecycleScopeTest() = lifecycleScope.launch {
        // main
        Log.d(TAG, "inner lifecycleScopeTest, 当前线程 = ${Thread.currentThread().name}")
    }

    fun lifecycleScopeTest1() = lifecycleScope.launch(Dispatchers.IO) {
        // DefaultDispatcher-worker-2
        Log.d(TAG, "inner lifecycleScopeTest1, 当前线程 = " + Thread.currentThread().name)
        // 通过 withContext 来切换线程
        withContext(Dispatchers.Main) {
            // 当前线程 = main
            Log.d(TAG, "inner withContext, 当前线程 = ${Thread.currentThread().name}")
        }
    }

    // whenResumed和launchWhenResumed执行时机一样,在生命周期为 onResume 后调用
    // 区别在于:whenResumed 可以有返回结果 , launchWhenResumed 返回的是Job对象
    fun lifecycleScopeTest2() = lifecycleScope.launch {
        whenResumed {
            // main
            Log.d(TAG, "inner lifecycleScopeTest2 whenResumed, 当前线程 = ${Thread.currentThread().name}")
        }
    }

    fun lifecycleScopeTest3() = lifecycleScope.launchWhenResumed  {
        // main
        Log.d(TAG, "inner lifecycleScopeTest3 launchWhenResumed, 当前线程 = ${Thread.currentThread().name}")
    }


    override fun onResume() {
        super.onResume()
        Log.d(TAG, "inner onResume")
    }
}

4.viewModelScope

添加依赖

    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
private const val TAG = "ViewModelScopeTest"
class ViewModelScopeTest: ViewModel(){

    // 通过viewModelScope让框架管理生命周期,无需执行cancel
    /**
     * 没法在主线程完成的繁重操作
     */
    fun launchDataLoad() {
        // viewModelScope 默认使用 Dispatchers.Main
        viewModelScope.launch {
            // inner launch, 当前线程 = main
            Log.d(TAG, "inner launch, 当前线程 = ${Thread.currentThread().name}")
            sortList()
            // 当前线程 = main  自动切换线程
            Log.d(TAG, "inner launch after sortList, 当前线程 = ${Thread.currentThread().name}")
            // 更新 UI
        }
    }

    suspend fun sortList() = withContext(Dispatchers.Default) {
        // 繁重任务
        // DefaultDispatcher-worker-3
        Log.d(TAG, "inner withContext, 当前线程 = ${Thread.currentThread().name}")

    }
}

MainActivity中调用。

// MainActivity
    private val viewModelScopeTest by lazy {
        ViewModelProvider(this).get(ViewModelScopeTest::class.java)
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModelScopeTest.launchDataLoad()
    }

withContext 可以将当前线程从主线程切换到后台线程。然后执行完毕再切换回来到ui线程执行操作。

5.runBlocking

阻塞式不推荐使用,仅做了解。
另外除了launch,还有async可以启动协程,launch更常用。async可以返回闭包最后一行,并通过await()获取。

        // runBlocking 阻塞式 不推荐
        runBlocking {
            // 当前线程 = main
            Log.d(TAG, "inner runBlocking , 当前线程 = ${Thread.currentThread().name}")
            launch(Dispatchers.IO) {
                // 当前线程 = DefaultDispatcher-worker-1
                Log.d(TAG, "inner runBlocking launch , 当前线程 = ${Thread.currentThread().name}")
            }

            /*
            * async 和 launch  和差不多,就是比 launch多了一个返回值。
            * 返回值 写在闭包 {}最后一行即可,然后通过 await()获取结果
            * */
            val job = async(Dispatchers.IO) {
                // 当前线程 = DefaultDispatcher-worker-1
                Log.d(TAG, "inner runBlocking async , 当前线程 = ${Thread.currentThread().name}")
                23
            }
            val await = job.await()
            // 当前线程 = main , await = 23
            Log.d(TAG, "inner runBlocking , 当前线程 = ${Thread.currentThread().name} , await = $await")
        }

image.png

最好是使用lifecycleScope和viewModelScope去启动协程。这2个扩展库可以更方便地处理生命周期相关的问题。

二、协程取消

在写线程的时候,其实是没有取消的,只有中断。协程的取消api为:cancel()。
协程中有一个Job,是对一个协程的句柄。创建的每个协程,不管是通过launch还是async来启动的,它都会返回一个Job实例,唯一标识该协程,并可以通过该Job管理其生命周期。

class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        //Job和Dispatcher 组合成 CoroutineContext
        val scope = CoroutineScope(Job() + Dispatchers.Default)
        val job = scope.launch {
            Log.d(TAG, "inner CoroutineScope before delay, 当前线程 = ${Thread.currentThread().name}")
            //通过延时模拟耗时任务
            delay(1000)
            Log.d(TAG, "inner CoroutineScope after delay, 当前线程 = ${Thread.currentThread().name}")
        }
        job.cancel()
    }
}

kotlinx.coroutines 中的所有挂起函数(例如 withContext 和 delay)都是可取消的。
由于调用job.cancel()取消,所以不执行after delay这条log。


image.png

三、异常处理
协程的异常处理,即使加了try-catch也会有问题,应用可能直接崩溃,而且完全无法捕获异常信息。

换句话说:如果把Java里的那一套异常处理机制,照搬到Kotlin协程里来,你一定会四处碰壁。因为在普通的程序当中,你使用try-catch就能解决大部分的异常处理问题,但是在协程当中,根据不同的协程特性,它的异常处理策略是随之变化的
要注意以下几点:

未处理协程中抛出的异常可能会导致应用崩溃。如果可能会发生异常,请在使用 viewModelScope 或 lifecycleScope 创建的任何协程主体中捕获相应异常。

https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=zh-cn
未处理协程中抛出的异常可能会导致应用崩溃。如果可能会发生异常,请在使用 viewModelScope 或 lifecycleScope 创建的任何协程主体中捕获相应异常。
注意:如需启用协程取消流程,请不要使用 CancellationException 类型的异常(不要捕获它们,或在被发现时总是重新抛出)。首选捕获特定类型的异常(如 IOException),而不是 Exception 或 Throwable 等一般类型。

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            try {
                loginRepository.login(username, token)
                // Notify view user logged in successfully
            } catch (exception: IOException) {
                // Notify view login attempt failed
            }
        }
    }
}

总而言之,还是要具体情况具体分析。

另外,官方使用demo如下:
在 Android 应用中使用 Kotlin 协程
学习采用 Kotlin Flow 和 LiveData 的高级协程

参考链接:
【Android】在Activity中使用LifecycleScope替代GlobalScope
lifecycleScope 和viewModelScope
ViewModel中的简易协程:viewModelScope
破解 Kotlin 协程(2):协程启动篇
kotlin学习-Coroutines(协程)
Kotlin之协程coroutine使用(1)
Kotlin 协程一 —— 协程 Coroutine
协程的取消和异常Part1-核心概念
在 Android 中使用协程的最佳实践
《Kotlin编程第一课》

上一篇 下一篇

猜你喜欢

热点阅读