文章目錄
-
-
- 協程是什麼?
- 協程的特性
- 協程怎麼使用
- 開始使用協程
- 案例1:
- suspend
-
- 線程
- 協程
- suspend 的挂起
- suspend 的意義
- 什麼時候需要自定義 suspend 函數
-
- 挂起函數的類型
- 将回調寫為挂起函數
- 案例2
- 非阻塞式挂起
- 協程的具體使用
-
- delay
- runBlocking
- 等待一個任務
- 結構化的并發
-
協程是什麼?
其實就是一套由 Kotlin 官方提供的線程 API 。可以以非常優雅的方式來執行異步代碼。使用簡單,高效。
協程的特性
- 結構化并發:
val scope = CoroutineScope(Dispatchers.Main).launch {
launch {
launch {
}
}
launch {
}
}
scope.cacel()
協程中支援嵌套,并且具有子協程和父協程的概念
如上,在啟動了一個協程後,在内部有啟動了多個子協程,在子協程中還可以繼續啟動協程。傳回來看線程,線程中可以建立線程,但是他們之間是沒有關聯的
協程的這種特性被稱為結構化并發,這種方式可以讓協程非常友善的管理,比方說關閉協程,隻需要關閉最外面的協程後,内部的協程都會被關閉。對于子協程也可以擷取他的傳回值并調用 cacel 進行關閉。
- 協程的取消
- 協稱的異常處理
協程怎麼使用
在項目中配置對 Kotlin 的協程的支援
//核心庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1"
//依賴目前平台對應的庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1"
Kotlin 協程是以官方擴充庫的形式進行支援的。而且 核心庫 和 平台庫的版本應該保持一緻。
- 核心庫中包含的代碼主要是協程的公共 API 部分,有了這一層的代碼,才使得協程在各個平台上的接口得到統一
- 平台庫中包含的代碼主要是協程架構在具體平台的具體實作方式。因為多線程在每個平台上都是有差異的。
開始使用協程
//launch 函數的含義:我要建立一個新的協程。并且在指定的線程上運作它
CoroutineScope(Dispatchers.IO).launch {
println(Thread.currentThread().name)
}
可嵌套使用
CoroutineScope.launch(Dispatchers.IO) {
val image = getImage(imageId)
launch(Dispatch.Main) { //将會運作在 Main 線程
avatarIv.setImageBitmap(image)
}
}
如果隻是嵌套,這并沒有多少作用。協程有一個非常好用的函數 :withContext。這個函數可以切換到指定線程,并在閉包中的邏輯執行完後自動把線程切回去繼續執行
CoroutineScope(Dispatchers.Main).launch {
println(Thread.currentThread().name)
val bitmap = withContext(Dispatchers.IO) { //切換到 IO 線程
getImage()
}
iv.setImageBitmap(bitmap) //主線程更新
}
由于可以自動切回來,我們甚至可以把 withContext 放進一個單獨的函數裡面,如下:
suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
//.....
}
但是要注意 suspend 關鍵字。這個關鍵字後面在說,他中文意思是 暫停 或者 可挂起。
案例1:
通過協程下載下傳一張網絡圖檔,并且顯示出來
override fun bindView(view: View) {
val iv = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv)
btn = view.findViewById(R.id.delegate_shop_btn)
btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val bitmap = getImage()
iv.setImageBitmap(bitmap)
}
}
}
private suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
OkHttpClient().newCall(Request.Builder() .url("https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193")
.get()
.build())
.execute().body()?.byteStream().use {
BitmapFactory.decodeStream(it)
}
}
suspend
協程指的就是 launch 中的代碼,那麼協程中的挂起是什麼呢? 其實挂起的對象就是協程
當執行在 launch 中時,在執行到某一個 suspend 函數時,這個協程就會被挂起。讓
時間靜止,兵分兩路,來看一下到底是怎麼回事,這兩路分别是協程和線程(UI線程)
線程
當代碼執行到協程中的 suspend 函數後,就暫時不會執行協程代碼,而是跳出協程的代碼塊。繼續向下執行。
CoroutineScope(Dispatchers.Main).launch {
val bitmap = getImage() //挂起
iv.setImageBitmap(bitmap)
}
ToastUtils.show("哈哈哈哈")
當主線程執行到 getImage 時,會跳出協程,執行下面的 Toast。
這個協程本質上會往主線程 post 一個 Runnable。然後繼續執行協程内部代碼。當執行到被挂起的時候,Runnable 會提前結束,線程繼續執行其他的東西。而協程則會被挂起。是以接下來看一下協程
協程
主線程在執行到 suspend 的時候會被掐斷,接下來協程會繼續往下執行。不過是執行在指定的線程。通過 withContext 傳入的 Dispatchers.IO 所指定的 IO 線程
Dispatchers 排程器:将協程限制在一個特定的線程執行,或者将他分派到一個線程池。
日常使用的排程器:Main:Android主線程,IO:網絡IO,Default:适合CPU 密集的任務,比如計算。
協程從 suspend 開始執行在指定的線程,執行完之後,就會自動将我們把線程切回來。
切回來就是切換到原本的線程,如原本是運作在主線程的,切回來後就會繼續在主線程執行。也就是說協程會幫我們 post 一個 Runnable 到主線程。
通過上面兩個角度,可以得到一個解釋:協程在執行到有 suspend 的時候就會被挂起,而這個挂起,則就是切個線程;隻不過挂起的執行完後會重新切回他原來的線程
這個切回來的動作,在 Kotlin 中叫做 resume ,恢複
suspend 的挂起
挂起函數,就是以 suspend 修飾的函數,挂起函數隻能在 其他挂起函數或者協程中使用
這是一個關鍵字。但是他并不是正真的挂起。你可以寫一個帶 supend 的函數,運作後就會發現并沒有挂起,為啥沒有被挂起,應為它不知道往哪切,需要我們來告訴他。如下:
suspend fun get() = withContext(Dispatchers.IO) {
}
withContext 本身就是一個挂起函數,接收一個
Dispatcher
參數,他必須依賴于這個參數,才能知道協程需要被挂起。接着才會切換到别的線程
是以 suspend 起不到任何挂起函數的作用,挂起函數是 kt 的協程幫我們做的
suspend 的意義
為啥 suspend 關鍵字沒有實際的挂起,但 Kotlin 為啥要把它提供出來?
因為他本來就不是用來操作挂起的。也就是說切線程依賴的是函數中的代碼,而不是這個關鍵字。是以這個關鍵字隻是用來提醒。
如果你建立一個 suspend 函數,但是内部不包含正真的挂起邏輯,編譯器會給你提醒: Redundant ‘suspend’ modifier ,這個關鍵字是多餘的
因為這個函數并不會發生挂起,那這個 suspend 隻有一個效果:限制此函數隻能在協程中被調用,如果在非協程中調用,則編譯不會通過
是以,建立一個 suspend 函數,為了讓他包含挂起,要在内部直接或者間接調用 Kotlin 自帶的 suspend 函數,這個時候函數才是有意義的
什麼時候需要自定義 suspend 函數
如果你某個函數比較耗時,也就是需要等,就可以把它寫成 suspend 函數
給函數加上 suspend 關鍵字,然後在 withContext 把函數内容包住就行了。當然并不是隻有 withContext 來輔助我們實作自定義的函數,如 delay ,他的作用是等一段時間後在繼續往下執行代碼。
suspend fun get() {
delay(5) //挂起
}
挂起函數的類型
suspend fun foo(){}
如果沒有 suspend 這個函數的類型就是 ()-> Unit
但是加了 suspend 後,函數的類型為 suspend()-> Unit
隻要你知道函數的類型是什麼,然後在前面加一個 suspend 就是挂起函數的類型
suspend fun bar(a:Int):String{
return "Hello"
}
挂起函數 bar 的類型為 suspend(Int)->String
還記得 suspend 為什麼隻能在 挂起函數或者協程中調用嗎?
因為所有的挂起函數都有一個 Continuation 參數,Continuation 是從哪來的呢,suspend 關鍵字會隐含在 函數的參數清單的最後加一個 Continuation 參數。Continuation 的泛型參數是由函數的傳回值來決定的

