Kotlin协程简介(一)
目錄:
- 一. 協程的基本概念
- 二. 從異步編程開始
- 回調
- CompletableFuture
- RxJava
- 協程
- 三. 協程的基本概念
- suspend funtion
- CoroutineScope
- CoroutineContext
- CoroutineDispatcher
- Job 和 Deffered
- Coroutine builders
一. 協程的基本概念
協程就像非常輕量級的線程。線程是由系統調度的,線程切換或線程阻塞的開銷都比較大。而協程依賴于線程,但是協程掛起時不需要阻塞線程,幾乎是無代價的,協程是由開發者控制的。
二. 從異步編程開始
我們先從一個例子說起,發送一個帶有認證的 post 請求,需要以下三個步驟,首先客戶端向服務其發送一個得到token的請求,然后構造一個 Post 請求,最后將 Post 請求發出去。這三個請求都是耗時操作,而且請求和請求之間有著依賴的關系。 fun requestToken(): Token {delay(500L) // 模擬請求過程return token }fun createPost(token: Token, item: Item): Post {delay(500L) // 模擬構造過程return post }fun processPost(post: Post) {delay(500L) // 模擬請求過程 }方法一:使用回調的方式
操作2依賴于操作1,所以把操作2作為回調放在操作1的參數內,由操作1決定回調時機。
fun requestTokenAsync(cb: (Token) -> Unit) { ... } fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... } fun processPost(post: Post) { ... }fun postItem(item: Item) {requestTokenAsync { token ->createPostAsync(token, item) { post ->processPost(post)}} }這種多層嵌套的方式比較復雜,而且不方便處理異常情況。
方法二:CompletableFuture
Java 8 引入的 CompletableFuture 可以將多個任務串聯起來,可以避免多層嵌套的問題。
可以簡單看一下API,具體的使用方法參考文章:
CompletableFuture 使用詳解
| runAsync | 創建一個異步操作,不支持返回值 |
| supplyAsync | 創建一個異步操作,支持返回值 |
| whenComplete | 計算結果完成的回調方法 |
| exceptionally | 計算結果出現異常的回調方法 |
| thenApply | 當一個線程依賴另一個線程時,可以使用 thenApply 方法來把這兩個線程串行化。 |
| handle | 與thenApply相似,handle還可以處理異常任務 |
| thenAccept | 與thenApply相似,但是沒有返回值 |
| thenRun | 與thenAccept相似,但是得不到上面任務的處理結果 |
| thenCombine | 合并任務,有返回值 |
| thenAcceptBoth | 合并任務,無返回值 |
| applyToEither | 兩個任務用哪個結果 |
| acceptEither | 誰返回的結果快使用那個結果 |
| runAfterEither | 任何一個完成都會執行下一步操作 |
| runAfterBoth | 都完成了才會執行下一步操作 |
| thenCompose | 允許你對兩個 CompletionStage 進行流水線操作,第一個操作完成時,將其結果作為參數傳遞給第二個操作。 |
知道了API后就可以這么寫
fun requestTokenAsync(): CompletableFuture<Token> { ... } fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... } fun processPost(post: Post) { ... }fun postItem(item: Item) {requestTokenAsync().thenCompose { token -> createPostAsync(token, item) }.thenAccept { post -> processPost(post) }.exceptionally { e ->e.printStackTrace()null} }方法三: RxJava
RxJava的用法跟CompletableFuture鏈式調用比較類似,這也是比較簡潔,比較多人使用的方式:
fun requestToken(): Token { ... } fun createPost(token: Token, item: Item): Post { ... } fun processPost(post: Post) { ... }fun postItem(item: Item) {Single.fromCallable { requestToken() }.map { token -> createPost(token, item) }.subscribe({ post -> processPost(post) }, // onSuccess{ e -> e.printStackTrace() } // onError) }方法四:協程的方式
suspend fun requestToken(): Token { ... } // 掛起函數 suspend fun createPost(token: Token, item: Item): Post { ... } // 掛起函數 fun processPost(post: Post) { ... }fun postItem(item: Item) {GlobalScope.launch {val token = requestToken()val post = createPost(token, item)processPost(post)// 需要異常處理,直接加上 try/catch 語句即可} }協程可以讓我們使用順序的方式去寫異步代碼,而且并不會阻塞UI線程。
三. 協程的基本概念
1. suspend funtion
我們寫的有兩個方法是掛起的函數(suspend function)
suspend fun requestToken(): Token { ... } suspend fun createPost(token: Token, item: Item): Post { ... }首先要知道的是,掛起函數掛起協程的時候,并不會阻塞線程。
然后一個 suspend function 只能在一個協程或一個 suspend function 中使用,但是suspend function和普通函數使用方法一樣,有自己的參數,有自己的返回值,那么為什么要使用suspend funtion呢?
我們可以看到delay函數是一個掛起函數 , Thread.sleep()是一個阻塞函數,如果我們在一個A函數可能會掛起協程,比如調用delay()方法,因為 delay() 是suspend function ,只能在一個協程或一個suspend function中使用,所以A函數也必須是suspend function。所以使用suspend funtion的標準是該函數有無掛起操作。
public suspend fun delay(timeMillis: Long) {if (timeMillis <= 0) return // don't delayreturn suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)} }2. CoroutineScope
CoroutineScope為協程的作用域,可以管理其域內的所有協程。一個CoroutineScope可以有許多的子scope。
創建子scope的方式有許多種, 常見的方式有:
方法一:使用lauch, async 等builder創建一個新的子協程。
我們來看一下CoroutineScop接口
// 每個Coroutine作用域都有一個Coroutine上下文 public interface CoroutineScope {// Scope 的 Contextpublic val coroutineContext: CoroutineContext }所以 CoroutineScope 只是定義了一個新 Coroutine 的 coroutineContext,其實每個 coroutine builder(launch
,async) 都是 CoroutineScope 的擴展函數,并且自動的繼承了當前 Scope 的 coroutineContext 和取消操作。我們以launch方法為例:
-
第一個參數 context,默認 launch 所創建的 Coroutine 會自動繼承當前 Coroutine 的 context,如果有額外的 conetxt 需要傳遞給所創建的 Coroutine 則可以通過第一個參數來設置。
-
第二個參數 start 為 CoroutineStart 枚舉類型,用來指定 Coroutine 啟動的選項。有如下幾個取值:
- DEFAULT (默認值)立刻安排執行該Coroutine實例
- LAZY 延遲執行,只有當用到的時候才執行
- ATOMIC 類似 DEFAULT,區別是當Coroutine還沒有開始執行的時候無法取消
- UNDISPATCHED 如果使用 Dispatchers.Unconfined dispatcher,則立刻在當前線程執行直到遇到第一個suspension point。然后當 Coroutine 恢復的時候,在繼續在 suspension的 context 中設置的 CoroutineDispatcher 中執行。 -
第三個參數 block 為一個 suspending function,這個就是 Coroutine 中要執行的代碼塊,在實際使用過程中通常使用 lambda 表達式,也稱之為 Coroutine 代碼塊。需要注意的是,這個 block 函數定義為 CoroutineScope 的擴展函數,所以在代碼塊中可以直接訪問 CoroutineScope 對象(也就是 this 對象)
結論:launch方法實際上就是new了一個LazyStandaloneCoroutine協程(isLazy屬性為false),協程自動的繼承了當前 Scope(this代表的協程scope) 的 coroutineContext 和取消操作。
方法二:使用coroutineScope Api創建新scope:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R這個api主要用于方便地創建一個子域(相當于創建一個局部作用域),并且管理域中的所有子協程。注意這個方法只有在所有 block中創建的子協程全部執行完畢后,才會退出。
// print輸出的結果順序將會是 1, 2, 3, 4 coroutineScope {delay(1000)println("1")launch { delay(6000) println("3")}println("2")return@coroutineScope}println("4")方法三:繼承CoroutineScope.這也是比較推薦的做法,用于處理具有生命周期的對象。
在 Android 環境中,通常每個界面(Activity、Fragment 等)啟動的 Coroutine 只在該界面有意義,如果用戶在等待 Coroutine 執行的時候退出了這個界面,則再繼續執行這個 Coroutine 可能是沒必要的。那么我們怎么讓activity管理好其內的 Coroutine 呢?
我們來看下面的例子:
class ScopedActivity : Activity(), CoroutineScope {lateinit var job: Job// CoroutineScope 的實現override val coroutineContext: CoroutineContextget() = Dispatchers.Main + joboverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)job = Job()}override fun onDestroy() {super.onDestroy()// 當 Activity 銷毀的時候取消該 Scope 管理的 job。// 這樣在該 Scope 內創建的子 Coroutine 都會被自動的取消。job.cancel()}/** 注意 coroutine builder 的 scope, 如果 activity 被銷毀了或者該函數內創建的 Coroutine* 拋出異常了,則所有子 Coroutines 都會被自動取消。不需要手工去取消。*/fun loadDataFromUI() = launch { // <- 自動繼承當前 activity 的 scope context,所以在 UI 線程執行val ioData = async(Dispatchers.IO) { // <- launch scope 的擴展函數,指定了 IO dispatcher,所以在 IO 線程運行// 在這里執行阻塞的 I/O 耗時操作}// 和上面的并非 I/O 同時執行的其他操作val data = ioData.await() // 等待阻塞 I/O 操作的返回結果draw(data) // 在 UI 線程顯示執行的結果} }解釋一下這個地方:get() = Dispatchers.Main + job
一個上下文(context)可以是多個上下文的組合。組合的上下文需要是不同的類型。所以,你需要做兩件事情:
- 一個 dispatcher: 用于指定協程默認使用的 dispatcher;
- 一個 job: 用于在任何需要的時候取消協程;
操作符號 + 用于組合上下文。如果兩種不同類型的上下文相組合,會生成一個組合的上下文(CombinedContext),這個新的上下文會同時擁有被組合上下文的特性。因為:get() = Dispatchers.Main + job,所以launch方法實際上是在Dispatchers.Main,也就是在UI線程中執行的。
3. CoroutineContext
CoroutineScope 可以理解為一個協程,里面有一個協程的上下文:CoroutineContext,這個協程上下文包含很多該協程的信息,比如:Job, ContinuationInterceptor, CoroutineName 和CoroutineId。在CoroutineContext中,是用map來存這些信息的, map的鍵是這些類的伴生對象,值是這些類的一個實例,你可以這樣子取得context的信息:
val job = context[Job] val continuationInterceptor = context[ContinuationInterceptor]4. CoroutineDispatcher
CoroutineDispatcher,協程調度器,決定協程所在的線程或線程池。它可以指定協程運行于特定的一個線程、一個線程池或者不指定任何線程(這樣協程就會運行于當前線程)。coroutines-core中 CoroutineDispatcher 有四種標準實現Dispatchers.Default、Dispatchers. IO,Dispatchers.Main 和 Dispatchers.Unconfined,Unconfined 就是不指定線程。
- Dispatchers.Default: 如果創建 Coroutine 的時候沒有指定 dispatcher,則一般默認使用這個作為默認值。Default dispatcher 使用一個共享的后臺線程池來運行里面的任務。
- Dispatchers. IO: 顧名思義這是用來執行阻塞 IO 操作的,也是用一個共享的線程池來執行里面的任務。根據同時運行的任務數量,在需要的時候會創建額外的線程,當任務執行完畢后會釋放不需要的線程。通過系統 property kotlinx.coroutines.io.parallelism 可以配置最多可以創建多少線程,在 Android 環境中我們一般不需要做任何額外配置。
- Dispatchers.Unconfined: 立刻在啟動 Coroutine 的線程開始執行該 Coroutine直到遇到第一個 suspension point。也就是說,coroutine builder 函數在遇到第一個 suspension point 的時候才會返回。而 Coroutine 恢復的線程取決于 suspension function 所在的線程。 一般而言我們不使用 Unconfined。
- Dispatchers.Main: 是在 Android 的 UI 線程執行。
- 通過 newSingleThreadContext 和 newFixedThreadPoolContext 函數可以創建在私有的線程池中運行的 Dispatcher。由于創建線程比較消耗系統資源,所以對于臨時創建的線程池在使用完畢后需要通過 close 函數來關閉線程池并釋放資源。
5. Job 和 Deffered
CoroutineScope.launch 函數返回一個 Job 對象,該對象代表了這個剛剛創建的 Coroutine實例,job 對象有不同的狀態(剛創建的狀態、活躍的狀態、執行完畢的狀態、取消狀態等),通過這個 job 對象可以控制這個 Coroutine 實例,比如調用 cancel 函數可以取消執行。Job對象持有所有的子job實例,可以取消所有子job的運行。Job的join方法會等待自己以及所有子job的執行, 所以Job給予了CoroutineScope一個管理自己所有子協程的能力。
CoroutineScope.async 函數也是三個參數,參數類型和 launch 一樣,唯一的區別是第三個block參數會返回一個值,而 async 函數的返回值為 Deferred 類型。可以通過 Deferred 對象獲取異步代碼塊(block)返回的值。Deferred 繼承了 Job,它有個 await() 方法。
// Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete, // returning the resulting value or throwing the corresponding exception if the deferred was cancelled. public suspend fun await(): T6. Coroutine builders
創建一個新的協程來阻塞當前線程,直到 runBlocking 代碼塊執行完成。通常它不會用于協程中,因為在協程中寫一個阻塞的代碼塊實在太別扭,可以通過掛起操作取代。它通常作為一個適配器,將 main 線程轉換成一個 main 協程,我們也就持有了一個 main 協程的 coroutineContext 上下文對象,就可以隨心所欲用(this)使用 coroutineContext 的擴展方法,隨心所欲使用 suspend 方法 ( suspend 方法只能用于 suspend 方法和協程中)。所以 runBlocking 一般用在 test 函數和 main 函數中。
withContext 不會創建一個新的協程,在指定的協程上運行代碼塊,并掛起該協程直到代碼塊運行完成。通常是用于切換協程的上下文。
例如:
// 使用 withContext 切換協程,上面的例子就是先在 IO 線程里執行,然后切換到主線程。 GlobalScope.launch(Dispatchers.IO) {...withContext(Dispatchers.Main) {...} }總結
以上是生活随笔為你收集整理的Kotlin协程简介(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓学习 之 bitmap用法
- 下一篇: 协程的挂起、恢复和调度的原理 (二)