天天看點

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

前言

協程系列文章:

  • 一個小故事講明白程序、線程、Kotlin 協程到底啥關系?
  • 少年,你可知 Kotlin 協程最初的樣子?
  • 講真,Kotlin 協程的挂起/恢複沒那麼神秘(故事篇)
  • 講真,Kotlin 協程的挂起/恢複沒那麼神秘(原理篇)
  • Kotlin 協程排程切換線程是時候解開真相了
  • Kotlin 協程之線程池探索之旅(與Java線程池PK)
  • Kotlin 協程之取消與異常處理探索之旅(上)
  • Kotlin 協程之取消與異常處理探索之旅(下)
  • 來,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理&使用
  • 繼續來,同我一起撸Kotlin Channel 深水區
  • Kotlin 協程 Select:看我如何多路複用
  • Kotlin Sequence 是時候派上用場了
  • Kotlin Flow 背壓和線程切換竟然如此相似
  • Kotlin Flow啊,你将流向何方?
  • Kotlin SharedFlow&StateFlow 熱流到底有多熱?

原本上篇已經結束協程系列了,後面有小夥伴建議可以再講講實際的使用,感覺停不下來了,再用幾篇收尾吧。我們知道Android開發繞不開的一個重要課題即是生命周期 ,引入了協程後兩者該怎麼配合呢?

通過本篇文章,你将了解到:

  1. 生命周期的前世今生
  2. Activity與協程的結合
  3. ViewModel與協程的配合
  4. Application建立全局的協程作用域
  5. Flow、協程、生命周期的三角關系

1. 生命周期的前世今生

生命周期簡述

現在的系統設計更聚焦于UI和資料的分離,目前的UI展示需要哪些資料的支援,在什麼時候需要展示這些資料,這些都需要開發者自己去控制。若控制不得當,可能會出現記憶體洩漏、資源浪費等現象。

Android提供了四大元件,其中Activity是用來展示UI的,它的建立到銷毀即是它的一個完整生命周期,四大元件中我們比較關注Activity和Service的生命周期,尤其是Activity是重中之重,而Fragment的生命周期依賴于Activity,是以隻要弄懂了Activity的生命周期,其它不在話下。

Activity 生命周期關注點

Activity記憶體洩漏

以典型的背景擷取資料,Toast到UI上為例:

binding.btnStartLifecycle.setOnClickListener {
            thread {
                //模拟網絡擷取資料
                Thread.sleep(5000)
                runOnUiThread {
                    //線程持有Activity執行個體
                    Toast.makeText(this@ThirdActivity, "hello world", Toast.LENGTH_SHORT).show()
                }
            }
        }
           

背景開啟線程,模拟網絡請求,等待5s後彈出Toast。

正常場景下沒問題,若此時還未彈出Toast就退出Activity,會發生什麼呢?

顯而易見,當然會記憶體洩漏,因為Activity執行個體被線程持有,無法回收,Activity洩漏了。

資源浪費

以背景擷取資料,展示到Activity上為例:

binding.btnStartGetInfo.setOnClickListener {
            thread {
                //模拟擷取資料
                var count = 0
                while (true) {
                    Thread.sleep(2000)
                    runOnUiThread {
                        binding.count.text = "計算值:${count++}"
                        println("${binding.count.text}")
                    }
                }
            }
        }
           

背景開啟線程,模拟網絡請求,等待5s後更新TextView。

正常場景下沒問題,若此時回到桌面或是切換到其它App,我們是不需要更新UI,也就不需要擷取網絡資料,此種情況下就會存在資源浪費,應當避免這種寫法。

存在以上兩種現象是因為在實作功能的過程中沒有注意Activity的生命周期,簡而言之,我們關注Activity生命周期就是為了解決兩類問題:

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

解決方法也很簡單,不管是Activity退出還是回到背景都會有各個階段生命周期的回調。是以,隻要監聽了Activity周期,在對應的地方進行防護就可以解決上述問題。

詳情請移步:Android Activity 生命周期詳解及監聽

2. Activity與協程的結合

沒有關聯生命周期的協程的使用

先看Demo:

