Jetpack LiveData 的設計理念及改進

一、架構指南

在日常的開發中,我們經常會講到 MVC、MVP、MVVM 等多種開發模式,這其實都是應用架構的不同呈現方式,你目前又是使用的什么應用架構呢?

一個好的架構,其至少應該遵循兩個原則

  • 關注點分離。關注點分離指的是架構中的每一層應只專注于實現某一特定目的。一種常見的錯誤就是在 ActivityFragment 中編寫所有代碼(例如,直接在界面層完成網絡請求),這種基于界面的類應僅包含與系統和用戶交互的邏輯,你應該使這些類盡可能保持精簡,這樣可以避免許多與生命周期相關的問題
  • 通過模型驅動界面。模型是指負責處理應用數據的組件,模型應獨立于應用的界面層和其它應用組件,這樣才能不受應用的生命周期以及相關的關注點的影響

關于第一點。對于一個移動設備來說,其擁有的資源是固定且極其有限的,系統可能會隨時終止某些應用進程以便為前臺進程騰出內存空間。而且,即使是對于前臺進程來說,我們也并非擁有ActivityFragment的完全所有權,系統也可能會隨時因為內存空間不足或者系統配置更改等意外情況而銷毀它們。做到關注點分離,就是將各個層次的職責劃分開,避免將用戶數據和界面的生命周期強綁定,這樣當意外情況發生時用戶數據才不會隨之一起被銷毀

關于第二點。通過模型驅動界面,即界面對模型來說應該是相當于一個觀察者而獨立存在,界面不應該直接持有數據。界面通過觀察數據的變化來驅動自身,模型也可以通過改變自身來驅動界面更改。這里指的模型最好是持久性模型,即模型的生命周期需要比界面甚至應用進程更加長,典型代表即 Jetpack 中的 ViewModel 和 Room。這樣當有意外情況發生時,用戶也不會丟失數據,我們可以在隨后就為用戶恢復數據

Google 推薦的應用架構圖如下所示

當中,每個組件僅依賴于其下一級的組件。ViewModel 就是關注點分離原則的一個具體實現,是作為用戶數據的承載體處理者而存在的,Activity/Fragment 僅依賴于 ViewModel,ViewModel 就用于響應界面層的輸入和驅動界面層變化,Repository 用于為 ViewModel 提供一個單一的數據來源及數據存儲域,Repository 可以同時依賴于持久性數據模型和遠程服務器數據源

二、LiveData 的優勢

本文想要討論的就是 ViewModel 所包含的 LiveData

從 Google 官方推薦的應用架構圖可以看到,LiveData 是包含在 ViewModel 中的。LiveData 是一種可觀察的數據存儲器,Activity/Fragment 是觀察者,LiveData 是被觀察者。LiveData 具有生命周期感知能力,當我們向 LiveData 注冊了一個和 LifecycleOwner 相綁定的 Observer 時,如果 LifecycleOwner 的生命周期處于STARTEDRESUMED 狀態,則認為該觀察者當前處于活躍狀態,此時 LiveData 才會向觀察者發送事件通知,非活躍狀態的觀察者不會收到任何事件通知。且當 LifecycleOwner 的狀態變為DESTROYED時,LiveData 會自動解除和觀察者之間的綁定關系,以防止內存泄漏和過多的內存消耗。所以說,LiveData 具有生命周期感知能力,Activity/Fragment 無需和 LiveData 創建明確且嚴格的依賴路徑

ViewModel 和 LiveData 可以看做是對關注點分離通過模型驅動界面原則的一個共同實現,ViewModel 提供了讓用戶數據獨立于界面而存在的能力,LiveData 提供了安全地通知并驅動界面變化的能力

三、LiveData 的缺陷

LiveData 的設計初衷就決定了其具有以下三點缺陷(或者說特性):

  1. 只在 Observer 至少處于 STARTED 狀態時才能收到事件通知。Activity 只有在 onStart 后和 onStop 前才能收到事件通知
  2. LiveData 是黏性的。假設存在一個靜態的 LiveData 變量,且已經包含了數據,對其進行監聽的 Activity 都會收到其當前值的回調通知,即收到了黏性消息。這個概念就類似于 EventBus 中的 StickyEvent
  3. 中間值可以被新值直接掩蓋。當 Activity 處于后臺時,如果 LiveData 先后接收到了多個值,那么當 Activity 回到前臺時也只會收到最新值的一次回調,中間值直接被掩蓋了,Activity 完全不會感受到中間值的存在

