在 Android 應(yīng)用程序中,Kotlin 流通常從 UI 層收集以在屏幕上顯示數(shù)據(jù)更新。 但是,您希望收集這些流,以確保在視圖轉(zhuǎn)到后臺(tái)時(shí)不會(huì)做多余的工作、浪費(fèi)資源(CPU 和內(nèi)存)或泄漏數(shù)據(jù)。
在本文中,您將了解 Lifecycle.repeatOnLifecycle 和 Flow.flowWithLifecycle API 如何保護(hù)您免于浪費(fèi)資源,以及為什么它們是 UI 層中用于流收集的良好默認(rèn)設(shè)置。
資源浪費(fèi)
建議從應(yīng)用程序?qū)哟谓Y(jié)構(gòu)的較低層公開 Flow<T> API,而不管流生成器的實(shí)現(xiàn)細(xì)節(jié)如何。 但是,您也應(yīng)該安全地收集它們。
由通道支持或使用帶有緩沖區(qū)(例如 buffer、conflate、flowOn 或 shareIn)的運(yùn)算符的冷流不安全地使用某些現(xiàn)有 API(例如 CoroutineScope.launch、Flow<T>.launchIn 或 LifecycleCoroutineScope.launchWhenX)收集 ,除非你在活動(dòng)進(jìn)入后臺(tái)時(shí)手動(dòng)取消啟動(dòng)協(xié)程的Job。 這些 API 將保持底層流生成器處于活動(dòng)狀態(tài),同時(shí)在后臺(tái)將項(xiàng)目發(fā)送到緩沖區(qū)中,從而浪費(fèi)資源。
注意:冷流是一種在新訂閱者收集時(shí)按需執(zhí)行生產(chǎn)者代碼塊的流。
例如,考慮這個(gè)使用 callbackFlow 發(fā)出位置更新的流:
// Implementation of a cold flow backed by a Channel that sends Location updates
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// clean up when Flow collection ends
awaitClose {
removeLocationUpdates(callback)
}
}
注意:在內(nèi)部,callbackFlow 使用一個(gè)通道,它在概念上非常類似于阻塞隊(duì)列,并且默認(rèn)容量為 64 個(gè)元素。
使用上述任何 API 從 UI 層收集此流,即使視圖未在 UI 中顯示它們,也會(huì)保持流發(fā)射位置! 請(qǐng)參閱下面的示例:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Collects from the flow when the View is at least STARTED and
// SUSPENDS the collection when the lifecycle is STOPPED.
// Collecting the flow cancels when the View is DESTROYED.
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
// Same issue with:
// - lifecycleScope.launch { /* Collect from locationFlow() here */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
LifecycleScope.launchWhenStarted 暫停協(xié)程的執(zhí)行。 新位置不會(huì)被處理,但是 callbackFlow 生產(chǎn)者會(huì)繼續(xù)發(fā)送位置。 使用lifecycleScope.launch 或launchIn API 更加危險(xiǎn),因?yàn)榧词挂晥D在后臺(tái),它也會(huì)不斷消耗位置! 這可能會(huì)使您的應(yīng)用程序崩潰。
要通過這些 API 解決這個(gè)問題,您需要在視圖轉(zhuǎn)到后臺(tái)時(shí)手動(dòng)取消收集以取消 callbackFlow 并避免位置提供者發(fā)出項(xiàng)目并浪費(fèi)資源。 例如,您可以執(zhí)行以下操作:
class LocationActivity : AppCompatActivity() {
// Coroutine listening for Locations
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
locationUpdatesJob?.cancel()
super.onStop()
}
}
這是一個(gè)很好的解決方案,但這是樣板文件,朋友們! 如果 Android 開發(fā)人員有一個(gè)普遍的真理,那就是我們絕對(duì)討厭編寫樣板代碼。 不必編寫樣板代碼的最大好處之一是代碼越少,出錯(cuò)的機(jī)會(huì)就越少!
Lifecycle.repeatOnLifecycle
現(xiàn)在我們知道問題出在哪里,是時(shí)候想出一個(gè)解決方案了。 解決方案需要 1) 簡(jiǎn)單,2) 友好或易于記憶/理解,更重要的是 3) 安全! 無論流程實(shí)現(xiàn)細(xì)節(jié)如何,它都應(yīng)該適用于所有用例。
不用多說,您應(yīng)該使用的 API 是 Lifecycle.repeatOnLifecycle 可用的 Lifecycle-runtime-ktx 庫(kù)。
注意:這些 API 在 lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 庫(kù)或更高版本中可用。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from locationFlow when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
repeatOnLifecycle 是一個(gè)掛起函數(shù),它以 Lifecycle.State 作為參數(shù),用于在生命周期達(dá)到該狀態(tài)時(shí)自動(dòng)創(chuàng)建和啟動(dòng)一個(gè)新的協(xié)程,并將塊傳遞給它,并在生命周期達(dá)到該狀態(tài)時(shí)取消正在執(zhí)行塊的正在進(jìn)行的協(xié)程 低于狀態(tài)。
repeatOnLifecycle 是一個(gè)掛起函數(shù),它以 Lifecycle.State 作為參數(shù),用于在生命周期達(dá)到該狀態(tài)時(shí)自動(dòng)創(chuàng)建和啟動(dòng)一個(gè)新的協(xié)程,并將塊傳遞給它,并在生命周期低于狀態(tài)該狀態(tài)時(shí)取消正在執(zhí)行的協(xié)程 。
這避免了任何樣板代碼,因?yàn)樵诓辉傩枰獏f(xié)程時(shí)取消協(xié)程的相關(guān)代碼是由 repeatOnLifecycle 自動(dòng)完成的。 如您所料,建議在活動(dòng)的 onCreate 或片段的 onViewCreated 方法中調(diào)用此 API 以避免意外行為。 請(qǐng)參閱以下使用片段的示例:
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
重要提示:Fragment應(yīng)始終使用 viewLifecycleOwner 來觸發(fā) UI 更新。 但是,有時(shí)可能沒有視圖的 DialogFragments 并非如此。 對(duì)于 DialogFragments,您可以使用lifecycleOwner。
Note: These APIs are available in the
*lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01*library or later.
回到開頭,直接從以生命周期范圍.launch 啟動(dòng)的協(xié)程收集 locationFlow 是危險(xiǎn)的,因?yàn)榧词?View 在后臺(tái),收集也會(huì)繼續(xù)發(fā)生。
repeatOnLifecycle 可防止您浪費(fèi)資源和應(yīng)用程序崩潰,因?yàn)樗鼤?huì)在生命周期移入和移出目標(biāo)狀態(tài)時(shí)停止并重新啟動(dòng)流收集。

