一、如何使用協程
1.1 添加依賴
implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
1.2 使用協程Coroutine
在kotlinx.coroutines包中,你可以使用launch或async啟動一個協程。從概念上講,async就像launch一樣,它啟動一個單獨的協程,協程相當于一個輕量級的線程,與其他所有的協同程式同時工作。
async和launch不同的地方在于,launch傳回一個Job并且不攜帶任何結果值,而async傳回Deffered。
Deffered表示一個輕量級的非阻塞未來,表示稍後提供結果的承諾。我們可以使用await()方法擷取一個deffered的傳回結果。Deffered本質上也是Job,是以可以在需要的時候取消它。
如果在launch中的代碼因為異常而終止,那麼它會被是為線程中未捕獲異常而導緻應用崩潰。異步代碼中未捕獲異常存儲在生成的Deffered中,并且不會在其他任何地方傳遞,除非經過處理,否則它會被靜默删除。
協程分發
在Android中,我們常用的又兩個分發器dispatcher:
uiDispatcher:将執行分發到Android主UI線程(用于父協程)
bgDispatcher:在背景線程中排程執行(用于子協程)
// dispatches execution into Android main thread
val uiDispatcher: CoroutineDispatcher = Dispatcher.Main
// represent a pool of shared thread as coroutine dispatcher
val bgDispatcher: CoroutineDispatcher = Dispatcher.IO
協程作用域
使用協程需要提供協程對應的作用域CoroutineScope或使用GlobalScope
// GlobalScope示例
class MainFragment : Fragment(){
fun loadData() = GlobalScope.launch{...}
}
//CoroutineScope示例
class MainFragment : Fragment(){
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch{...}
}
//Fragment實作CoroutineScope示例
class MainFragment : Fragment(),CoroutineScope{
override val coroutineContext: CoroutineContext
get() = Dispatcher.Main
fun loadData() = launch {...}
}
lauch+async(執行任務)
父協程通過Main Dispatcher調用的launch方法啟動。
子協程通過IO Dispatcher調用async方法啟動。
Note:父協程會一直等待它的子協程完成
Note:協程如果發生未捕獲異常,程式會崩潰
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() //ui thread
val task = async(bgDispatcher){ //background thread
// your blocking call
}
val result = task.await()
view.showDta()
}
lauch+withContext(執行任務)
使用上一個例子中的方法,我們可以正常的運作。但我們浪費了啟用第二個背景任務協程的資源。
如果我們隻啟用一個協程,可以使用withContext來優化我們的代碼。
背景任務通過帶有IO Dispatcher的withContext函數執行。
val uiScope = CoroutineScope(Dispatcher.Main)
fun loadData() = uiScope.launch {
view.showLoading() //ui thread
val result = withContext(bgDispatcher){
// your blocking call
}
view.showData(result) // ui thread
}
launch+ withContext(按順序執行兩個任務)
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result1 = withContext(bgDispatcher){
// your blocking call
}
val result2 = withContext(bgDispatcher){
//your blocking call
}
val result = result1 + result2
vuew,showData(result) //ui thread
}
launch+async+async(并行執行兩個任務)
val uiScope = CoroutineScope(Dispatcher.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val task1 = async(bgDispatcher){
//your blocking call
}
val task2 = async(bgDispatcher){
//your blocking call
}
val result = task1.await() + task2.await()
view.showData() // ui thread
}
二、如何使用協程的timeout
如果我們想為一個協程任務設定逾時,我們可以使用withTimeoutOrNull()方法,如果逾時就傳回null。
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val task = async(bgDispatcher){
//your blocking call
}
// suspend until task is finished or return null in 2s
val result = withTimeoutOrNull(2000) { task.await() }
view.showData(result) // ui thread
}
三、如何取消一個協程
3.1 job
loadData()方法傳回一個Job對象,Job對象是可以被取消的。當父協程被取消的時候,它的所有子協程都會被結束。當stopPresenting()方法被調用,view.showData()肯定不會被調用。
val uiScope = CoroutineScope(Dispatchers.Main)
val job: Job? = null
fun startPresenting(){
job = loadData()
}
fun stopPresenting(){
job?.cancel()
}
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result = withContext(bgDispatcher){
// your blocking call
}
view.showData(result) //ui thread
}
3.2 parent job
取消協程的另一種方法是建立SupervisorJob對象,并通過重載+運算符在作用域構造函數中指定它。
var job = SipervisorJob()
val uiScope = CoroutineScope(Dispatchers.Main + job)
fun startPresenting(){
loadData()
}
fun stopPresenting(){
scope.coroutineContext.cancelChildren()
}
fun loadData() = uiScope.launch {
view.showLoading()
val result = withContext(bgDispatcher) {
// your blocking call
}
view.showData(result)
}
3.3 自定義具有生命周期感覺的協程作用域
class MainScope : CoroutineScope, LifecycleObsever {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun destory() = coroutineContext.cancelChildren()
}
//使用
class MainFragment : Fragment(){
private val uiScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
lifecycle.addObserver(mainScope)
}
private fun loadData() = uiScope.launch {
val result = withContext(bgDispatcher) {
// your blocking call
}
}
}
下面,舉一個在ViewModel中使用生命周期感覺的協程。
open class ScopedViewModel : ViewModel(){
private val job = SupervisorJob()
protected val uiScope = CoroutineScope(Dispathcers.Main + job)
override fun onCleared(){
super.onCleared()
uiScope.coroutineContext.cancelChildren()
}
}
//使用
class MyViewModel : ScopedViewModel(){
private fun loadData() = uiScope.launch {
val result = withContext(bgDispatcher) {
// your blocking call
}
}
}
四、如何處理協程中的異常
4.1 try-catch
我們可以使用try-catcher捕獲并處理異常
private fun loadData() = GlobalScope.launch(uiDispatcher) {
view.showLoading()
try {
val result = withContext(bgDispatcher) { dataProvider.loadData() }
view.showData(result)
} catch(e: Exception){
e.printStackTrace()
}
}
為了避免在Presenter中使用try-catch,最好在dataProvider.loadData()函數中處理異常并使其傳回通用Result類。
data class Result(val success: T? = null,
val error: Throwable? = null)
private fun loadData() = launch(uiContext){
view.showLoading()
val task = async(bgContext) { dataProvider.loadData("Task") }
val result: Result = task.await()
if(result.success != null){
view.showData(result.success)
} else if(result.error != null){
result.error.printStackTrace()
}
}
4.2 async parent
使用async啟動父協程來忽視異常。
private fun loadData() = GlobalScope.async(uiDispatcher) {
view.showLoading()
val result = withContext(bgDispatcher) { dataProvider.loadData() }
view.showData(result)
}
使用這種方法, 異常會被儲存在job對象中。我們可以使用invokeOnCompletion()方法來取回它。
var job: Job? = null
fun startPresenting() {
job = loadData()
job?.invokeOnCompletion { it: Throwable? ->
it?.printStackTrace()
job?.getCompletionException()?.printStackTrace()
}
4.3 launch + coroutine exception handler
你可以将CoroutineExceptionHandler添加到父協同程式上下文以捕獲異常并處理它們。
val exceptionHandler: CoroutineContext = CoroutineExceptionHandler {
-, throwable->
view.showData(throwable.message)
job = Job()
}
private fun loadData() = GlobalScope.async(uiDispatcher + exceptionHandler){
view.showLoading()
val result = withContext(bgDispatcher) { dataProvider.loadData() }
view.showData(result) //如果發生異常就不會被調用
}
五、如何測試協程
啟動一個協程需要你指定一個CoroutineDispatcher。
class MainPresenter(val view: MainView,
val dataProvider: DataProviderAPI) {
private fun loadData() = GlobalScope.launch(Dispacthers.Main){
view.showLoading()
val result = withContetx(Dispatchers.IO) { dataProvider.loadData() }
view.showData(result)
}
}
如果你想為上面的MainPresenter編寫一個單元測試,你可能需要指定一個協程context用于ui和background執行。
可能最簡單的方法是向MainPresenter構造函數添加兩個參數:uiDispatcher,預設值為Main,ioContext,預設值為IO。
class MainPresnter(val view: MainView,
val dataProvider: DataProviderAPI,
val uiDispatcher: CoroutineDispatcher = UI,
val ioDispatcher: CoroutineDispatcher = IO){
private fun loadData() = GlobalScope.launch(uiDispatcher) {
view.showLoading()
val result = withContext(ioDispatcher) { dataProvider.loadData() }
view.showData(result)
}
}
現在,您可以通過提供Unconfined來輕松測試您的MainPresenter類,它隻會在目前線程上執行代碼。
@Test
fun startPresenting(){
//given
val view = mock(MainView::class.java)
val dataProvider = mock(DataProviderAPI::class.java)
val presenter = MainPresenter(view,
dataProvider,
Dispatcher.Unconfined,
Dispacther.Unconfined)
//when
presenter.startPresenting()
//then
}
六、如何實作協程線程日志
要了解哪個協同程式執行目前工作,可以通過System.setProperty打開調試工具并通過Thread.currentThread().name來記錄線程名稱。
//調式模式
System.setProperty("kotlinx.coroutines.debug", if(BuildConfig.DEBUG) "on" else "off")
launch(UI) {
log("Data loading started")
val task1 = async { log("Hello") }
val task2 = async { log("World") }
val result = task1.await() + task2.await()
log("Data loading completed: $result")
}
fun log(msg: String){
Log.d(TAG, "[${Thread.currentThread().name}] $msg")
}