以上三點特性都是由于界面層的特點來決定的:

  • 當界面處于后臺時,此時就完全沒有必要更新界面,因為此時界面對用戶來說完全不可見,且界面有可能再也沒有機會回到前臺了,所以只有當界面回到前臺時更新操作才是有意義的
  • 當界面被意外銷毀后,我們需要根據已有的數據來進行界面重建,所以 LiveData 被設計為黏性的
  • 對于 LiveData 所代表的界面狀態值來說,我們往往需要的只是其最新狀態,不需要處理中間值,所以 LiveData 的中間值可以被新值直接掩蓋

四、LiveData 作為消息通知組件

如果將 LiveData 單純地作為界面層狀態更新的載體來看待的話,那么以上三點特性就挺合情合理的了。但如果我們是將 LiveData 作為應用全局的消息通知組件的話,這三個特性就會給我們帶來困擾了

相信很多開發者都嘗試過將一個 LiveData 實例聲明為靜態變量,然后多個 Activity 通過同時監聽該 LiveData 來實現數據通信。這種方式的優點是:能夠以非常簡單的方式來實現跨頁面通信,同時也保障了生命周期安全。缺點是:在 Activity 處于 onStop 狀態時無法收到通知,且會收到黏性消息這種臟數據

在 Activity 處于 onStop 狀態時收到通知有什么意義呢?收到了黏性消息又會導致什么問題呢?這可以通過假設一個需求來說明

假設當前你的 App 包含一個圈子列表頁面,每個圈子 item 包含了一個按鈕用于改變對此圈子的關注狀態,當點擊關注后就會向用戶展示一個幾百毫秒的動畫效果。點擊 item 可以跳轉到圈子詳情頁,在詳情頁也包含一個按鈕用于改變圈子的關注狀態

現在,產品要求兩個頁面間的關注狀態是要能夠實時統一的,即在圈子詳情頁改變關注狀態后,圈子列表頁面也要跟著一起改變關注狀態。為了實現這個效果,可以聲明一個全局的靜態變量來實現跨頁面通知圈子關注狀態的變化

object FocusRepository {

    //String 表示圈子ID,Boolean 表示對該圈子的關注狀態
    val focusLiveData = MutableLiveData<Pair<String, Boolean>>()

}

class CircleListActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circle_list)
        //建立監聽
        FocusRepository.focusLiveData.observe(this, Observer {

        })
    }

}

class CircleDetailsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circle_details)
        onCircleFocusStateChanged("100", true)
    }

    private fun onCircleFocusStateChanged(circleId: String, focused: Boolean) {
        FocusRepository.focusLiveData.value = Pair(circleId, focused)
    }

}

這種方式就會導致三個問題:

  • 當用戶在 CircleDetailsActivity 改變了圈子的關注狀態后返回,CircleListActivity 從后臺回到前臺后才會收到 focusLiveData 的事件通知,此時才會觸發執行動畫。而在這種情況下我們并不希望用戶看到動畫效果,而是希望能夠在 CircleListActivity 改變關注狀態的同時就實時在觸發動畫了。此時使用 LiveData 就無法滿足我們的需求了,LiveData 不支持在 Activity 處于 onStop 狀態時下發通知
  • 如果在 CircleDetailsActivity 先后改變了多個圈子的關注狀態的話,那么就會導致另一個問題:中間值被最新值直接掩蓋了。這也是由于LiveData 不支持在 Activity 處于 onStop 狀態時下發通知導致的
  • 在 focusLiveData 已經有值的情況下,當用戶第一次打開 CircleListActivity 時,就會收到 focusLiveData 的回調通知。而此時 CircleListActivity 的數據會從服務器獲取,可以保證是最新的,并不需要本地值的回調通知,此時 focusLiveData 就相當于臟數據了。這種情況下,LiveData 也會給我們帶來困擾,其黏性消息其實就相當于臟數據了

五、EventLiveData

考慮到 LiveData 不那么適合用做應用全局的消息通知組件,所以我就基于其源碼實現了一個改良版的 EventLiveData,以此來解決 LiveData 的缺陷。EventLiveData 在使用上基本 LiveData 一樣,我只是對其進行了功能擴展

