Kotlin Coroutine 最佳实践

在这里插入图片描述
协程API使用起来有一定门槛,本文整理了一些协程使用中的最佳实践,希望能为新接触的同学提供一点参考:

1.更安全地处理async{}中的异常


Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce).

不同于launchasync构建器启动的协程中发生非CancellationException异常,会向外抛出,让其父协程及其其他子协程停止。

bad

val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw Exception
fun doWork1(): Deferred<String> = scope.async { ... }
// no Exception
fun doWork2(): Deferred<String> = scope.async { ... }

@JvmStatic
fun main(args: Array<String>) {

   runBlocking {
       try {
           doWork().await() // (1)
       } catch (e: Exception) {
               ...
       }
       doWork2().await()  // (2)
   }

虽然(1)通过try/catch捕获了work1的异常,但是由于work2与work1使用同一个Job,work1的异常向上抛出影响到Job及其子协程,所以(2)仍然会崩溃

good

可以使用SupervisorJob替代Job

val job: Job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw Exception
fun doWork1(): Deferred<String> = scope.async { ... }
// no Exception
fun doWork2(): Deferred<String> = scope.async { ... }

@JvmStatic
fun main(args: Array<String>) {

   runBlocking {
       try {
           doWork().await() // (1)
       } catch (e: Exception) {
               ...
       }
       doWork2().await()  // (2)
   }

SupervisorJobJob基本类似,区别在于不会被子协程的异常所影响。

另一个推荐的做法是使用coroutineScope包装async,coroutineScope内部的抛出的异常不会影响到父协程

// may throw Exception
suspend fun doWork1(): String = coroutineScope {
    async { ... }.await()
}

fun main(args: Array<String>) {

   runBlocking {
       try {
           doWork().await()
       } catch (e: Exception) {
               ...
       }
       doWork2().await()
   }

另外,推荐使用CoroutineExceptionHandler减少每次try/catch的模板代码,

val exceptionHandler: CoroutineExceptionHandler =
	 CoroutineExceptionHandler { _, throwable ->
        ... //在此处捕获异常
     }
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job + exceptionHandler)

// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... }

fun loadData() {
   scope.launch { 
      doWork() //不需要try catch
   }
}

2.对异常进行适当的包装


async{}发生异常时,堆栈日志只有Coroutine中的信息,难以调查。此时可以通过coroutineScope捕获异常,包装后抛出,此时错误堆栈中就包含代码信息了。

try {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1()
        }
        val mayFailAsync2 = async {
            mayFail2()
        }
        useResult(mayFailAsync1.await(), mayFailAsync2.await())
    }
} catch (e: IOException) {
    // handle this
    throw MyIoException("Error doing IO", e)
} catch (e: AnotherException) {
    // handle this too
    throw MyOtherException("Error doing something", e)
}

3.使用Main Dispatcher 处理UI的更新


在类似AsyncTask的使用场景中(后台处理异步任务最重更新UI),使用Main Dispatcher启动作为父协程的上下文

bad

如果使用Dispatchers.Default启动父协程,需要通过withContext进行多余的转换

val scope = CoroutineScope(Dispatchers.Default)        

fun login() = scope.launch {
    withContext(Dispatcher.Main) { view.showLoading() }
    networkClient.login(...)
    withContext(Dispatcher.Main) { view.hideLoading() }
}

good

使用Main Dispatcher启动父协程,较少多余的withContext

val scope = CoroutineScope(Dispatchers.Main)

fun login() = scope.launch {
    view.showLoading()    
    withContext(Dispatcher.IO) { networkClient.login(...) }
    view.hideLoading()
}

4.仅在必要时使用async/await


不要仅仅为了切换协程上下文而使用async

bad

launch {
    val data = async(Dispatchers.Default) { /* code */ }.await()
}

避免async后紧跟await的写法,切换协程上下文推荐用withContext

good

launch {
    val data = withContext(Dispatchers.Default) { /* code */ }
}

5.谨慎对Job进行cancel操作


Job进行cancel,Job关联的所有子协程都将停止的同时,Job变为Completed状态,此后无法再用此Job启动协程

bad

class WorkManager {
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    
    fun doWork1() {
        scope.launch { /* do work */ }
    }
    
    fun doWork2() {
        scope.launch { /* do work */ }
    }
    
    fun cancelAllWork() {
        job.cancel()
    }
}

fun main() {
    val workManager = WorkManager()
    
    workManager.doWork1() // (1)
    workManager.doWork2() // (2)
    workManager.cancelAllWork()
    workManager.doWork1() // (3)
}

如果想停止(1)与(2)然后启动(3),上面的写法是错误的。(3)已经无法使用CompletedJob启动协程了

good

如果想取消当前启动的所有子协程,同时不影响后续的新协程的启动,应该使用CoroutineContext.cancelChildren()

class WorkManager {
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    
    fun doWork1(): Job = scope.launch { /* do work */ }
    
    fun doWork2(): Job = scope.launch { /* do work */ }
    
    fun cancelAllWork() {
        scope.coroutineContext.cancelChildren()                          
    }
}
fun main() {
    val workManager = WorkManager()
    
    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1()
}

6.避免suspend函数对Dispatcher的隐式依赖


bad

suspend fun login(): Result {
    view.showLoading()
    
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()
    
    return result
}

上面的suspend函数隐式得依赖了Main Dispatcher的上线文中,否则会造成Crash:

launch(Dispatcher.Main) {     // (1) no crash
    val loginResult = login()
    ...
}

launch(Dispatcher.Default) {  // (2) cause crash
    val loginResult = login()
    ...
}

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

good

suspend函数应该可以在任何上下文中调用

suspend fun login(): Result = withContext(Dispatcher.Main) {
    view.showLoading()
    
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    
    view.hideLoading()
	return result
}

7.注意GlobalScope的使用场景


bad

在Android中不要随处使用GlobalScope

GlobalScope.launch {
    // code
}

GlobalScope应该仅用于Application级别的任务,且生命周期应该与App一致,不应该在中途被Cancel

good

非App级别的任务,应该为其单独定义合适的CoroutineScope,例如ActivityFragment或者ViewModel

class MainActivity : AppCompatActivity(), CoroutineScope {
    
    private val job = SupervisorJob()
    
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job
    
    override fun onDestroy() {
        super.onDestroy()
        coroutineContext.cancelChildren()
    }
    
    fun loadData() = launch {
        // code
    }
}

另外一个推荐做法是使用LifecycleOwner创建CoroutineScope,更利于在不同Activity中复用

/**
 * Coroutine context that automatically is cancelled when UI is destroyed
 */
class UiLifecycleScope : CoroutineScope, LifecycleObserver {

    private lateinit var job: Job
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onCreate() {
        job = Job()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun destroy() = job.cancel()
}

//... inside Support Lib Activity or Fragment
private val uiScope = UiLifecycleScope()
override fun onCreate(savedInstanceState: bundle) {
  super.onCreate(savedInstanceState)
  lifecycle.addObserver(uiScope)
}

猜你喜欢

转载自blog.csdn.net/vitaviva/article/details/106448695