轉自: http://www.lxweimin.com/p/acbd9bf65063
一、什么是Coroutines(協程)
協程是很久之前就提出的一個概念,目前支持協程的語言包括 lua、C#、go等。也包括Android官方開發語言Kotlin。當然網上對此也有很多的爭議,很多說法認為Kotlin中的協程是個偽協程,沒有實現go語言的那種協程特性,而僅僅是對于java線程的一個包裝,本文也認同這種觀點,因為它并沒有脫離JVM來實現,所以仍然受java線程模型限制。這里只去談論Kotlin協程的用法和原理,暫時拋開對于協程概念的不同理解。
kotlinx.coroutines 是由 JetBrains開發的功能豐富的協程庫。它涵蓋很多啟用高級協程的原語,包括 launch、 async 等等。
coroutines通過掛起函數的概念完成協程任務調度,協程是輕量級線程,本質上是在線程上進行任務調度。甚至可以粗俗的理解為類似于進程和線程的關系,一個進程中可以包括多個線程,而一個線程中可以包括多個協程。但執行上是有區別的,一個進程中可以有多個線程同時并發執行,但是一個線程中的多個協程本質上是順序執行的,是應用協程掛起的方式來表現為并發執行。
二、協程創建
1.協程的創建主要有三種方式:
1)launch創建。
返回值是Job,Job用來處理協程的取消等操作。這種創建方式是非阻塞的,創建的協程并不會阻塞創建協程的線程,也可以通過Job的join方法阻塞線程,來等待協程執行結束。如果當前創建處沒有協程上下文信息需要使用GlobalScope調用launch方法以頂層協程的方式創建。但是用GlobalScope.launch和直接用launch方式創建有一些區別,GlobalScope.launch默認是開啟新線程來執行協程任務的,launch是直接在當前上下文中的線程執行。
valcoroutineJob=GlobalScope.launch{Log.d(TAG,"current Thread is${Thread.currentThread()}")}Log.d(TAG,"GlobalScope.launch create coroutine")
可以看到輸出的日志順序是先輸出協程外部的日志,后輸出協程內部的日志,并且協程內部任務的執行是在工作線程。
2020-05-2115:52:39.13720964-20964/com.common.coroutines_retrofit_okhttp D/MainActivity:GlobalScope.launchcreate coroutine2020-05-2115:52:39.13820964-20997/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[DefaultDispatcher-worker-1,5,main]
這里可能會有人有疑問,因為協程在工作線程執行,工作線程本身就不會阻塞主線程,為了進一步驗證這種方式創建了非阻塞的協程,在協程的創建時指定協程執行在主線程。
valcoroutineJob=GlobalScope.launch(Dispatchers.Main){Log.d(TAG,"current Thread is${Thread.currentThread()}")}Log.d(TAG,"GlobalScope.launch create coroutine")
可以看到輸出的日志順序仍然和之前一樣,但是協程執行的線程變成了主線程。從這里可以看出協程并沒有阻塞住主線程的執行。
2020-05-2115:55:59.66422312-22312/com.common.coroutines_retrofit_okhttp D/MainActivity:GlobalScope.launchcreate coroutine2020-05-2115:55:59.69522312-22312/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[main,5,main]
2)runBlocking創建。
返回一個指定的類型,類型由協程任務的返回值控制,阻塞式創建,這種方式會阻塞住創建協程的線程,只有協程執行結束才能繼續線程的下一步執行,默認執行在創建協程的線程。
valcoroutine2=runBlocking{Log.d(TAG,"current Thread is${Thread.currentThread()}")}Log.d(TAG,"runBlocking create coroutine")
從日志輸出可以看到在協程執行完畢,主線程的日志才進行打印。
2020-05-2115:57:27.92722781-22781/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[main,5,main]2020-05-2115:57:27.92722781-22781/com.common.coroutines_retrofit_okhttp D/MainActivity:runBlocking create coroutine
為了進一步驗證阻塞性,指定runBlocking創建的協程在工作線程執行,并且在協程中模擬一個耗時任務。
valcoroutine2=runBlocking(Dispatchers.IO){Log.d(TAG,"current Thread is${Thread.currentThread()}")delay(5000)}Log.d(TAG,"runBlocking create coroutine")
從日志中可以看到協程執行在工作線程,但是主線程仍然等待5秒,等待協程執行完畢。
2020-05-2115:58:47.50623031-23106/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[DefaultDispatcher-worker-1,5,main]2020-05-2115:58:52.51623031-23031/com.common.coroutines_retrofit_okhttp D/MainActivity:runBlocking create coroutine
3)async創建。
返回值是Deferred,非阻塞式創建,很類似launch方式。如果當前創建處沒有協程上下文信息也需要使用GlobalScope調用async方法創建,GlobalScope.async和直接用async方式創建的區別和launch是一樣的。主要是特點是處理協程并發,當多個協程在同一個線程執行時,一個協程掛起了,不會阻塞另一個協程執行。
runBlocking{varstartTime=System.currentTimeMillis()valtime=measureTimeMillis{valdeferred1=async{delay(2000L)Log.d(TAG,"deferred1 get result , current thread is{Thread.currentThread()}")}Log.d(TAG,"result is
time")Log.d(TAG,"cost time2 is${System.currentTimeMillis()-startTime}")}
從日志中可以看出兩個協程執行總耗時大概3s中,并不是兩個協程總體延遲5s,說明在第一個協程掛起進行延時的時候,第二個協程已開始調度執行。并且兩個協程都是在runBlocking所在的主線程中執行
2020-05-2116:00:23.53423638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:deferred1getresult,current threadisThread[main,5,main]2020-05-2116:00:24.53623638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:deferred2getresult,current threadisThread[main,5,main]2020-05-2116:00:24.53823638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:resultis1502020-05-2116:00:24.53923638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:cost timeis30112020-05-2116:00:24.53923638-23638/com.common.coroutines_retrofit_okhttp D/MainActivity:cost time2is3012
2.協程可以嵌套使用。
父子協程來執行不同的任務。在協程的嵌套中子協程可以省略GlobalScope,直接調用launch和async就可以進行創建,這樣直接共用父協程的作用域,在父協程所在的線程執行。也可以通過Dispatchers指定作用的線程。GlobalScope其實是協程的作用域,協程的執行必須有作用域,這個后面會講解到。這里舉一個最簡單的嵌套的例子。
runBlocking { launch { Log.d(TAG, "launch current Thread is {Thread.currentThread()}") }
2020-05-2116:02:11.16124076-24076/com.common.coroutines_retrofit_okhttp D/MainActivity:currentThreadisThread[main,5,main]2020-05-2116:02:11.16224076-24076/com.common.coroutines_retrofit_okhttp D/MainActivity:launch currentThreadisThread[main,5,main]
可以看到runBlocking內部通過launch又創建了一個協程,并且launch使用runBlocking的協程上下文在主線程中執行。
協程嵌套有幾個需要注意的點:
1)父協程取消執行的時候,子協程也會被取消執行。
2)父協程總是會等待子協程執行結束。
3.掛起函數
說起協程就必須講掛起函數的概念,掛起函數是實現協程機制的基礎,Kotlin中通過suspend關鍵字聲明掛起函數,掛起函數只能在協程中執行,或者在別的掛起函數中執行。delay就是一個掛起函數,掛起函數會掛起當前協程。協程會等待掛起函數執行完畢再繼續執行其余任務。
privatesuspendfundoWork(){Log.d(TAG,"doWork start")delay(5000)Log.d(TAG,"doWork end")}
這里定義一個掛起函數,打印兩行日志,在這兩行日志之間調用delay掛起函數掛起協程5s中。
2020-05-2116:04:40.02225119-25119/?D/MainActivity:doWork start2020-05-2116:04:45.02525119-25119/?D/MainActivity:doWorkend
三、協程取消與超時
1.協程取消。
協程提供了取消操作,如果一個協程任務未執行完畢,但是執行結果已經不需要了,這時可以調用cancel函數取消協程,也可以調用cancelAndJoin方法取消協程并等待任務結束,相當于調用cancel然后調用join。
runBlocking{valjob=launch{delay(500)Log.d(TAG,"launch running Coroutines")}Log.d(TAG,"waiting launch running")job.cancelAndJoin()Log.d(TAG,"runBlocking running end")}
2.超時處理
協程在執行中可能超過預期的執行時間,這時候就需要取消協程的執行,協程提供了withTimeout函數來處理超時的情況,但是withTimeout函數在超時的時候會拋出異常TimeoutCancellationException,可以選擇捕獲這個異常。協程也提供了withTimeoutOrNull函數并返回null來替代拋出異常。
/**
添加超時處理
withTimeout
*/funtimeOutCoroutines()=runBlocking{withTimeout(1300L){repeat(1000){i->Log.d(TAG,"I'm sleeping$i...")delay(500L)}}}
四、協程調度器與作用域
1.協程調度器
協程上下文包含一個協程調度器,即CoroutineDispatcher,它確定了哪些線程或與線程相對應的協程執行。協程調度器可以將協程限制在一個特定的線程執行,或將它分派到一個線程池,亦或是讓它不受限地運行。所有的協程構建器諸如 launch 和 async 接收一個可選的 CoroutineContext 參數,它可以被用來顯式的為一個新協程或其它上下文元素指定一個調度器。
/**
協程上下文(實際控制協程在那個線程執行)
launch和async都可接收CoroutineContext函數控制協程執行的線程
Dispatchers.Unconfined一種特殊的調度器(非受限調度器),運行在默認的調度者線程,掛起后恢復在默認的執行者kotlinx.coroutines.DefaultExecutor中執行
Dispatchers.Default 默認調度器,采用后臺共享的線程池(不傳上下文,默認采用這種)
newSingleThreadContext 單獨生成一個線程
Dispatchers.IO IO線程
*/funcoroutineConetxt()=runBlocking{launch{// 運行在父協程的上下文中,即 runBlocking 主協程Log.d(TAG,"Im working in thread {Thread.currentThread().name}")delay(500)Log.d(TAG,"Unconfined after I'm working in thread
{Thread.currentThread().name}")}launch(newSingleThreadContext("MyOwnThread")){// 將使它獲得一個新的線程Log.d(TAG,"newSingleThreadContext I'm working in thread
{Thread.currentThread().name}")}}
2020-05-2116:06:32.75225509-25509/com.common.coroutines_retrofit_okhttp D/MainActivity:Unconfined before I'mworkinginthread main2020-05-2116:06:32.76425509-25553/com.common.coroutines_retrofit_okhttp D/MainActivity:Default I'mworkinginthread DefaultDispatcher-worker-12020-05-2116:06:32.76625509-25555/com.common.coroutines_retrofit_okhttp D/MainActivity:newSingleThreadContext I'mworkinginthread MyOwnThread2020-05-2116:06:32.76625509-25553/com.common.coroutines_retrofit_okhttp D/MainActivity:IO I'mworkinginthread DefaultDispatcher-worker-12020-05-2116:06:32.76625509-25509/com.common.coroutines_retrofit_okhttp D/MainActivity:Im workinginthread main2020-05-2116:06:33.25525509-25552/com.common.coroutines_retrofit_okhttp D/MainActivity:Unconfined after I'mworkinginthread kotlinx.coroutines.DefaultExecutor
從日志輸出可以看到。
1)launch默認在調用的協程上下文中執行,即runBlocking所在的主線程。
2)Dispatchers.Unconfined在調用線程啟動以一個協程,掛起之后再次恢復執行在默認的執行者kotlinx
.coroutines.DefaultExecutor線程中執行。
3)Dispatchers.Default默認調度器,開啟新線程執行協程。
4)Dispatchers.IO創建在IO線程執行。
5)newSingleThreadContext創建一個獨立的線程執行。
如果需要在協程中控制和切換部分任務執行所在的線程,可通過withContext關鍵字。withContext關鍵字接收的也是協程調度器,由此控制切換任務所在線程。
/**
- withContext 線程切換
*/funswitchThread()=runBlocking{launch{Log.d(TAG,"start in thread{Thread.currentThread().name}")}Log.d(TAG,"end in thread${Thread.currentThread().name}")}}
2020-05-2116:07:55.22525723-25723/com.common.coroutines_retrofit_okhttp D/MainActivity:startinthread main2020-05-2116:08:00.23925723-25796/com.common.coroutines_retrofit_okhttp D/MainActivity:I'mworkinginthread DefaultDispatcher-worker-12020-05-2116:08:00.24025723-25723/com.common.coroutines_retrofit_okhttp D/MainActivity:endinthread main
從日志輸出可以看到withContext將任務調度到IO線程執行。
協程作用域
協程都有自己的作用域(CoroutineScope),協程調度器是在協程作用域上的擴展,協程的執行需要由作用域控制。除了由不同的構建器提供協程作用域之外,還可以使用coroutineScope構建器聲明自己的作用域。它會創建一個協程作用域并且在所有已啟動子協程執行完畢之前不會結束。runBlocking 與 coroutineScope 可能看起來很類似,因為它們都會等待其協程體以及所有子協程結束。 這兩者的主要區別在于,runBlocking 方法會阻塞當前線程來等待, 而 coroutineScope 只是掛起,會釋放底層線程用于其他用途。 由于存在這點差異,runBlocking 是常規函數,而 coroutineScope 是掛起函數。
/**
協程作用域 coroutineScope創建協程作用域
runBlocking會等待協程作用域內執行結束
*/funmakeCoroutineScope()=runBlocking{launch{Log.d(TAG,"launch current Thread is{Thread.currentThread()}")}Log.d(TAG,"coroutineScope current Thread is
{Thread.currentThread()}")}
五、應用
從以上分析應該知道協程可以用來做什么了,協程可用來處理異步任務,如網絡請求、讀寫文件等,可以用編寫同步代碼的方式來完成異步的調用,省去了各種網絡、異步的回調。這里做一個最簡單的網絡請求的例子,使用Retrofit+Okhttp請求網絡數據,然后用Glide加載請求回來的圖片。以前寫網絡請求的時候往往封裝一套RxJava+Retrofit+Okhttp來處理,這里將RxJava替換成Coroutines(協程)。
image
主要看請求網絡相關的代碼。
classMainViewModel:ViewModel(){companionobject{constvalTAG="MainViewModel"}privatevalmainScope=MainScope()privatevalrepertory:MainRepositorybylazy{MainRepository()}vardata:MutableLiveData<JsonBean>=MutableLiveData()fungetDataFromServer()=mainScope.launch{valjsonBeanList=withContext(Dispatchers.IO){Log.d(TAG,"${Thread.currentThread()}")repertory.getDataFromServer()}data.postValue(jsonBeanList)}overridefunonCleared(){super.onCleared()mainScope.cancel()}}
使用了MainScope來引入協程作用域,在這里跟正常使用GlobalScope.launch來創建運行在主線程的協程是一樣的,然后在協程中通過withContext開啟IO線程執行聯網請求。
classMainRepository{suspendfungetDataFromServer():JsonBean{returnRetrofitRequest.instance.retrofitService.json()}}
classRetrofitRequestprivateconstructor(){privatevalretrofit:Retrofitbylazy{Retrofit.Builder().client(RetrofitUtil.genericClient()).addConverterFactory(GsonConverterFactory.create()).baseUrl(RetrofitUtil.baseUrl).addCallAdapterFactory(CoroutineCallAdapterFactory()).build()}valretrofitService:RetrofitServicebylazy{retrofit.create(RetrofitService::class.java)}companionobject{valinstance:RetrofitRequestbylazy(mode=LazyThreadSafetyMode.SYNCHRONIZED){RetrofitRequest()}}}
interfaceRetrofitService{@GET(Api.json)suspendfunjson():JsonBean}
這里導入了JakeWharton大神編寫的retrofit2-kotlin-coroutines-adapter適配器來做轉換,替換之前的Retrofit轉RxJava的適配器。可以看到處理線程切換只需要withContext一行代碼,并且沒有類似CallBack的回調,整體代碼編寫就是同步代碼的方式。之前使用RxJava的時候還需要對RxJava鏈式請求進行一些封裝來完成網絡請求的CallBack。代碼如下:
fun<T>Observable<T>.parse(success:(T)->Unit){this.subscribeOn(Schedulers.io()).unsubscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object:Subscriber<T>(){overridefunonNext(t:T){success(t)}overridefunonCompleted(){}overridefunonError(e:Throwable?){}})}
創建了一個Observable的擴展函數parse,通過success函數將網絡請求結果回傳到界面層,相比RxJava協程不需要進行添加CallBack。
Demo地址:
Coroutines
https://github.com/24KWYL/Coroutines-Retrofit-Okhttp
RxJava
https://github.com/24KWYL/MVVM
六、總結
通過協程可以很方便的處理異步任務,可以用同步的方式處理異步請求,減少回調代碼。協程也提供Flow、Channel等操作,類似于RxJava的流式操作。功能上在很多地方可以替換RxJava,也可以實現RxJava的多種操作符。并且使用上更加簡單。
作者:24k金
鏈接:http://www.lxweimin.com/p/acbd9bf65063
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。