當(dāng)您只有一個(gè)流要收集時(shí),您也可以使用 Flow.flowWithLifecycle 運(yùn)算符。 該 API 在底層使用了 repeatOnLifecycle API,并在 Lifecycle 移入和移出目標(biāo)狀態(tài)時(shí)發(fā)出項(xiàng)目或取消底層生產(chǎn)者。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Listen to one flow in a lifecycle-aware manner using flowWithLifecycle
lifecycleScope.launch {
locationProvider.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {
// New location! Update the map
}
}
// Listen to multiple flows
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// As collect is a suspend function, if you want to collect
// multiple flows in parallel, you need to do so in
// different coroutines
launch {
flow1.collect { /* Do something */ }
}
launch {
flow2.collect { /* Do something */ }
}
}
}
}
}
注意:此 API 名稱以 Flow.flowOn(CoroutineContext) 運(yùn)算符為先例,因?yàn)?Flow.flowWithLifecycle 更改了用于收集上游流的 CoroutineContext,同時(shí)不影響下游。 此外,類似于 flowOn,F(xiàn)low.flowWithLifecycle 添加了一個(gè)緩沖區(qū),以防消費(fèi)者跟不上生產(chǎn)者。 這是因?yàn)樗膶?shí)現(xiàn)使用了 callbackFlow。
配置底層生產(chǎn)者
即使您使用這些 API,也要注意可能浪費(fèi)資源的熱流,即使它們沒有被任何人收集! 它們有一些有效的用例,但請(qǐng)記住這一點(diǎn)并在需要時(shí)記錄下來。 讓底層流生成器在后臺(tái)處于活動(dòng)狀態(tài),即使浪費(fèi)資源,對(duì)某些用例也是有益的:您可以立即獲得可用的新數(shù)據(jù),而不是趕上并暫時(shí)顯示陳舊數(shù)據(jù)。 根據(jù)用例,決定生產(chǎn)者是否需要始終處于活動(dòng)狀態(tài)。
MutableStateFlow 和 MutableSharedFlow API 公開了一個(gè) subscriptionCount 字段,您可以使用該字段在 subscriptionCount 為零時(shí)停止底層生產(chǎn)者。 默認(rèn)情況下,只要持有流實(shí)例的對(duì)象在內(nèi)存中,它們就會(huì)使生產(chǎn)者保持活動(dòng)狀態(tài)。 但是,有一些有效的用例,例如,使用 StateFlow 從 ViewModel 向 UI 公開 UiState。 沒關(guān)系! 此用例要求 ViewModel 始終向 View 提供最新的 UI 狀態(tài)。
同樣, Flow.stateIn 和 Flow.shareIn 運(yùn)算符可以為此配置共享啟動(dòng)策略。 WhileSubscribed() 將在沒有活動(dòng)觀察者時(shí)停止底層生產(chǎn)者! 相反,只要他們使用的 CoroutineScope 處于活動(dòng)狀態(tài),Eagerly 或 Lazily 就會(huì)使底層生產(chǎn)者保持活動(dòng)狀態(tài)。
Note: The APIs shown in this article are a good default to collect flows from the UI and should be used regardless of the flow implementation detail. These APIs do what they need to do: stop collecting if the UI isn’t visible on screen. It’s up to the flow implementation if it should be always active or not.
與 LiveData 的比較
您可能已經(jīng)注意到這個(gè) API 的行為與 LiveData 類似,這是真的! LiveData 知道 Lifecycle,它的重啟行為使其非常適合從 UI 觀察數(shù)據(jù)流。 Lifecycle.repeatOnLifecycle 和 Flow.flowWithLifecycle API 也是如此!
使用這些 API 收集流是純 Kotlin 應(yīng)用程序中 LiveData 的自然替代品。 如果您使用這些 API 進(jìn)行流收集,LiveData 不會(huì)比協(xié)程和流提供任何好處。 更重要的是,流更加靈活,因?yàn)樗鼈兛梢詮娜魏?Dispatcher 收集,并且可以由所有操作員提供支持。 與 LiveData 不同,LiveData 的可用運(yùn)算符有限,并且始終從 UI 線程觀察其值。
數(shù)據(jù)綁定中的 StateFlow 支持
另一方面,您可能使用 LiveData 的原因之一是數(shù)據(jù)綁定支持它。 那么,StateFlow 也是如此! 有關(guān)數(shù)據(jù)綁定中 StateFlow 支持的更多信息,請(qǐng)查看官方文檔。