Jetpack MVVM 常見(jiàn)錯(cuò)誤二:在 launchWhenX 中啟動(dòng)協(xié)程

image.png

Flow vs LiveData

自 StateFlow/ SharedFlow 出現(xiàn)后纫骑, 官方開(kāi)始推薦在 MVVM 中使用 Flow 替換 LiveData辉懒。 見(jiàn)文章:https://juejin.cn/post/6979008878029570055

Flow 基于協(xié)程實(shí)現(xiàn)渗蟹,具有豐富的操作符僧叉,通過(guò)這些操作符可以實(shí)現(xiàn)線程切換、處理流式數(shù)據(jù)哥谷,相比 LiveData 功能更加強(qiáng)大岸夯。 但唯有一點(diǎn)不足,無(wú)法像 LiveData 那樣感知生命周期们妥。

感知生命周期為 LiveData 至少帶來(lái)以下兩個(gè)好處:

  • 避免泄漏:當(dāng) lifecycleOwner 進(jìn)入 DESTROYED 時(shí)猜扮,會(huì)自動(dòng)刪除 Observer
  • 節(jié)省資源:當(dāng) lifecycleOwner 進(jìn)入 STARTED 時(shí)才開(kāi)始接受數(shù)據(jù),避免 UI 處于后臺(tái)時(shí)的無(wú)效計(jì)算监婶。

Flow 也需要做到上面兩點(diǎn)旅赢,才能真正地替代 LiveData。

lifecycleScope

lifecycle-runtime-ktx 庫(kù)提供了 lifecycleOwner.lifecycleScope 擴(kuò)展,可以在當(dāng)前 Activity 或 Fragment 銷(xiāo)毀時(shí)結(jié)束此協(xié)程鲜漩,防止泄露源譬。

Flow 也是運(yùn)行在協(xié)程中的集惋,lifecycleScope 可以幫助 Flow 解決內(nèi)存泄露的問(wèn)題:

lifecycleScope.launch {
    viewMode.stateFlow.collect { 
       updateUI(it)
    }
}

雖然解決了內(nèi)存泄漏問(wèn)題孕似, 但是 lifecycleScope.launch 會(huì)立即啟動(dòng)協(xié)程,之后一直運(yùn)行直到協(xié)程銷(xiāo)毀刮刑,無(wú)法像 LiveData 僅當(dāng) UI 處于前臺(tái)才執(zhí)行喉祭,對(duì)資源的浪費(fèi)比較大。

因此雷绢,lifecycle-runtime-ktx 又為我們提供了 LaunchWhenStartedLaunchWhenResumed ( 下文統(tǒng)稱為 LaunchWhenX

launchWhenX 的利與弊

LaunchWhenX 會(huì)在 lifecycleOwner 進(jìn)入 X 狀態(tài)之前一直等待泛烙,又在離開(kāi) X 狀態(tài)時(shí)掛起協(xié)程。 lifecycleScope + launchWhenX 的組合終于使 Flow 有了與 LiveData 相媲美的生命周期可感知能力:

  • 避免泄露:當(dāng) lifecycleOwner 進(jìn)入 DESTROYED 時(shí)翘紊, lifecycleScope 結(jié)束協(xié)程
  • 節(jié)省資源:當(dāng) lifecycleOwner 進(jìn)入 STARTED/RESUMED 時(shí) launchWhenX 恢復(fù)執(zhí)行蔽氨,否則掛起。

但對(duì)于 launchWhenX 來(lái)說(shuō)帆疟, 當(dāng) lifecycleOwner 離開(kāi) X 狀態(tài)時(shí)鹉究,協(xié)程只是掛起協(xié)程而非銷(xiāo)毀,如果用這個(gè)協(xié)程來(lái)訂閱 Flow踪宠,就意味著雖然 Flow 的收集暫停了自赔,但是上游的處理仍在繼續(xù),資源浪費(fèi)的問(wèn)題解決地不夠徹底柳琢。

image.png

資源浪費(fèi)

舉一個(gè)資源浪費(fèi)的例子绍妨,加深理解

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            try { offer(result.lastLocation) } catch(e: Exception) {}
        }
    }
    // 持續(xù)獲取最新地理位置
    requestLocationUpdates(
        createLocationRequest(), callback, Looper.getMainLooper())

}

如上,使用 callbackFlow 封裝了一個(gè) GoogleMap 中獲取位置的服務(wù)柬脸,requestLocationUpdates 實(shí)時(shí)獲取最新位置他去,并通過(guò) Flow 返回

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 進(jìn)入 STATED 時(shí),collect 開(kāi)始接收數(shù)據(jù)
        // 進(jìn)入 STOPED 時(shí)倒堕,collect 掛起
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // Update the UI
            } 
        }
    }
}

當(dāng) LocationActivity 進(jìn)入 STOPED 時(shí)灾测, lifecycleScope.launchWhenStarted 掛起,停止接受 Flow 的數(shù)據(jù)涩馆,UI 也隨之停止更新行施。但是 callbackFlow 中的 requestLocationUpdates 仍然還在持續(xù),造成資源的浪費(fèi)魂那。

