在 Android 應(yīng)用中,通常需要從 UI 層收集 Kotlin 數(shù)據(jù)流,以便在屏幕上顯示數(shù)據(jù)更新。同時(shí),您也會(huì)希望通過(guò)收集這些數(shù)據(jù)流,來(lái)避免產(chǎn)生不必要的操作和資源浪費(fèi) (包括 CPU 和內(nèi)存),以及防止在 View 進(jìn)入后臺(tái)時(shí)泄露數(shù)據(jù)。
本文將會(huì)帶您學(xué)習(xí)如何使用 LifecycleOwner.addRepeatingJob
、Lifecycle.repeatOnLifecycle
以及 Flow.flowWithLifecycle
API 來(lái)避免資源的浪費(fèi);同時(shí)也會(huì)介紹為什么這些 API 適合作為在 UI 層收集數(shù)據(jù)流時(shí)的默認(rèn)選擇。
資源浪費(fèi)
無(wú)論數(shù)據(jù)流生產(chǎn)者的具體實(shí)現(xiàn)如何,我們都 推薦 從應(yīng)用的較底層級(jí)暴露 Flow<T> API。不過(guò),您也應(yīng)該保證數(shù)據(jù)流收集操作的安全性。
使用一些現(xiàn)存 API (如 CoroutineScope.launch、Flow<T>.launchIn 或 LifecycleCoroutineScope.launchWhenX) 收集基于 channel 或使用帶有緩沖的操作符 (如 buffer、conflate、flowOn 或 shareIn) 的冷流的數(shù)據(jù)是 不安全的,除非您在 Activity 進(jìn)入后臺(tái)時(shí)手動(dòng)取消啟動(dòng)了協(xié)程的 Job。這些 API 會(huì)在內(nèi)部生產(chǎn)者在后臺(tái)發(fā)送項(xiàng)目到緩沖區(qū)時(shí)保持它們的活躍狀態(tài),而這樣一來(lái)就浪費(fèi)了資源。
注意: 冷流 是一種數(shù)據(jù)流類型,這種數(shù)據(jù)流會(huì)在新的訂閱者收集數(shù)據(jù)時(shí),按需執(zhí)行生產(chǎn)者的代碼塊。
例如下面的例子中,使用 callbackFlow 發(fā)送位置更新的數(shù)據(jù)流:
// 基于 Channel 實(shí)現(xiàn)的冷流,可以發(fā)送位置的更新
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) // 在出現(xiàn)異常時(shí)關(guān)閉 Flow
}
// 在 Flow 收集結(jié)束時(shí)進(jìn)行清理操作
awaitClose {
removeLocationUpdates(callback)
}
}
注意: callbackFlow 內(nèi)部使用 channel 實(shí)現(xiàn),其概念與阻塞 隊(duì)列 十分類似,并且默認(rèn)容量為 64。
使用任意前述 API 從 UI 層收集此數(shù)據(jù)流都會(huì)導(dǎo)致其持續(xù)發(fā)送位置信息,即使視圖不再展示數(shù)據(jù)也不會(huì)停止!示例如下:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 最早在 View 處于 STARTED 狀態(tài)時(shí)從數(shù)據(jù)流收集數(shù)據(jù),并在
// 生命周期進(jìn)入 STOPPED 狀態(tài)時(shí) SUSPENDS(掛起)收集操作。
// 在 View 轉(zhuǎn)為 DESTROYED 狀態(tài)時(shí)取消數(shù)據(jù)流的收集操作。
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖
}
}
// 同樣的問(wèn)題也存在于:
// - lifecycleScope.launch { /* 在這里從 locationFlow() 收集數(shù)據(jù) */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
lifecycleScope.launchWhenStarted
掛起了協(xié)程的執(zhí)行。雖然新的位置信息沒(méi)有被處理,但 callbackFlow
生產(chǎn)者仍然會(huì)持續(xù)發(fā)送位置信息。使用 lifecycleScope.launch
或 launchIn
API 會(huì)更加危險(xiǎn),因?yàn)橐晥D會(huì)持續(xù)消費(fèi)位置信息,即使處于后臺(tái)也不會(huì)停止!這種情況可能會(huì)導(dǎo)致您的應(yīng)用崩潰。
為了解決這些 API 所帶來(lái)的問(wèn)題,您需要在視圖轉(zhuǎn)入后臺(tái)時(shí)手動(dòng)取消收集操作,以取消 callbackFlow
并避免位置提供者持續(xù)發(fā)送項(xiàng)目并浪費(fèi)資源。舉例來(lái)說(shuō),您可以像下面的例子這樣操作:
class LocationActivity : AppCompatActivity() {
// 位置的協(xié)程監(jiān)聽(tīng)器
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖。
}
}
}
override fun onStop() {
// 在視圖進(jìn)入后臺(tái)時(shí)停止收集數(shù)據(jù)
locationUpdatesJob?.cancel()
super.onStop()
}
}
這是一個(gè)不錯(cuò)的解決方案,美中不足的是有些冗長(zhǎng)。如果這個(gè)世界有一個(gè)有關(guān) Android 開(kāi)發(fā)者的普遍事實(shí),那一定是我們都不喜歡編寫(xiě)模版代碼。不必編寫(xiě)模版代碼的一個(gè)最大好處就是——寫(xiě)的代碼越少,出錯(cuò)的概率越小!
LifecycleOwner.addRepeatingJob
現(xiàn)在我們境遇相同,并且也知道問(wèn)題出在哪里,是時(shí)候找出一個(gè)解決方案了。我們的解決方案需要: 1. 簡(jiǎn)單;2. 友好或者說(shuō)便于記憶與理解;更重要的是 3. 安全!無(wú)論數(shù)據(jù)流的實(shí)現(xiàn)細(xì)節(jié)如何,它都應(yīng)能夠應(yīng)對(duì)所有用例。
事不宜遲——您應(yīng)該使用的 API 是 lifecycle-runtime-ktx 庫(kù)中所提供的 LifecycleOwner.addRepeatingJob
。請(qǐng)參考下面的代碼:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 最早在 View 處于 STARTED 狀態(tài)時(shí)從數(shù)據(jù)流收集數(shù)據(jù),并在
// 生命周期進(jìn)入 STOPPED 狀態(tài)時(shí) STOPPED(停止)收集操作。
// 它會(huì)在生命周期再次進(jìn)入 STARTED 狀態(tài)時(shí)自動(dòng)開(kāi)始進(jìn)行數(shù)據(jù)收集操作。
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖
}
}
}
}
addRepeatingJob
接收 Lifecycle.State 作為參數(shù),并用它與傳入的代碼塊一起,在生命周期到達(dá)該狀態(tài)時(shí),自動(dòng)創(chuàng)建并啟動(dòng)新的協(xié)程;同時(shí)也會(huì)在生命周期低于該狀態(tài)時(shí)取消正在運(yùn)行的協(xié)程。
由于 addRepeatingJob
會(huì)在協(xié)程不再被需要時(shí)自動(dòng)將其取消,因而可以避免產(chǎn)生取消操作相關(guān)的模版代碼。您也許已經(jīng)猜到,為了避免意外行為,這一 API 需要在 Activity 的 onCreate 或 Fragment 的 onViewCreated
方法中調(diào)用。下面是配合 Fragment 使用的示例:
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖
}
}
}
}
注意: 這些 API 在
lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01
庫(kù)或其更新的版本中可用。
使用 repeatOnLifecycle
出于提供更為靈活的 API 以及保存調(diào)用中的 CoroutineContext
的目的,我們也提供了 掛起函數(shù) Lifecycle.repeatOnLifecycle
供您使用。repeatOnLifecycle
會(huì)掛起調(diào)用它的協(xié)程,并會(huì)在進(jìn)出目標(biāo)狀態(tài)時(shí)重新執(zhí)行代碼塊,最后在 Lifecycle
進(jìn)入銷(xiāo)毀狀態(tài)時(shí)恢復(fù)調(diào)用它的協(xié)程。
如果您需要在重復(fù)工作前執(zhí)行一次配置任務(wù),同時(shí)希望任務(wù)可以在重復(fù)工作開(kāi)始前保持掛起,該 API 可以幫您實(shí)現(xiàn)這樣的操作。示例如下:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// 單次配置任務(wù)
val expensiveObject = createExpensiveObject()
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在生命周期進(jìn)入 STARTED 狀態(tài)時(shí)開(kāi)始重復(fù)任務(wù),在 STOPED 狀態(tài)時(shí)停止
// 對(duì) expensiveObject 進(jìn)行操作
}
// 當(dāng)協(xié)程恢復(fù)時(shí),`lifecycle` 處于 DESTROY 狀態(tài)。repeatOnLifecycle 會(huì)在
// 進(jìn)入 DESTROYED 狀態(tài)前掛起協(xié)程的執(zhí)行
}
}
}
Flow.flowWithLifecycle
當(dāng)您只需要收集一個(gè)數(shù)據(jù)流時(shí),也可以使用 Flow.flowWithLifecycle
操作符。這一 API 的內(nèi)部也使用 suspend Lifecycle.repeatOnLifecycle
函數(shù)實(shí)現(xiàn),并會(huì)在生命周期進(jìn)入和離開(kāi)目標(biāo)狀態(tài)時(shí)發(fā)送項(xiàng)目和取消內(nèi)部的生產(chǎn)者。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// 新的位置!更新地圖
}
.launchIn(lifecycleScope)
}
}
注意:
Flow.flowWithLifecycle
API 的命名以Flow.flowOn(CoroutineContext)
為先例,因?yàn)樗鼤?huì)在不影響下游數(shù)據(jù)流的同時(shí)修改收集上游數(shù)據(jù)流的CoroutineContext
。與flowOn
相似的另一點(diǎn)是,Flow.flowWithLifecycle
也加入了緩沖區(qū),以防止消費(fèi)者無(wú)法跟上生產(chǎn)者。這一特點(diǎn)源于其實(shí)現(xiàn)中使用的callbackFlow
。
配置內(nèi)部生產(chǎn)者
即使您使用了這些 API,也要小心那些可能浪費(fèi)資源的熱流,就算它們沒(méi)有被收集亦是如此!雖然針對(duì)這些熱流有一些合適的用例,但是仍要多加注意并在必要時(shí)進(jìn)行記錄。另一方面,在一些情況下,即使可能造成資源的浪費(fèi),令處于后臺(tái)的內(nèi)部數(shù)據(jù)流生產(chǎn)者保持活躍狀態(tài)也會(huì)利于某些用例,如: 您需要即時(shí)刷新可用數(shù)據(jù),而不是去獲取并暫時(shí)展示陳舊數(shù)據(jù)。您可以根據(jù)用例決定生產(chǎn)者是否需要始終處于活躍狀態(tài)。
您可以使用 MutableStateFlow
與 MutableSharedFlow
兩個(gè) API 中暴露的 subscriptionCount
字段來(lái)控制它們,當(dāng)該字段值為 0 時(shí),內(nèi)部的生產(chǎn)者就會(huì)停止。默認(rèn)情況下,只要持有數(shù)據(jù)流實(shí)例的對(duì)象還在內(nèi)存中,它們就會(huì)保持生產(chǎn)者的活躍狀態(tài)。針對(duì)這些 API 也有一些合適的用例,比如使用 StateFlow
將 UiState
從 ViewModel 中暴露給 UI。這么做很合適,因?yàn)樗馕吨?ViewModel 總是需要向 View 提供最新的 UI 狀態(tài)。
相似的,也可以為此類操作使用 共享開(kāi)始策略 配置 Flow.stateIn 與 Flow.shareIn 操作符。WhileSubscribed() 將會(huì)在沒(méi)有活躍的訂閱者時(shí)停止內(nèi)部的生產(chǎn)者!相應(yīng)的,無(wú)論數(shù)據(jù)流是 Eagerly (積極) 還是 Lazily (惰性) 的,只要它們使用的 CoroutineScope
還處于活躍狀態(tài),其內(nèi)部的生產(chǎn)者就會(huì)保持活躍。
注意: 本文中所描述的 API 可以很好的作為默認(rèn)從 UI 收集數(shù)據(jù)流的方式,并且無(wú)論數(shù)據(jù)流的實(shí)現(xiàn)方式如何,都應(yīng)該使用它們。這些 API 做了它們要做的事: 在 UI 于屏幕中不可見(jiàn)時(shí),停止收集其數(shù)據(jù)流。至于數(shù)據(jù)流是否應(yīng)該始終處于活動(dòng)狀態(tài),則取決于它的實(shí)現(xiàn)。
在 Jetpack Compose 中安全地收集數(shù)據(jù)流
Flow.collectAsState
函數(shù)可以在 Compose 中收集來(lái)自 composable 的數(shù)據(jù)流,并可以將值表示為 State<T>,以便能夠更新 Compose UI。即使 Compose 在宿主 Activity 或 Fragment 處于后臺(tái)時(shí)不會(huì)重組 UI,數(shù)據(jù)流生產(chǎn)者仍會(huì)保持活躍并會(huì)造成資源的浪費(fèi)。Compose 可能會(huì)遭遇與 View 系統(tǒng)相同的問(wèn)題。
在 Compose 中收集數(shù)據(jù)流時(shí),可以使用 Flow.flowWithLifecycle
操作符,示例如下:
@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
// 當(dāng)前位置,可以拿它做一些操作
}
注意,您 需要記得 生命周期感知型數(shù)據(jù)流使用 locationFlow
與 lifecycleOwner
作為鍵,以便始終使用同一個(gè)數(shù)據(jù)流,除非其中一個(gè)鍵發(fā)生改變。
Compose 的副作用 (Side-effect) 便是必須處在 受控環(huán)境中,因此,使用 LifecycleOwner.addRepeatingJob
不安全。作為替代,可以使用 LaunchedEffect
來(lái)創(chuàng)建跟隨 composable 生命周期的協(xié)程。在它的代碼塊中,如果您需要在宿主生命周期處于某個(gè) State 時(shí)重新執(zhí)行一個(gè)代碼塊,可以調(diào)用掛起函數(shù) Lifecycle.repeatOnLifecycle
。
對(duì)比 LiveData
您也許會(huì)覺(jué)得,這些 API 的表現(xiàn)與 LiveData 很相似——確實(shí)是這樣!LiveData 可以感知 Lifecycle,而且它的重啟行為使其十分適合觀察來(lái)自 UI 的數(shù)據(jù)流。同理 LifecycleOwner.addRepeatingJob
、suspend Lifecycle.repeatOnLifecycle
以及 Flow.flowWithLifecycle
等 API 亦是如此。
在純 Kotlin 應(yīng)用中,使用這些 API 可以十分自然地替代 LiveData 收集數(shù)據(jù)流。如果您使用這些 API 收集數(shù)據(jù)流,換成 LiveData (相對(duì)于使用協(xié)程和 Flow) 不會(huì)帶來(lái)任何額外的好處。而且由于 Flow 可以從任何 Dispatcher 收集數(shù)據(jù),同時(shí)也能通過(guò)它的 操作符 獲得更多功能,所以 Flow 也更為靈活。相對(duì)而言,LiveData 的可用操作符有限,且它總是從 UI 線程觀察數(shù)據(jù)。
數(shù)據(jù)綁定對(duì) StateFlow 的支持
另一方面,您會(huì)想要使用 LiveData
的原因之一,可能是它受到數(shù)據(jù)綁定的支持。不過(guò) StateFlow 也一樣!更多有關(guān)數(shù)據(jù)綁定對(duì) StateFlow 的支持信息,請(qǐng)參閱 官方文檔。
在 Android 開(kāi)發(fā)中,請(qǐng)使用 LifecycleOwner.addRepeatingJob
、suspend Lifecycle.repeatOnLifecycle
或Flow.flowWithLifecycle
從 UI 層安全地收集數(shù)據(jù)流。