val scope = CoroutineScope(Job())
        binding.btnStartUnlifecyleCoroutine.setOnClickListener {
            scope.launch {
                delay(5000)
                scope.launch(Dispatchers.Main) {
                    Toast.makeText(this@ThirdActivity, "協程還在運作中", Toast.LENGTH_SHORT).show()
                }
            }
        }
           

如上,構造了協程作用域,通過它啟動協程,5s後在背景列印。

當點選該按鈕後,我們退出Activity,最後發現Toast還會出現,說明發生了洩漏。

關聯生命周期的協程的使用

解決洩漏

協程的出現簡化了我們的程式設計結構,然而隻要和Activity産生瓜葛都避免不了要關注它的生命周期。

還好,協程内部主動關聯了生命周期,不用開發者去手動處理,來看看怎麼使用的。

binding.btnStartWithlifecyleCoroutine.setOnClickListener {
            lifecycleScope.launch {
                delay(5000)
                lifecycleScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@ThirdActivity, "協程還在運作中", Toast.LENGTH_SHORT).show()
                }
                //假設有網絡請求
                println("協程還在運作中")
            }
        }
           

與上個demo不同的是協程作用域的選擇,這次用的是lifecycleScope,它是LifecycleOwner的擴充屬性。

點選按鈕後,退出Activity,此時看不到Toast,也看不到列印,說明協程作用域檢測到Activity退出後将自己銷毀了,也就不會引用Activity執行個體,當然就解決了記憶體洩漏問題。

避免資源浪費

細心的你可能發現了:若此時點選按鈕後回到桌面,發現列印還在繼續,實際上為了節約資源我們不想讓它們繼續運作,怎麼辦呢?

當然,協程也考慮了這種場景,提供了幾個便利的函數。

binding.btnStartPauseLifecyleCoroutine.setOnClickListener {
            lifecycleScope.launchWhenResumed {
                delay(5000)
                lifecycleScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@ThirdActivity, "協程還在運作中", Toast.LENGTH_SHORT).show()
                }
                println("協程還在運作中")
            }
        }
           

點選按鈕後,退回到桌面,等待幾秒後也沒發現列印,從桌面回到App後,發現Toast和列印都出現了。

這也符合了我們的要求:App在前台時協程工作,App在背景時協程停止工作,避免不必要的資源浪費。

launchWhenResumed()函數顧名思義是當Activity處在Resume狀态時激活協程,非Resume狀态時挂起協程,類似的還有launchWhenCreated、launchWhenStarted。

關聯生命周期的協程的原理

解決記憶體洩漏的原理

知道了怎麼使用,又到了探索原理的時刻,重點在協程作用域。

#LifecycleOwner.kt
//擴充屬性
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

#Lifecycle.kt
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            //構造新的協程作用域,預設在主線程執行協程
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                //協程作用域關聯生命周期
                newScope.register()
                return newScope
            }
        }
    }

fun register() {
    launch(Dispatchers.Main.immediate) {
        if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
            //監聽生命周期變化
            lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
        } else {
            //如果已經處在destroy狀态,直接取消協程
            coroutineContext.cancel()
        }
    }
}
           

由上可知:

  1. LifecycleOwner有個擴充屬性lifecycleScope,而LifecycleOwner又持有了Lifecycle,是以LifecycleOwner的lifecycleScope來自于Lifecycle的擴充屬性coroutineScope
  2. 既然是Lifecycle的擴充屬性,理所當然可以監聽Lifecycle的狀态變化

lifecycleScope 監聽了Lifecycle的狀态變化,直接看其回調的處理即可:

#Lifecycle.kt
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
        //如果處于Destroy狀态,也就是Activity被銷毀了,那麼移除監聽者
        lifecycle.removeObserver(this)
        //取消協程
        coroutineContext.cancel()
    }
}
           

至此就比較明了了:

每個Activity執行個體就是一個LifecycleOwner,進而每個Activity都關聯了一個lifecycleScope對象,該對象可以監聽Activity的生命周期,在Activity銷毀時取消協程。

避免資源浪費原理

相較于解決記憶體洩漏原理,避免資源浪費原理比較繞,我們簡單捋一下。

