關鍵詞:Kotlin 協程 Android Ankojava
Android 上面使用協程來替代回調或者 RxJava 其實是一件很是輕鬆的事兒,咱們甚至能夠在更大的範圍內結合 UI 的生命週期作控制協程的執行狀態~android
本文涉及的 MainScope 以及 AutoDispose 源碼:kotlin-coroutines-androidgit
咱們曾經提到過,若是在 Android 上作開發,那麼咱們須要引入github
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version'
複製代碼
這個框架裏面包含了 Android 專屬的 Dispatcher
,咱們能夠經過 Dispatchers.Main
來拿到這個實例;也包含了 MainScope
,用於與 Android 做用域相結合。api
Anko 也提供了一些比較方便的方法,例如 onClick
等等,若是須要,也能夠引入它的依賴:安全
//提供 onClick 相似的便捷的 listener,接收 suspend Lambda 表達式
implementation "org.jetbrains.anko:anko-sdk27-coroutines:$anko_version"
//提供 bg 、asReference,還沒有沒有跟進 kotlin 1.3 的正式版協程,不過代碼比較簡單,若是須要能夠本身改造
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
複製代碼
簡單來講:bash
協程的原理和用法咱們已經探討了不少了,關於 Android 上面的協程使用,咱們就只給出幾點實踐的建議。app
Android 開發常常想到的一點就是讓發出去的請求可以在當前 UI 或者 Activity 退出或者銷燬的時候可以自動取消,咱們在用 RxJava 的時候也有過各類各樣的方案來解決這個問題。框架
協程有一個很自然的特性能剛夠支持這一點,那就是做用域。官方也提供了 MainScope
這個函數,咱們具體看下它的使用方法:異步
val mainScope = MainScope()
launchButton.setOnClickListener {
mainScope.launch {
log(1)
textView.text = async(Dispatchers.IO) {
log(2)
delay(1000)
log(3)
"Hello1111"
}.await()
log(4)
}
}
複製代碼
咱們發現它其實與其餘的 CoroutineScope
用起來沒什麼不同的地方,經過同一個叫 mainScope
的實例啓動的協程,都會遵循它的做用域定義,那麼 MainScope
的定義時怎樣的呢?
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
複製代碼
原來就是 SupervisorJob
整合了 Dispatchers.Main
而已,它的異常傳播是自上而下的,這一點與 supervisorScope
的行爲一致,此外,做用域內的調度是基於 Android 主線程的調度器的,所以做用域內除非明確聲明調度器,協程體都調度在主線程執行。所以上述示例的運行結果以下:
2019-04-29 06:51:00.657 D: [main] 1
2019-04-29 06:51:00.659 D: [DefaultDispatcher-worker-1] 2
2019-04-29 06:51:01.662 D: [DefaultDispatcher-worker-2] 3
2019-04-29 06:51:01.664 D: [main] 4
複製代碼
若是咱們在觸發前面的操做以後當即在其餘位置觸發做用域的取消,那麼該做用域內的協程將再也不繼續執行:
val mainScope = MainScope()
launchButton.setOnClickListener {
mainScope.launch {
...
}
}
cancelButton.setOnClickListener {
mainScope.cancel()
log("MainScope is cancelled.")
}
複製代碼
若是咱們快速依次點擊上面的兩個按鈕,結果就顯而易見了:
2019-04-29 07:12:20.625 D: [main] 1
2019-04-29 07:12:20.629 D: [DefaultDispatcher-worker-2] 2
2019-04-29 07:12:21.046 D: [main] MainScope is cancelled.
複製代碼
儘管咱們前面體驗了 MainScope
發現它能夠很方便的控制全部它範圍內的協程的取消,以及可以無縫將異步任務切回主線程,這都是咱們想要的特性,不過寫法上仍是不夠美觀。
官方推薦咱們定義一個抽象的 Activity
,例如:
abstract class ScopedActivity: Activity(), CoroutineScope by MainScope(){
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
複製代碼
這樣在 Activity
退出的時候,對應的做用域就會被取消,全部在該 Activity
中發起的請求都會被取消掉。使用時,只須要繼承這個抽象類便可:
class CoroutineActivity : ScopedActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine)
launchButton.setOnClickListener {
launch { // 直接調用 ScopedActivity 也就是 MainScope 的方法
...
}
}
}
suspend fun anotherOps() = coroutineScope {
...
}
}
複製代碼
除了在當前 Activity
內部得到 MainScope
的能力外,還能夠將這個 Scope 實例傳遞給其餘須要的模塊,例如 Presenter
一般也須要與 Activity
保持一樣的生命週期,所以必要時也能夠將該做用域傳遞過去:
class CoroutinePresenter(private val scope: CoroutineScope): CoroutineScope by scope{
fun getUserData(){
launch { ... }
}
}
複製代碼
多數狀況下,Presenter
的方法也會被 Activity
直接調用,所以也能夠將 Presenter
的方法生命成 suspend
方法,而後用 coroutineScope
嵌套做用域,這樣 MainScope
被取消後,嵌套的子做用域同樣也會被取消,進而達到取消所有子協程的目的:
class CoroutinePresenter {
suspend fun getUserData() = coroutineScope {
launch { ... }
}
}
複製代碼
抽象類不少時候會打破咱們的繼承體系,這對於開發體驗的傷害仍是很大的,所以咱們是否是能夠考慮構造一個接口,只要 Activity
實現這個接口就能夠擁有做用域以及自動取消的能力呢?
首先咱們定義一個接口:
interface ScopedActivity {
val scope: CoroutineScope
}
複製代碼
咱們有一個樸實的願望就是但願實現這個接口就能夠自動得到做用域,不過問題來了,這個 scope
成員要怎麼實現呢?留給接口實現方的話顯然不是很理想,本身實現吧,又礙於本身是個接口,所以咱們只能這樣處理:
interface MainScoped {
companion object {
internal val scopeMap = IdentityHashMap<MainScoped, MainScope>()
}
val mainScope: CoroutineScope
get() = scopeMap[this as Activity]!!
}
複製代碼
接下來的事情就是在合適的實際去建立和取消對應的做用域了,咱們接着定義兩個方法:
interface MainScoped {
...
fun createScope(){
//或者改成 lazy 實現,即用到時再建立
val activity = this as Activity
scopeMap[activity] ?: MainScope().also { scopeMap[activity] = it }
}
fun destroyScope(){
scopeMap.remove(this as Activity)?.cancel()
}
}
複製代碼
由於咱們須要 Activity
去實現這個接口,所以直接強轉便可,固然若是考慮健壯性,能夠作一些異常處理,這裏做爲示例僅提供核心實現。
接下來就是考慮在哪兒完成建立和取消呢?顯然這件事兒用 Application.ActivityLifecycleCallbacks
最合適不過了:
class ActivityLifecycleCallbackImpl: Application.ActivityLifecycleCallbacks {
...
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
(activity as? MainScoped)?.createScope()
}
override fun onActivityDestroyed(activity: Activity) {
(activity as? MainScoped)?.destroyScope()
}
}
複製代碼
剩下的就是在 Application
裏面註冊一下這個監聽了,這個你們都會,我就不給出代碼了。
咱們看下如何使用:
class CoroutineActivity : Activity(), MainScoped {
override fun onCreate(savedInstanceState: Bundle?) {
...
launchButton.setOnClickListener {
scope.launch {
...
}
}
}
}
複製代碼
咱們也能夠增長一些有用的方法來簡化這個操做:
interface MainScoped {
...
fun <T> withScope(block: CoroutineScope.() -> T) = with(scope, block)
}
複製代碼
這樣在 Activity
當中還能夠這樣寫:
withScope {
launch { ... }
}
複製代碼
注意,示例當中用到了
IdentityHashMap
,這代表對於 scope 的讀寫是非線程安全的,所以不要在其餘線程試圖去獲取它的值,除非你引入第三方或者本身實現一個IdentityConcurrentHashMap
,即使如此,從設計上scope
也不太應該在其餘線程訪問。
按照這個思路,我提供了一套更加完善的方案,不只支持 Activity
還支持 support-fragment 版本在 25.1.0 以上的版本的 Fragment
,而且相似於 Anko 提供了一些有用的基於 MainScope
的 listener 擴展,引入這個框架便可使用:
api 'com.bennyhuo.kotlin:coroutines-android-mainscope:1.0'
複製代碼
咱們以前作例子常用 GlobalScope
,但 GlobalScope
不會繼承外部做用域,所以你們使用時必定要注意,若是在使用了綁定生命週期的 MainScope
以後,內部再使用 GlobalScope
啓動協程,意味着 MainScope
就不會起到應有的做用。
這裏須要當心的是若是使用了一些沒有依賴做用域的構造器,那麼必定要當心。例如 Anko 當中的 onClick
擴展:
fun View.onClick( context: CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}
}
}
複製代碼
也許咱們也就是圖個方便,畢竟 onClick
寫起來可比 setOnClickListener
要少不少字符,同時名稱上看也更加有事件機制的味道,但隱藏的風險就是經過 onClick
啓動的協程並不會隨着 Activity
的銷燬而被取消,其中的風險須要本身思考清楚。
固然,Anko 會這麼作的根本緣由在於 OnClickListener
根本拿不到有生命週期加持的做用域。不用 GlobalScope
就沒法啓動協程,怎麼辦?結合咱們前面給出的例子,其實這個事兒徹底有別的解法:
interface MainScoped {
...
fun View.onClickSuspend(handler: suspend CoroutineScope.(v: View) -> Unit) {
setOnClickListener { v ->
scope.launch { handler(v) }
}
}
}
複製代碼
咱們在前面定義的 MainScoped
接口中,能夠經過 scope
拿到有生命週期加持的 MainScope
實例,那麼直接用它啓動協程來運行 OnClickListener
問題不就解決了嘛。因此這裏的關鍵點在於如何拿到做用域。
這樣的 listener 我已經爲你們在框架中定義好啦,請參見 2.3。
固然除了直接使用一個合適的做用域來啓動協程以外,咱們還有別的辦法來確保協程及時被取消。
你們必定用過 RxJava,也必定知道用 RxJava 發了個任務,任務還沒結束頁面就被關閉了,若是任務遲遲不回來,頁面就會被泄露;若是任務後面回來了,執行回調更新 UI 的時候也會大機率空指針。
所以你們必定會用到 Uber 的開源框架 AutoDispose。它其實就是利用 View
的 OnAttachStateChangeListener
,當 View
被拿下的時候,咱們就取消全部以前用 RxJava 發出去的請求。
static final class Listener extends MainThreadDisposable implements View.OnAttachStateChangeListener {
private final View view;
private final CompletableObserver observer;
Listener(View view, CompletableObserver observer) {
this.view = view;
this.observer = observer;
}
@Override public void onViewAttachedToWindow(View v) { }
@Override public void onViewDetachedFromWindow(View v) {
if (!isDisposed()) {
//看到沒看到沒看到沒?
observer.onComplete();
}
}
@Override protected void onDispose() {
view.removeOnAttachStateChangeListener(this);
}
}
複製代碼
考慮到前面提到的 Anko 擴展 onClick
沒法取消協程的問題,咱們也能夠搞一個 onClickAutoDisposable
。
fun View.onClickAutoDisposable ( context: CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View) -> Unit
) {
setOnClickListener { v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}.asAutoDisposable(v)
}
}
複製代碼
咱們知道 launch
會啓動一個 Job
,所以咱們能夠經過 asAutoDisposable
來將其轉換成支持自動取消的類型:
fun Job.asAutoDisposable(view: View) = AutoDisposableJob(view, this)
複製代碼
那麼 AutoDisposableJob
的實現只要參考 AutoDisposable 的實現依樣畫葫蘆就行了 :
class AutoDisposableJob(private val view: View, private val wrapped: Job)
//咱們實現了 Job 這個接口,但沒有直接實現它的方法,而是用 wrapped 這個成員去代理這個接口
: Job by wrapped, OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) = Unit
override fun onViewDetachedFromWindow(v: View?) {
//當 View 被移除的時候,取消協程
cancel()
view.removeOnAttachStateChangeListener(this)
}
private fun isViewAttached() =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null
init {
if(isViewAttached()) {
view.addOnAttachStateChangeListener(this)
} else {
cancel()
}
//協程執行完畢時要及時移除 listener 省得形成泄露
invokeOnCompletion() {
view.removeOnAttachStateChangeListener(this)
}
}
}
複製代碼
這樣的話,咱們就可使用這個擴展了:
button.onClickAutoDisposable{
try {
val req = Request()
val resp = async { sendRequest(req) }.await()
updateUI(resp)
} catch (e: Exception) {
e.printStackTrace()
}
}
複製代碼
當 button
這個對象從 window 上撤下來的時候,咱們的協程就會收到 cancel 的指令,儘管這種狀況下協程的執行不會跟隨 Activity
的 onDestroy
而取消,但它與 View
的點擊事件緊密結合,即使 Activity
沒有被銷燬,View
自己被移除時也會直接將監聽中的協程取消掉。
若是你們想要用這個擴展,我已經幫你們放到 jcenter 啦,直接使用:
api "com.bennyhuo.kotlin:coroutines-android-autodisposable:1.0"
複製代碼
添加到依賴當中便可使用。
在 Android 上使用協程,更多的就是簡化異步邏輯的寫法,使用場景更多與 RxJava 相似。在使用 RxJava 的時候,我就發現有很多開發者僅僅用到了它的切線程的功能,並且因爲自己 RxJava 切線程 API 簡單易用,還會形成不少無腦線程切換的操做,這樣其實是很差的。那麼使用協程就更要注意這個問題了,由於協程切換線程的方式被 RxJava 更簡潔,更透明,原本這是好事情,就怕被濫用。
比較推薦的寫法是,絕大多數 UI 邏輯在 UI 線程中處理,即便在 UI 中用 Dispatchers.Main
來啓動協程,若是涉及到一些 io 操做,使用 async
將其調度到 Dispatchers.IO
上,結果返回時協程會幫咱們切回到主線程——這很是相似 Nodejs 這樣的單線程的工做模式。
對於一些 UI 不相關的邏輯,例如批量離線數據下載任務,一般默認的調度器就足夠使用了。
這一篇文章,主要是基於咱們前面講了的理論知識,進一步往 Android 的具體實戰角度遷移,相比其餘類型的應用,Android 做爲 UI 程序最大的特色就是異步要協調好 UI 的生命週期,協程也不例外。一旦咱們把協程的做用域規則以及協程與 UI 生命週期的關係熟稔於心,那麼相信你們使用協程時必定會駕輕就熟的。
歡迎關注 Kotlin 中文社區!
中文官網:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公衆號:Kotlin
知乎專欄:Kotlin
CSDN:Kotlin中文社區
掘金:Kotlin中文社區
簡書:Kotlin中文社區