根據維基百科的定義,協程(Coroutine)是計算機程序的一類組件,推廣了協做式多任務的子程序,容許執行被掛起與被恢復。android
協程(Coroutine)並非一個新詞,馬爾文·康威於1958年發明了術語「coroutine」,並將它用於彙編程序。而在其餘語言,如Go、Python也都有協程的概念,因此它也不是Kotlin獨有的。git
在不一樣的語言層面上,協程的實現方式是不太同樣的,本文介紹的Kotlin協程在本質上,它是一種輕量級的線程。github
Kotlin協程是運行在線程中的,這裏的線程能夠是單線程,也能夠是多線程。在單線程使用協程,比不使用協程的耗時並不會少。編程
上面介紹的都是協程的一些概念,以及Kotlin協程的特色。那究竟爲何會有Kotlin協程?它究竟比線程好在哪裏?咱們繼續往下看。promise
在Kotlin中,協程就是線程的封裝,它提供了一套標準的API來幫助咱們寫併發任務。回想一下,在Java和Android中,咱們是怎麼寫併發任務的?markdown
在Java中,咱們可使用線程或者線程池來實現多任務併發:網絡
//線程
new Thread(new Runnable() {
@Override
public void run() {
//耗時的工做
}
}).start();
//線程池
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new Runnable() {
@Override
public void run() {
//耗時的工做
}
});
複製代碼
在Android中,除了能夠經過Java的方式,建立線程、使用線程池實現多任務併發以外,還能夠AsyncTask
等方式來實現多個耗時任務的併發執行:多線程
//AsyncTask
public abstract class AsyncTask<Params, Progress, Result> {
//線程池中執行,執行耗時任務
protected abstract Result doInBackground(Params... params);
//UI線程中執行,後臺任務進度有變化則執行該方法
protected void onProgressUpdate(Progress... values) {}
//UI線程執行,耗時任務執行完成後,該方法會被調用,result是任務的返回值
protected void onPostExecute(Result result) {}
}
複製代碼
不管是Java仍是Android提供的組件,均可以實現多任務併發的執行,可是上面的組件都或多或少存在着問題:併發
AsyncTask
處理的回調方法比較多,當有多個任務時可能會出現回調嵌套。繼續以AsyncTask
舉個🌰:異步
AsyncTask<String, Integer, String> task = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String... strings) {
String userId = getUserId(); //獲取userId
return userId;
}
@Override
protected void onPostExecute(final String userId) {
AsyncTask<String, Integer, String> task1 = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String... strings) {
String name = getUserName(userId); //獲取userName,須要用到userId
return name;
}
@Override
protected void onPostExecute(String name) {
textView.setText(name); //設置到TextView控件中
}
};
task1.execute(); //假設task1是一個耗時任務,去獲取userName
}
};
task.execute(); //假設task是一個耗時任務,去獲取userId
複製代碼
若是是使用協程,上面的例子能夠簡化爲:
GlobalScope.launch(Dispatchers.Main) {
val userId = getUserId() //耗時任務,這裏會切換到子線程
val userName = getUserName(userId) //耗時任務,這裏會切換到子線程
textView.text = userName //設置到TextView控件中,切換到主線程
}
suspend fun getUserId(): String = withContext(Dispatchers.IO) {
//耗時操做,返回userId
}
suspend fun getUserName(userId: String): String = withContext(Dispatchers.IO) {
//耗時操做,返回userName
}
複製代碼
上面launch
函數的{}的邏輯,就是一個協程。
相比於AsyncTask
的寫法,使用kotlin協程有如下好處:
AsyncTask
的回調嵌套,使用起來更加方便、簡潔。在模塊的build.gradle
中加入如下依賴:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
}
複製代碼
Kotlin提供了三種方式來建立協程,以下所示:
//方式一
runBlocking { //runBlocking是一個頂級函數
...
}
//方式二
GlobalScope.launch { //GlobalScope是一個單例對象,直接使用launch開啓協程
...
}
//方式三
val coroutineScope = CoroutineScope(context) //使用CoroutineContext建立CoroutineScope對象,經過launch開啓協程
coroutineScope.launch {
...
}
複製代碼
CoroutineContext
來建立一個CoroutineScope
對象,經過CoroutineScope.launch
或CoroutineScope.async
能夠開啓協程,經過CoroutineContext
也能夠控制協程的生命週期。在開發過程當中,通常推薦使用這種方式開啓協程。上面說到推薦使用CoroutineScope.launch
開啓協程,而不論是GlobalScope.launch
仍是CoroutineScope.launch
,launch
方法的第一個參數就是CoroutineContext
,源碼以下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
複製代碼
這裏的context
,即CoroutineContext
,它的其中一個做用是起到線程切換的功能,即協程體將運行在CoroutineContext
表徵的指定的線程中。
Kotlin協程官方定義了幾個值,可供咱們在開發過程當中使用,它們分別是:
協程體將運行在主線程,用於UI的更新等須要在主線程執行的場景,這個你們應該都清楚。
協程體將運行在IO線程,用於IO密集型操做,如網絡請求、文件操做等場景。
協程體將運行在默認的線程,context沒有指定或指定爲Dispatchers.Default,都屬於這種狀況。用於CPU密集型,如涉及到大量計算等場景。要特別注意的是,這裏的默認線程,其實和上面的IO線程共享同一個線程池。
不受限的調度器,在開發中不該該使用它,暫不研究。
看一下下面這個例子:
GlobalScope.launch(Dispatchers.Main) {
println("Main Dispatcher, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch {
println("Default Dispatcher1, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.IO) {
println("IO Dispatcher, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.Default) {
println("Default Dispatcher2, currentThread=${Thread.currentThread().name}")
}
複製代碼
程序運行結果以下:
能夠看到,Dispatchers.Main
調度器的協程運行在主線程,而無調度器、Dispatchers.IO
、Dispatchers.Default
調度器的協程運行在同一個線程池。
上面提到可使用launch
來建立一個協程,可是除了使用launch
以外,Kotlin還提供了async
來幫助咱們建立協程。二者的區別是:
Job
,可是並不攜帶協程執行後的結果。Deferred
(也是一個Job),並攜帶協程執行後的結果。async
返回的Deferred
是一個輕量級的非阻塞future,它表明的是一個將會在稍後提供結果的promise,因此它須要使用await
方法來獲得最終結果。拿Kotlin官方的一個例子對async
進行說明:
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假設咱們在這裏作了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假設咱們在這裏也作了些有用的事
return 29
}
複製代碼
執行上述代碼,獲得的結果是:
The answer is 42
複製代碼
在介紹《CoroutineContext》一節時,舉的例子中的協程仍是運行在單一線程中。在實際開發過程當中,常見的場景就是線程的切換與恢復,這須要用到withContext
方法了。
咱們繼續以《與線程的對比》這一節的例子來講明:
GlobalScope.launch(Dispatchers.Main) {
val userId = withContext(Dispatchers.IO) {
getUserId() //耗時任務,這裏會切換到子線程
}
textView.text = userId //設置到TextView控件中,切換到主線程
}
複製代碼
上面是一個典型的網絡請求場景:一開始運行在主線程,而後須要到後臺獲取userId
的值(這裏會執行getUserId
方法),獲取結束,結果返回後,會切換回主線程,最後更新UI控件。
在獲取userId
的時候,調用了getUserId
方法,這裏用到了withContext
方法,將線程從main
切換到了IO
線程,當耗時任務執行結束後(即上面的getUserId
方法執行完畢),withContext
的另一個做用是恢復到切換子線程前的所在線程,對應上面的例子是main
線程,因此咱們才能作更新UI控件的操做。
咱們也能夠將withContext
的邏輯單獨放到一個方法去管理,以下所示:
GlobalScope.launch(Dispatchers.Main) {
val userId = getUserIdAsync()
textView.text = userId //設置到TextView控件中,切換到主線程
}
fun getUserIdAsync() = withContext(Dispatchers.IO) {
getUserId() //耗時任務,這裏會切換到子線程
}
複製代碼
這樣看上去就像在使用同步調用的方式執行異步邏輯,可是若是按照上面的方式來寫,IDE會報錯的,提示信息是: Suspend function'withContext' should be called only from a coroutine or another suspend funcion
。
意思是withContext
是一個suspend
方法,它須要在協程或另一個suspend
方法中被調用。
suspend
是Kotlin協程的一個關鍵字,它表示 「掛起」 的意思。因此上面的報錯,只要加上suspend
關鍵字就能解決,即:
GlobalScope.launch(Dispatchers.Main) {
val userId = getUserIdAsync()
textView.text = userId //設置到TextView控件中,切換到主線程
}
suspend fun getUserIdAsync() = withContext(Dispatchers.IO) {
getUserId() //耗時任務,這裏會切換到子線程
}
複製代碼
當代碼執行到suspend
方法時,當前協程就會被掛起,這裏所說的掛起是非阻塞的,也就是說它不會阻塞當前所在的線程。這就是所謂的「非阻塞式掛起」。
非阻塞式掛起的一個前提是:涉及的必須是多線程的操做。由於阻塞的概念是針對單線程而言的。當咱們切換了線程,那確定是非阻塞的,由於耗時的操做跑到別的線程了,原來的線程就自由了,該幹嗎幹嗎唄~
若是在主線程中啓動多個協程,那麼協程的執行順序是怎樣的呢?是按照代碼順序執行麼?仍是有別的執行順序?以下代碼所示,假設test方法在主線程中執行,那麼這段代碼應該輸出什麼呢?
//假設test方法運行在主線程
fun test() {
println("start test fun, thread=${Thread.currentThread().name}")
//協程A
GlobalScope.launch(Dispatchers.Main) {
println("start coroutine1, thread=${Thread.currentThread().name}")
val userId = getUserIdAsync()
println("end coroutine1, thread=${Thread.currentThread().name}")
}
//協程B
GlobalScope.launch(Dispatchers.Main) {
println("start coroutine2, thread=${Thread.currentThread().name}")
delay(100)
println("end coroutine2, thread=${Thread.currentThread().name}")
}
println("end test fun, thread=${Thread.currentThread().name}")
}
suspend fun getUserIdAsync() = withContext(Dispatchers.IO) {
println("getUserIdAsync, thread=${Thread.currentThread().name}")
delay(1000)
return@withContext "userId from async"
}
複製代碼
在Android中運行上述代碼,執行結果是:
經過打印的日誌能夠看到,雖然協程的代碼順序在println("end test fun...")
以前,可是在執行順序上,協程的啓動仍然在println("end test fun...")
以後,結合非阻塞式掛起,下圖展現了協程的執行順序流程:
一、Kotlin 的協程用力瞥一眼 - 學不會協程?極可能由於你看過的教程都是錯的
二、協程入門指南