以launchWhenResumed函數為例,它是LifecycleCoroutineScope裡的函數:

#Lifecycle.kt
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
    //啟動了協程
    lifecycle.whenResumed(block)
}

#PausingDispatcher.kt
public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
    return whenStateAtLeast(Lifecycle.State.RESUMED, block)
}

public suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspend CoroutineScope.() -> T
): T = withContext(Dispatchers.Main.immediate) {
    //切換協程,在主線程執行
    val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
    //協程分發器
    val dispatcher = PausingDispatcher()
    //關聯了生命周期
    val controller =
        LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
    try {
        //在新的協程裡執行block
        withContext(dispatcher, block)
    } finally {
        controller.finish()
    }
}
           

以上透露了三個資訊:

  1. launchWhenResumed 不是挂起函數,它内部啟動了新的協程
  2. launchWhenResumed的閉包要通過PausingDispatcher 排程
  3. LifecycleController 關聯了生命周期

重點看第3點:

#LifecycleController.kt
private val observer = LifecycleEventObserver { source, _ ->
    if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
        //取消協程
        handleDestroy(parentJob)
    } else if (source.lifecycle.currentState < minState) {
        //小于目标狀态,比如非Resume,則挂起協程
        dispatchQueue.pause()
    } else {
        //繼續分發協程
        dispatchQueue.resume()
    }
}

init {
    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
        handleDestroy(parentJob)
    } else {
        //LifecycleController 初始化時監聽生命周期
        lifecycle.addObserver(observer)
    }
}
           

還是通過了lifecycle關聯了生命周期。

以上代碼結合着看估計還是有點懵,也有點繞,沒關系老規矩,用圖一看便知:

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

重點在于是否可以分發的判斷,該判斷是基于DispatchQueue裡的狀态:

fun canRun() = finished || !paused
           

當非Resume狀态時,paused=true,不能分發;

當處在Resume狀态時,paused=false,能分發。

當Activity退出,finished=true。

3. ViewModel與協程的配合

沒有關聯生命周期的協程的使用

在MVVM的架構裡,推薦的做法是在ViewModel裡進行資料的請求,如:

val liveData = MutableLiveData<String>()
    fun getStuInfo() {
        thread {
            //模拟網絡請求
            Thread.sleep(2000)
            liveData.postValue("hello world")
        }
    }
           

而後在Activity裡監聽資料的變化:

//監聽資料變化
        val vm  by viewModels<MyVM>()
        vm.liveData.observe(this) {
            Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        }
        vm.getStuInfo()

           

當然直接開線程的請求資料的方式并不優雅,既然有了協程,那麼用協程切換到子線程請求即可。

val scope = CoroutineScope(Job())
    fun getStuInfoV2() {
        scope.launch {
            //模拟網絡請求
            delay(4000)
            liveData.postValue("hello world")
            println("hello world")
        }
    }
           

和上面一樣的測試步驟:

當退出Activity後,ViewModel裡的協程列印還在持續,雖然此時Activity并沒有洩漏,但我們也知道ViewModel是為Activity服務的,Activity都銷毀了,ViewModel沒存在的必要了,是以其關聯的協程也該取消達到節約資源的目的。

關聯生命周期的協程的使用

fun getInfo() {
        viewModelScope.launch {
            //模拟網絡請求
            delay(4000)
            liveData.postValue("hello world")
            println("hello world")
        }
    }
           

此種寫法比上面的更簡潔。

當退出Activity後,協程被取消了,當然列印也不會出現了。

關聯生命周期的協程的原理

重點在viewModelScope對象,它是ViewModel的一個擴充屬性:

#ViewModel.kt
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        //查緩存
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        //加入到緩存裡
        return setTagIfAbsent(
            JOB_KEY,
            //構造協程作用域
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }
           

ViewModel構造了一個擴充屬性:viewModelScope,用以表示目前ViewModel的協程作用域,将作用域對象存儲到Map裡。

後續在ViewModel裡想要使用協程的地方調用viewModelScope即可,極大增強了便利性。

接下來看看它如何在Activity銷毀後取消協程。

