手寫訊息匯流排LiveDataBus

你的使用者名稱發表於2019-01-28

Android四大元件和執行緒間通訊方式有很多,比如Handler管道、廣播、介面回撥、rxBus、EventBus等,但是這些方式都存在一些瑕疵,具體的優缺點如下:

手寫訊息匯流排LiveDataBus

那麼有沒有一種通訊方式可以集以上所有框架的優點於一身,並且避免以上缺點呢?答案就是作者今天要分享的livedatabus,livedatabus是基於原生的livedata實現的通訊框架,它擁有以下的優點:

手寫訊息匯流排LiveDataBus

首先我們來看一下LiveDataBus的整體架構,訊息匯流排用來儲存所有的訊息通道,然後訂閱者訂閱其中任意的通道,釋出者向通道釋出訊息

手寫訊息匯流排LiveDataBus


LiveDataBus核心原理是什麼?

LiveDataBus原理其實就是釋出-訂閱模式+liveData,接下來作者會一一道來。首先說說釋出-訂閱模式,這個模式和觀察者模式有些類似,甚至在有的設計模式書籍裡也認為這2個模式是等同的。我個人覺得仔細分析的話還是有一些不同的地方,最大的地方在於在觀察者模式中觀察者和被觀察者是互相知道對方的,但是釋出-訂閱模式中訂閱者並不知道釋出者是誰。所以在需要對二者進行解耦時最好使用釋出-訂閱模式,釋出者不需要知道訂閱者的存在,二者只是共用一個資訊通道。一般是在單執行緒中使用觀察者模式,但是如果是在不同執行緒中通訊就用釋出-訂閱模式會更適合。

觀察者模式和釋出-訂閱者模式對比

觀察者模式:

手寫訊息匯流排LiveDataBus

釋出-訂閱者模式:

手寫訊息匯流排LiveDataBus

在客車裡乘客相當於觀察者,上車時乘務員通過買票知道了每位乘客到站資訊,所以乘客只需要時刻觀察乘務員的指示,當到站點時乘務員會給出當面指示,到站點的乘客可以根據乘務員的當面提示執行下車操作。但是在火車上乘務員不可能一個一個人去通知到站,只能通過傳送廣播的方式,而且乘客並不知道是哪個乘務員傳送的廣播。

接下來說說LiveData,首先看一下LiveData的定義:

LiveData是一個資料持有類,持有資料並且這個資料可以被觀察被監聽,和其他Observable(被觀察者)不同的是,它是和Lifecycle繫結的,在生命週期內使用有效,減少記憶體洩漏和引用問題。

適合的使用場景這裡舉個例子,我們通過網路下載資料需要耗時一般放在子執行緒中,但是並不知道什麼時候會下載完成,如果我們不使用LiveData,那麼就有可能出現等資料回來時主執行緒的介面已經被銷燬的情況,這樣就有可能出現問題了。這裡如果使用LiveData,就不需要管資料什麼時候回來,回來後介面是否存在了,因為LiveData是自帶生命週期監測的。

接下來我們來簡單使用一下LiveData

步驟一    獲取MutableLiveData物件

NameViewModel mModel = ViewModelProviders.of(this).get(NameViewModel.class);複製程式碼

這個獲取方式比較奇怪,首先NameViewModel是我們的一個自定義類,內容非常簡單,就是獲取到了一個系統的MutableLiveData物件而已,不過需要注意的是這個類一定要繼承ViewModel,不然是要報錯的,獲取不到MutableLiveData,具體程式碼如下:

public class NameViewModel extends ViewModel {    
       private MutableLiveData<String> mCurrentName;    
       public MutableLiveData<String> getCurrentName() {        
              if (mCurrentName == null) {            
                       mCurrentName = new MutableLiveData<>();        
              }       
               return mCurrentName;    
       }
}複製程式碼

步驟二    新建觀察者類

final Observer<String> nameObserver = new Observer<String>() {   
     @Override    
     public void onChanged(String s) {        
           nameText.setText(s);    
    }
};複製程式碼

步驟三    將觀察者類傳入LiveData

mModel.getCurrentName().observe(this, nameObserver);複製程式碼

注意,這裡的mModel.getCuttentName其實就是MutableLiveData物件,這個this就是當前activity的引用,也就是說將當前activity引用傳入了observe方法,這個其實就是LiveData能監測到當前Activity生命週期的原因所在,具體怎麼監測下面會詳細講到。

步驟四    傳送訊息給觀察者

mModel.getCurrentName().postValue(anotherName);複製程式碼

注意,這裡的anotherName是在NameViewModel中設定好的泛型,詳見第一步中MutableLiveData的物件獲取,指定了anotherName只能傳遞String過去。另外postValue方法是使用在非同步執行緒中,setValue使用在主執行緒中,都是傳送訊息。

LiveData是如何做到監測頁面的生命週期的?

這個就必須從原始碼著手了,我們首先看一下將Activity傳進去的MutableLiveData中的observe方法

手寫訊息匯流排LiveDataBus

發現在這個方法中,將Activity物件和觀察者物件傳入了LifecycleBoundObserver中,所以我們點進去看一下LifecycleBoundObserver是一個什麼樣的類,然後它接收到了這2個物件以後都做了一些什麼操作

手寫訊息匯流排LiveDataBus

我們看到LifecycleBoundObserver實現了GenericLifecycleObserver,然後GenericLifecycleObserver又繼承了LifecycleObserver,而這個類正是系統檢測頁面生命週期改變相關的類。根據lifecycle的用法,實現了LifecycleObserver並且將觀察者傳入就可以在生命週期改變時通知該觀察者。另外在其中我們發現在onStateChanged中,如果當前頁面狀態是destroy的話,就移除我們的觀察者,這樣觀察者就收不到回撥了。