因此蛾号,即使在 launchWhenX 中訂閱 Flow 仍然是不夠的,無(wú)法完全避免資源的浪費(fèi)

解決辦法:repeatOnLifecycle

lifecycle-runtime-ktx 自 2.4.0-alpha01 起涯雅,提供了一個(gè)新的協(xié)程構(gòu)造器 lifecyle.repeatOnLifecycle鲜结, 它在離開(kāi) X 狀態(tài)時(shí)銷(xiāo)毀協(xié)程,再進(jìn)入 X 狀態(tài)時(shí)再啟動(dòng)協(xié)程。從其命名上也可以直觀地認(rèn)識(shí)這一點(diǎn)精刷,即圍繞某生命周期的進(jìn)出反復(fù)啟動(dòng)新協(xié)程拗胜。

image.png

使用 repeatOnLifecycle 可以彌補(bǔ)上述 launchWhenX 對(duì)協(xié)程僅掛起而不銷(xiāo)毀的弊端。因此怒允,正確訂閱 Flow 的寫(xiě)法應(yīng)該如下(以在 Fragment 中為例):

onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
            viewMode.stateFlow.collect { ... }
        }
    }
}

當(dāng) Fragment 處于 STARTED 狀態(tài)時(shí)會(huì)開(kāi)始收集數(shù)據(jù)埂软,并且在 RESUMED 狀態(tài)時(shí)保持收集,最終在 Fragment 進(jìn)入 STOPPED 狀態(tài)時(shí)結(jié)束收集過(guò)程纫事。

需要注意 repeatOnLifecycle 本身是個(gè)掛起函數(shù)勘畔,一旦被調(diào)用,將走不到后續(xù)代碼丽惶,除非 lifecycle 進(jìn)入 DESTROYED炫七。

順道提一點(diǎn),前面舉得地圖SDK的例子是個(gè)冷流的例子钾唬,對(duì)于熱流(StateFlow/SharedFlow)是否有必要使用 repeatOnLifecycle 呢万哪? 個(gè)人認(rèn)為熱流的使用場(chǎng)景中,像前面例子那樣的情況會(huì)少一些抡秆,但是在 StateFlow/SharedFlow 的實(shí)現(xiàn)中奕巍,需要為每個(gè) FlowCollector 分配一些資源,如果 FlowCollector 能即使銷(xiāo)毀也是有利的琅轧,同時(shí)為了保持寫(xiě)法的統(tǒng)一伍绳,無(wú)論冷流熱流都建議使用 repeatOnLifecycle

最后:Flow.flowWithLifecycle

當(dāng)我們只有一個(gè) Flow 需要收集時(shí),可以使用 flowWithLifecycle 這樣一個(gè) Flow 操作符的形式來(lái)簡(jiǎn)化代碼

lifecycleScope.launch {
     viewMode.stateFlow
          .flowWithLifecycle(this, Lifecycle.State.STARTED)
          .collect { ... }
 }

當(dāng)然乍桂,其本質(zhì)還是對(duì) repeatOnLifecycle 的封裝:

public fun <T> Flow<T>.flowWithLifecycle(
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
    lifecycle.repeatOnLifecycle(minActiveState) {
        this@flowWithLifecycle.collect {
            send(it)
        }
    }
    close()
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冲杀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子睹酌,更是在濱河造成了極大的恐慌权谁,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憋沿,死亡現(xiàn)場(chǎng)離奇詭異旺芽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)辐啄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)采章,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人壶辜,你說(shuō)我怎么就攤上這事悯舟。” “怎么了砸民?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵抵怎,是天一觀的道長(zhǎng)奋救。 經(jīng)常有香客問(wèn)我,道長(zhǎng)反惕,這世上最難降的妖魔是什么尝艘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮姿染,結(jié)果婚禮上背亥,老公的妹妹穿的比我還像新娘。我一直安慰自己盔粹,他們只是感情好隘梨,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著舷嗡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嵌莉。 梳的紋絲不亂的頭發(fā)上进萄,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音锐峭,去河邊找鬼中鼠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛沿癞,可吹牛的內(nèi)容都是我干的援雇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼椎扬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惫搏!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蚕涤,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤筐赔,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后揖铜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體茴丰,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年天吓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贿肩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡龄寞,死狀恐怖汰规,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情萄焦,我是刑警寧澤控轿,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布冤竹,位于F島的核電站,受9級(jí)特大地震影響茬射,放射性物質(zhì)發(fā)生泄漏鹦蠕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一在抛、第九天 我趴在偏房一處隱蔽的房頂上張望钟病。 院中可真熱鬧,春花似錦刚梭、人聲如沸肠阱。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)屹徘。三九已至,卻和暖如春衅金,著一層夾襖步出監(jiān)牢的瞬間噪伊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工氮唯, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鉴吹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓惩琉,卻偏偏與公主長(zhǎng)得像豆励,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞒渠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容