final void clear() {
        mCleared = true;
        if (mBagOfTags != null) {
            synchronized (mBagOfTags) {
                //從緩存取出協程作用域
                for (Object value : mBagOfTags.values()) {
                //取消協程
                closeWithRuntimeException(value);
            }
            }
        }
    }
           

整個流程用圖表示:

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

上面的流程涉及到ViewModel的原理,有興趣可以移步:Jetpack ViewModel 抽絲剝繭

4. Application建立全局的協程作用域

無論是Activity裡的lifecycleScope亦或是ViewModel裡的viewModelScope,都和頁面有關系,頁面銷毀了它們都沒有存在的必要了。而有時候我們需要在頁面之外的其它地方使用協程,它們不受頁面建立與銷毀的影響,通常我們會想到使用全局的協程。

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

自定義Application擴充屬性

val Application.scope: CoroutineScope
get() {
    return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
//使用
application.scope.launch {
    delay(5000)
    println("協程在全局狀态運作1")
}
           

構造了全局的協程作用域,當在其它子產品拿到Application執行個體時就可以通路該擴充屬性。

此種方式的好處:可以友善地自定義協程上下文。

GlobalScope

一般在測試的時候使用,不推薦使用在正式的項目裡。

GlobalScope.launch {
    delay(5000)
    println("協程在全局狀态運作2")
}
           

ProcessLifecycleOwner

官方出品,它更多的時候被用來監測App在前背景的狀态,原理是通過監聽Lifecycle,既然有Lifecycle,當然有協程作用域了:

ProcessLifecycleOwner.get().lifecycleScope.launch {
    delay(5000)
    println("協程在全局狀态運作3")
}
           

5. Flow、協程、生命周期的三角關系

概念明晰

從Android開發的角度來看,三者有如下差別:

  1. 生命周期主要說的是UI的生命周期
  2. Flow和協程是Kotlin語言範疇的,Kotlin是跨平台的
  3. Flow必須要在協程裡使用
  4. 結合1.2兩點,我們發現關聯了生命周期的協程作用域都是以擴充屬性的形式存在的,畢竟其它平台可能不需要關聯生命周期

Flow 與生命周期

LiveData關聯生命周期

Flow号稱是LiveData的增強實作,我們知道LiveData是可以檢測生命周期的,如:

binding.btnStartLifecycleLivedata.setOnClickListener { 
            vm.liveData.observe(this) {
                //接收資料
                println("hello world")
            }
            vm.getInfo()
        }
           

當App退回到桌面,此時即使ViewModel裡繼續往LiveData裡指派,也不會觸發LiveData回調。當App恢複到前台後,LiveData回調将被觸發。

此種設計是為了避免不必要的資源浪費。

Flow結合launchWhenXX

此時你可能會想到:不用LiveData傳遞資料,改用Flow替代它,該怎麼關聯生命周期呢?

按照前面的經驗,很容易有如下寫法:

binding.btnStartLifecycleFlowWhen.setOnClickListener {
            lifecycleScope.launchWhenResumed {
                MyFlow().flow.collect {
                    println("collect when $it")
                }
            }
        }

    val flow = flow {
        var count = 0
        while (true) {
            kotlinx.coroutines.delay(1000)
            println("emit hello world $count")
            emit(count++)
        }
    }
           

構造一個冷流Flow,在Activity裡通過launchWhenResumed啟動協程,并在協程裡調用collect末端操作符。collect觸發flow閉包裡的代碼執行,源源不斷地發射資料,collect閉包裡的列印也将持續。

此時将App退回到桌面,發現列印沒有出現,而後将App傳回前台,列印繼續。如此一來就可以達成和LiveData一樣的效果。

從列印結果我們還發現有趣的現象:

在列印到數字5的時候,我們退回桌面,等待若幹秒後再回到前台,此時從6開始列印

說明launchWhenXX函數在Activity不活躍時并沒有終止flow上遊的工作,僅僅隻是将協程挂起了

Flow結合repeatOnLifecycle

而更多的時候,當Activity不活躍時,我們不想要flow繼續工作,此時引入了另一個API:repeatOnLifecycle

binding.btnStartLifecycleFlowRepeat.setOnClickListener {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.RESUMED) {
                    MyFlow().flow.collect {
                        println("collect repeat $it")
                    }
                }
                println("repeatOnLifecycle over")
            }
        }
           