發送消息:

val eventLiveData = EventLiveData<String>()

//主線程調用
eventLiveData.setValue("leavesC")
//子線程調用
eventLiveData.postValue("leavesC")
//任意線程都可以調用,內部會自動判斷線程
eventLiveData.submitValue("leavesC")

不接收黏性消息:

//不接收黏性消息
//在 onResume 之后和 onDestroy 之前均能收到 Observer 回調
eventLiveData.observe(LifecycleOwner, Observer {

})

//不接收黏性消息
//在 onCreate 之后和 onDestroy 之前均能收到 Observer 回調
eventLiveData.observe(LifecycleOwner, Observer {

}, false)

接收黏性消息:

//接收黏性消息
//在 onResume 之后和 onDestroy 之前均能收到 Observer 回調
eventLiveData.observeSticky(LifecycleOwner, Observer {

})

//接收黏性消息
//在 onCreate 之后和 onDestroy 之前均能收到 Observer 回調
eventLiveData.observeSticky(LifecycleOwner, Observer {

}, false)

不和生命周期綁定:

//不接收黏性消息
eventLiveData.observeForever(Observer {

})

//接收黏性消息
eventLiveData.observeForeverSticky(Observer {

})

六、實現原理

EventLiveData 是基于 LiveData 的源碼來改造實現的,在理解了 LiveData 的設計理念和實現原理后來進行自定義其實就非常簡單了,這里就簡單說下我的實現思路

LiveData 內部包含一個 mVersion 變量用來標記當前值的版本,即值的新舊程度,當外部傳遞了新值時(不管是 setValue 還是 postValue),mVersion 均會遞增 +1

@MainThread
private fun setValue(value: T) {
    assertMainThread(
        "setValue"
    )
    mVersion++
    mData = value
    dispatchingValue(null)
}

同時 ObserverWrapper 內部包含一個 mLastVersion 用于標記 Observer 內最后一個被回調的 value 的新舊程度

private abstract class ObserverWrapper {

    //外部傳進來的對 LiveData 進行數據監聽的 Observer
    final Observer<? super T> mObserver;

    //用于標記 mObserver 是否處于活躍狀態
    boolean mActive;

    //用于標記 Observer 內最后一個被回調的 value 的新舊程度
    int mLastVersion = START_VERSION;

    ObserverWrapper(Observer<? super T> observer) {
        mObserver = observer;
    }

}

considerNotify 方法會根據 mLastVersion 的大小來決定是否需要向 Observer 回調值,那么我們只要控制 Observer 的 mLastVersion 的初始值大小不就可以避免舊值的通知了嗎?

private void considerNotify(ObserverWrapper observer) {
    ···
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

再然后,LifecycleBoundObserver 的 shouldBeActive() 方法就限制了只有當 Lifecycle 的當前狀態是 STARTED 或者 RESUMED 時才進行數據回調,那么我們只要改變此限制條件,就可以增大 Observer 的有效生命周期范圍了

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }

    @Override
    boolean shouldBeActive() {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

}

七、引入依賴

EventLiveData 已托管到 jitpack,可以直接遠程依賴。GitHub 地址:https://github.com/leavesCZY/EventLiveData

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    implementation 'com.github.leavesCZY:EventLiveData:1.0.2'
}

八、參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • LiveData是一種可觀察的數據存儲器類。與常規的可觀察類不同,LiveData 具有生命周期感知能力,意指它遵...
    tse1y閱讀 3,067評論 0 2
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學已經沒多少時間了。班主任說已經安排了三個家長分享經驗。 放學鈴聲...
    飄雪兒5閱讀 7,539評論 16 22
  • 今天感恩節哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉變要...
    迷月閃星情閱讀 10,590評論 0 11
  • 可愛進取,孤獨成精。努力飛翔,天堂翱翔。戰爭美好,孤獨進取。膽大飛翔,成就輝煌。努力進取,遙望,和諧家園。可愛游走...
    趙原野閱讀 2,750評論 1 1
  • 在妖界我有個名頭叫胡百曉,無論是何事,只要找到胡百曉即可有解決的辦法。因為是只狐貍大家以訛傳訛叫我“傾城百曉”,...
    貓九0110閱讀 3,299評論 7 3