上面的 挂起函數 bar 最終的樣子如上圖,他會在參數清單的最後加一個參數,并且傳回值成為了 Any。這個 Any 有兩種情況,如果這個函數沒有真正的挂起,比如 bar函數,foo 函數。在函數沒有真正挂起的時候,這個 Any就是用來承載傳回值結果。如果函數真的被挂起了,這個Any 傳回的是一個挂起的标志 COROUTINE_SUSPENDED ,讓外部的協程體知道我這個協程真正被挂起了。要等待這個函數的回調,才能繼續往下執行。 是以這個 Any 是非常重要的
将回調寫為挂起函數
通過 suspendCoroutine 來實作:
private suspend fun getImage() = suspendCoroutine<Bitmap> {
continuation ->
OkHttpClient().newCall(Request.Builder()
.url("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581869591580&di=e0412feb1e101a144e416f7a873bd88d&imgtype=0&src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F6febb183087736d089b6583a790c491f2dc7469a.jpg")
.get()
.build())
.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
continuation.resume(response.body()?.byteStream().use { BitmapFactory.decodeStream(it)})
}
})
}
如果要将回調轉為挂起,需要使用 suspendCoroutine 這個函數調用擷取目前函數的 Continuation。通過這個方法就可以拿得到 Continuation。這個參數被編譯器藏了起來。
回調成功可以使用 resume 或者 resumeWith 将結果傳回
異常就是用 resumeWithException 即可。
案例2
網絡請求一張圖檔,并進行兩次切割 。1,切成四份,取第一份。2,切9份,去最後一份
override fun bindView(view: View) {
// getSupportDelegate().loadRootFragment(R.id.delegate_shop_layout,
// BaseShopListDelegate.newInstance(CarPreference.getMyCar(), BusinessScope.BUSINESSSCOPE_SHOP_LIST));
val iv1 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv1)
val iv2 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv2)
val iv3 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv3)
btn = view.findViewById(R.id.delegate_shop_btn)
btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val bitmap = getImage()
iv1.setImageBitmap(bitmap)//原圖
val bm1 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 2, bitmap.height / 2)
iv2.setImageBitmap(bm1)
val bm2 = Bitmap.createBitmap(bitmap, bitmap.width / 3 * 2, bitmap.height / 3 * 2, bitmap.width / 3, bitmap.height / 3)
iv3.setImageBitmap(bm2)
}
}
}
private suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
OkHttpClient().newCall(Request.Builder()
.url("https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193")
.get()
.build())
.execute().body()?.byteStream().use {
BitmapFactory.decodeStream(it)
}
}
非阻塞式挂起
首先什麼是阻塞呢?
1,前面有障礙物,過不去了(線程卡主)
2,清除障礙物(等待耗時任務結束)
3,繞道而行(切到别的線程)
非阻塞式挂起并沒有限定在一個線程中,因為挂起本來就涉及到多個線程。主線程執行的時候遇到耗時任務,然後将耗時任務挂起,這時主線程就自由了,可以繼續做别的事了。是以非阻塞式挂起其實就是在講 協程在挂起的時候切換線程這件事。
協程隻是看起來會阻塞,但其實是非阻塞的,因為它可以切線程
協程與線程:在 Kotlin 中,協程就是基于線程實作的一種更上層的工具 API ,隻不過他的用法非常簡單。
協程是什麼:基于線程的一個架構
協程的挂起:自動切換線程
非阻塞式挂起:可以用看起來阻塞的代碼來實作非阻塞的操作
協程的具體使用
delay
fun main() {
GlobalScope.launch {
delay(1000L) //協程挂起,阻塞1秒
println(" 2020")
}
print("hello") //協程挂起時,主線程繼續執行
Thread.sleep(2000L)//延時:保證 主線程存活
}
//hello 2020
runBlocking
在講建立協程的時候說過,runBlocking 是阻塞式的
fun main() {
GlobalScope.launch {
delay(1000L)
println("2020 ")
}
println("Hello ")
//表達式阻塞了主線程,調用了 runBlocking 的主線程會一直等到 runBlocking執行完畢
runBlocking {
delay(2000L) //延時:保證 主線程存活
}
}
//Hello 2020
修改代碼如下:
fun main() = runBlocking {//開始執行主協程
println(Thread.currentThread().name)
GlobalScope.launch {
delay(1000L)
println("2020 ")
}
print("Hello ")
delay(2000L) //延時:保證 主線程存活
}
//main
//Hello 2020
其實還是主線程
等待一個任務
suspend fun main() {
val job = GlobalScope.launch {
delay(1000L)
println(" 2020")
}
println("Hello")
job.join() //等待子線程執行結束
}
注意 main 方法被 suspend 修飾了,因為 join 方法被 suspend 修飾過,suspend 本身不會挂起,挂起是因為 join 内部有挂起的代碼。suspend 隻是一個提示。隻是這個提示必須寫。
結構化的并發
fun main() = runBlocking {
//開始執行主協程
launch {
delay(1000L)
println(" 2020")
}
print("Hello")
}
//Hello 2020
注意:在最後并沒有讓主線程等待,也沒有調用 join。為啥會列印出 2020 呢?
在 runBlocking 内的每個協程建構器中都将 CoruntineScope 的執行個體添加到代碼塊的作用域中。我們可以在這個作用域中啟動協程而無需顯示調用 join。因為外部協程(runBlocking)直到在其作用域中啟動的所有協程執行完畢後才會結束。
參考自慕課網視訊
參考自扔物線
參考自官網