五分鐘技術趣談 | 試論Android異步框架Kotlin協程
Part 01
什么是協程
作為開發人員尤其是客戶端應用開發,我們一直面臨著需要解決的問題——如何防止我們的應用程序被阻塞。考慮下面一個異步應用場景。客戶端順序進行3次網絡請求,最后更新UI展示結果。
圖片
圖1 異步場景
有多種方法實現上述需求,主流的包括:
- 回調
- Rx(反應式擴展)
- 協程
1.1 回調方式
圖2 回調代碼示例
異步回調的方式雖然實現了需求,但是這種結構的代碼無論是閱讀還是維護起來都是極其糟糕的。這種回調函數的層層嵌套耦合,親切地稱為 "回調地獄"。
1.2 Rx方式
圖3 Rx代碼示例
Rx系列的鏈式調用,是在協程之前推薦的做法,RxJava豐富的操作符、簡便的線程調度、異常處理使得大多數人滿意。但是還有沒有更簡潔易讀的寫法呢?
1.3 協程方式
圖4 協程代碼示例
使用協程后的代碼非常簡潔,以順序的方式編寫異步代碼,不會阻塞當前UI線程,錯誤處理、線程切換也和平常代碼一樣簡單。
協程具有以下幾個特點:
- 輕量:您可以在單個線程上運行多個協程,因為協程支持掛起,掛起時不需要阻塞線程,幾乎是無代價的,協程是由開發者控制的。所以協程也像用戶態的線程,非常輕量級。
- 內存泄漏更少:使用結構化并發機制在一個作用域內執行多項操作。
- 內置取消支持:取消操作會自動在運行中的整個協程層次結構內傳播。
- Jetpack集成:許多Jetpack庫都提供全面協程支持的擴展。某些庫還提供自己的協程作用域,可用于結構化并發。
總而言之:協程可以簡化異步編程,可以順序地表達程序。協程使用掛起,這意味可以在代碼的特定點暫停和恢復執行,無需阻塞主線程或顯示創建額外的線程。
Part 02
協程的使用
- 引入gradle依賴
圖5 gradle依賴引入
- 啟動協程
圖6 啟動協程
上面就是啟動協程的代碼,啟動協程的代碼可以分為三部分:GlobalScope、launch、Dispatchers,它們分別對應:協程的作用域、構建器和調度器。
2.1 協程作用域
指的是協程內的代碼運行的時間周期范圍,如果超出了指定的協程范圍,協程會被取消執行。
官方庫給我們提供了一些作用域可以直接來使用:
- runBlocking
頂層函數,但是它會阻塞當前線程,主要用于測試。
- GlobalScope
全局協程作用域,它啟動的協程的生命周期只受整個應用程序的生命周期的限制,且不能取消,運行時會消耗一些內存資源,這可能會導致內存泄露,不適用于業務開發。
- coroutineScope
創建一個獨立的協程作用域,直到所有啟動的協程都完成后才結束自身。它是一個掛起函數,需要運行在協程內或掛起函數內,為并行分解工作而設計的。
- supervisorScope
與coroutineScope類似,不同的是子協程的異常不會影響父協程,也不會影響其他子協程。
- MainScope
為UI組件創建主作用域。一個頂層函數,上下文是SupervisorJob() + Dispatchers.Main,說明它是一個在主線程執行的協程作用域。推薦使用。
Android官方對協程的支持是非常友好的,KTX為Jetpack的Lifecycle相關組件提供了已經綁定UV聲明周期的作用域供我們直接使用:
- lifecycleScope
與Lifecycle綁定生命周期,生命周期被銷毀時,此作用域將被取消不會造成協程泄漏,推薦使用。
- viewModelScope
與lifecycleScope類似,與ViewModel綁定生命周期,當ViewModel被清除時,這個作用域將被取消,推薦使用。
2.2 調度器
調度器的作用是將協程限制在特定的線程執行。主要的調度器類型有:
- Dispatchers.Main:指定執行的線程是主線程
- Dispatchers.IO:指定執行的線程是IO線程
- Dispatchers.Default:默認的調度器,適合執行CPU密集性的任務
- Dispatchers.Unconfined:非限制的調度器,指定的線程可能會隨著掛起的函數的發生變化
2.3 構建器
kotlinx.continues庫提供的三個基本協程構建器:
- Launch
- async
- runBlocking
launch{}是最常用的協程構建器,不阻塞當前線程,在后臺創建一個新協程,也可以指定協程調度器。
async創建一個新的協程,不會阻塞當前線程,必須在協程作用域中才可以調用,并返回Deffer對象。可通過調用Deffer.await()方法等待該子協程執行完成并獲取結果。常用于并發執行-同步等待和獲取返回值的情況。
runBlocking是創建一個新的協程同時阻塞當前線程,直到協程結束,主要是為測試設計。
Part 03
協程掛起、恢復原理剖析
協程的概念最核心的點就是掛起,即函數或者某段程序可以在某個時刻暫停執行并稍后恢復。suspend是Kotlin協程最核心的關鍵字,使用suspend關鍵字修飾的函數叫作掛起函數,掛起函數只能在協程體內或者在其他掛起函數內調用。內部實現使用了Kotlin編譯器的一些編譯技術,被關鍵字suspend修飾的方法在編譯階段,編譯器會修改方法的簽名. 包括返回值,修飾符,入參,方法體實現。我們以下面一個簡單的掛起方法來剖析。
圖7 掛起函數
通過AS的工具欄中 Tools->Kotlin->show Kotlin ByteCode,得到java字節碼,再點擊Decompile按鈕反編譯成java源碼:
圖8 掛起函數反編譯java源碼
上面主要步驟為:
1??函數返回值變成Object,函數入參編譯后增加了Continuation參數。
2??創建一個ContinuationImpl ,復寫invokeSuspend()方法,在這個方法里面它又調用了一次自己,并且把continuation傳遞進去。
3??在switch狀態機中,label初始值為0,第一次會進入case 0分支,delay()是一個掛起函數,傳入上面的continuation參數,會有一個Object類型的返回值。
4??DelayKt.delay(2000, continuation)的返回結果如果是 COROUTINE_SUSPENDED,則直接return,那么方法執行就被結束了,方法就被掛起了。
這就是掛起的真正原理。協程的掛起本質上是方法的掛起,而方法的掛起本質上是return,協程的恢復本質上方法的恢復,而恢復的本質是callback回調。
Part 04
總結
異步編程是現代軟件開發的重要組成部分,它允許我們創建響應迅速、可擴展的應用程序。Kotlin協程是一款輕量級、高效、易于使用的并發框架,借助Kotlin的語言優勢,用同步的方式寫出異步的代碼,變得更加可維護和可讀,有助于改善開發體驗。在Android客戶端開發中,結合Jetpack可以更加輕松使用不阻塞UI線程同時避免內存泄露。