协程API使用起来有一定门槛,本文整理了一些协程使用中的最佳实践,希望能为新接触的同学提供一点参考:
Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce).
不同于launch
,async
构建器启动的协程中发生非CancellationException
异常,会向外抛出,让其父协程及其其他子协程停止。
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)仍然会崩溃
可以使用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)
}
SupervisorJob
与Job
基本类似,区别在于不会被子协程的异常所影响。
另一个推荐的做法是使用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
}
}
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)
}
在类似AsyncTask
的使用场景中(后台处理异步任务最重更新UI),使用Main Dispatcher
启动作为父协程的上下文
如果使用Dispatchers.Default
启动父协程,需要通过withContext
进行多余的转换
val scope = CoroutineScope(Dispatchers.Default)
fun login() = scope.launch {
withContext(Dispatcher.Main) { view.showLoading() }
networkClient.login(...)
withContext(Dispatcher.Main) { view.hideLoading() }
}
使用Main Dispatcher
启动父协程,较少多余的withContext
val scope = CoroutineScope(Dispatchers.Main)
fun login() = scope.launch {
view.showLoading()
withContext(Dispatcher.IO) { networkClient.login(...) }
view.hideLoading()
}
不要仅仅为了切换协程上下文而使用async
launch {
val data = async(Dispatchers.Default) { /* code */ }.await()
}
避免async
后紧跟await
的写法,切换协程上下文推荐用withContext
launch {
val data = withContext(Dispatchers.Default) { /* code */ }
}
对Job
进行cancel
,Job关联的所有子协程都将停止的同时,Job变为Completed
状态,此后无法再用此Job启动协程
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
启动协程了
如果想取消当前启动的所有子协程,同时不影响后续的新协程的启动,应该使用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()
}
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.
suspend函数应该可以在任何上下文中调用
suspend fun login(): Result = withContext(Dispatcher.Main) {
view.showLoading()
val result = withContext(Dispatcher.IO) {
someBlockingCall()
}
view.hideLoading()
return result
}
在Android中不要随处使用GlobalScope
GlobalScope.launch {
// code
}
GlobalScope应该仅用于Application级别的任务,且生命周期应该与App一致,不应该在中途被Cancel
非App级别的任务,应该为其单独定义合适的CoroutineScope
,例如Activity
、Fragment
或者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)
}