通過列印發現:

在列印到數字5的時候,我們退回桌面,等待若幹秒後再回到前台,此時從0開始列印

說明repeatOnLifecycle函數在Activity不活躍時終止了flow上遊的工作,因為協程被取消了。當Activity活躍後,協程又重新啟動,flow工作重來一次

你也許還有疑惑:上面的Demo沒有直接證明兩者的差別,因為在Activity退到桌面後flow閉包裡的列印都沒出現。

對Demo稍加修改,結果就會顯而易見:

val flow = flow {
        var count = 0
        while (true) {
            kotlinx.coroutines.delay(1000)
            println("emit hello world $count")
            emit(count++)
        }
    }.flowOn(Dispatchers.IO)
           

使用repeatOnLifecycle時,在Activity退到桌面後,列印消失,說明flow停止工作

使用launchWhenXX是,在Activity退到桌面後,列印繼續,說明flow在工作

repeatOnLifecycle 原理

repeatOnLifecycle 是LifecycleOwner的擴充函數,進而是lifecycle的擴充函數,是以它就擁有了生命周期。

repeatOnLifecycle 函數裡開啟了新的協程,并監聽生命周期的變化:

//監聽生命周期
observer = LifecycleEventObserver { _, event ->
    if (event == startWorkEvent) {
        //大于目标生命狀态,則啟動協程
        launchedJob = [email protected] {
            // Mutex makes invocations run serially,
            // coroutineScope ensures all child coroutines finish
            mutex.withLock {
                coroutineScope {
                    block()
                }
            }
        }
        return@LifecycleEventObserver
    }
    if (event == cancelWorkEvent) {
        //小于目标生命狀态,則取消協程
        launchedJob?.cancel()
        launchedJob = null
    }
    if (event == Lifecycle.Event.ON_DESTROY) {
        //Activity退出,則喚醒挂起的協程
        cont.resume(Unit)
    }
}
[email protected](observer as LifecycleEventObserver)
           

repeatOnLifecycle 還有另一種使用方式:

MyFlow().flow.flowWithLifecycle([email protected], Lifecycle.State.RESUMED)
                    .collectLatest {
                        println("collect repeat $it")
                    }
           

和repeatOnLifecycle一樣的效果,隻是此種方式産生的Flow是線程安全的。

launchWhenXX與repeatOnLifecycle差別與應用場景

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

最後,總結三者之間的關系。

狂飙吧,Lifecycle與協程、Flow的化學反應前言1. 生命周期的前世今生2. Activity與協程的結合3. ViewModel與協程的配合4. Application建立全局的協程作用域5. Flow、協程、生命周期的三角關系您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

Flow很強大也很好用,關鍵是怎麼用,如何從衆多的Flow操作符選擇合适進行業務開發,如何一眼就分辨它們的作用,下篇将揭開Flow常見操作符神秘的面紗,敬請關注。

本文基于Kotlin 1.5.3,文中完整實驗Demo請點選

您若喜歡,請點贊、關注、收藏,您的鼓勵是我前進的動力

持續更新中,和我一起步步為營系統、深入學習Android/Kotlin

1、Android各種Context的前世今生

2、Android DecorView 必知必會

3、Window/WindowManager 不可不知之事

4、View Measure/Layout/Draw 真明白了

5、Android事件分發全套服務

6、Android invalidate/postInvalidate/requestLayout 徹底厘清

7、Android Window 如何确定大小/onMeasure()多次執行原因

8、Android事件驅動Handler-Message-Looper解析

9、Android 鍵盤一招搞定

10、Android 各種坐标徹底明了

11、Android Activity/Window/View 的background

12、Android Activity建立到View的顯示過

13、Android IPC 系列

14、Android 存儲系列

15、Java 并發系列不再疑惑

16、Java 線程池系列

17、Android Jetpack 前置基礎系列

18、Android Jetpack 易學易懂系列

19、Kotlin 輕松入門系列

20、Kotlin 協程系列全面解讀