LiveData是不是就足夠解決業務中的問題了?

根據上面LiveData的基本使用,每更新一個控制元件就需要定義一個NameViewModel,因為需要不同的LiveData,原因是觀察者的介面回撥決定的,因為一個LiveData會執行一個onChange方法,但是一次只能帶來一個引數,所以不能讓所有的控制元件都獲取到想要的值,所以我們必須想辦法進行優化,那就是LiveDataBus。

LiveDataBus應該如何構建?

LiveDataBus其實就是用map儲存所有的LiveData,以唯一字串作為key,在使用的地方進行傳入key,獲取到map中儲存的MutableLiveData

手寫訊息匯流排LiveDataBus

但是,我們接下來做一個嘗試,在A頁面傳送訊息給B頁面,若此時B頁面還沒啟動。過一段時間後啟動B頁面還會收到訊息,這是不合理的,因為傳送訊息的時候B頁面還沒啟動,所以那個時候傳送的訊息不應該被收到。當然這裡如果想做得更好可以讓使用者進行設定,讓自定義的LiveDataBus支援粘性事件,這裡可以參考一個第三方的LiveEventBus的實現。這次我們主要講解一下如何通過hook技術取消這個粘性訊息的接收,即在頁面未開啟時,就算後面開啟了也不接收訊息。

想要解決這個問題就要從原始碼入手了,我們首先從呼叫的源頭MutableLiveData類中的setValue開始研究

手寫訊息匯流排LiveDataBus

我們看到,這個setValue其實就是呼叫了父類LiveData中的setValue,所以我們找到看下

手寫訊息匯流排LiveDataBus

可以看到這裡面呼叫了dispatchingValue,所以我們點進去看看

手寫訊息匯流排LiveDataBus

這裡面核心是condiderNotify方法,所以我們當然要進去看看了

手寫訊息匯流排LiveDataBus

最後一行是不是很眼熟?沒錯,這就是觀察者的介面回撥方法。大家要是不信可以反過來看也可以,首先到觀察者的介面回撥方法,然後find useages一樣可以看到是這個方法。那麼應該如何讓這個訊息第一次訂閱Livedata的時候,這個onChange方法不執行呢?這個就必須用到修改系統程式碼執行流程的hook技術了。

從上面程式碼可以看出,上面有3個判斷,只要其中有一個判斷執行了那麼都不會跑到最後的onChange方法,經過詳細分析這裡最好改的是第三個判斷。在第三個判斷中只要讓observer.mLastVersion >= mVersion就不會執行onChanged了,那麼應該如何讓這兩者符合要求呢?

首先看一下這個mLastVersion和mVersion是在哪裡賦值的,先看mLastVersion吧,mLastVersion賦值總共有3個地方,前兩個是將mLastVersion賦值成和mVersion相等,這個不用考慮,因為這就是我們想要的結果,最後那一次是賦值成一個變數,而且是在初始化的時候賦值的,這個地方是在private abstract修飾的一個內部類中,沒法進行修改。所以我們只能寄希望於mVersion身上了,我們看到mVersion的賦值處第一個是賦值為變數,這個是在LiveData的成員屬性中賦值的,在類載入的時候就會建立,這裡就算修改也會被mLastVersion複製過去,所以關鍵不在這裡。我們把目標看向mVersion++,沒錯就是這裡打破了二者的平衡,讓mVersion+1,最後的結果就是observer.mLastVersion<mVersion,導致那個判斷沒有進去,最後執行到了onChanged方法。那麼這個mVersion++是在哪裡執行的呢?這個是在setValue方法中執行的。所以經過分析,我們利用好給為初始化的頁面傳送訊息是先傳送後註冊這個特點。只需要在判斷observer.mLastVersion>=mVersion之前將二者賦值為相等即可,換句話說,我們需要在setValue後的某個地方將這二者賦值為相同即可。

手寫訊息匯流排LiveDataBus


手寫訊息匯流排LiveDataBus

mLastVersion是在observer物件中,而observer物件時considerNotify方法的引數傳進來的,而considerNotify方法是在dispatchingValue方法中呼叫的,進入dispatchingValue中可以看到,實際上傳下去的值是mObservers這個map中的值,也就是說我們只需要對當前頁面對應的Observer進行修改即可

手寫訊息匯流排LiveDataBus

修改的方式就是反射,首先拿到LiveData中的mObservers這個map,接下來獲取到當前頁面對應的Observer,然後呼叫其中的get方法獲取到Entry,然後呼叫set方法將其設定成mVersion的值,實際程式碼如下

手寫訊息匯流排LiveDataBus

核心原理:當進入一個新頁面時,會執行對observers的初始化,其中呼叫hook方法對mLastVersion進行修改,導致系統流程走不到onChanged方法。當再次發訊息時,由於已經初始化過了,所以不會走到hook方法,就是正常流程,mLastVersion值為-1,mVersion執行了++以後值變為了0,這樣就會走入onChange方法了,所以可以正常跑起來。

總結:本節我們分析了很多跨執行緒、頁面通訊的方法,總結了它們的優缺點,並且介紹了釋出-訂閱模式和觀察者模式的區別。經過對比很多通訊方式我們最終選擇了LiveDataBus,並且進行了模仿手寫,解決了其中發現的問題。總而言之,LiveDataBus是一個官方支援的高效率、無記憶體洩漏、簡單的優秀通訊框架。

相關文章