Android組件內核之間組件間通信方案(四)下篇


阿里P7Android高級架構進階視頻免費學習請點擊:https://space.bilibili.com/474380680
本篇文章將繼續從以下兩個內容來介紹通信方案:

  • [ViewModel 與 View 的通信]
  • [ EventBus源碼分析]

一、ViewModel 與 View 的通信

以一個用戶信息修改界面為例,在請求服務器之前,必須先校驗用戶數據,而 Presenter 或 ViewModel 的職責就是顯示和取消 Loading,以及將校驗或服務器的返回結果展示到界面上。此外,如果一個 Dialog 正在顯示,當配置變更后也應該恢復 Dialog。

 
7273107-811787ff545742b0.png
image

Presenter 和 ViewModel 不應持有 View 的引用。

在 MVP 架構中,我們經常需要定義一些契約類接口(Contract),View 實現 Contract.View 接口,Presenter 實現 Contract.Presenter 接口,在 Presenter 中不持有 Activity/Fragment 的引用,只持有 View 實例,這樣可以方便地調用 View 接口暴露的方法。
例如 EditProfileContract.kt

interface EditProfileContract {

    interface view {

        fun setProgress(show: Boolean)

        fun showEmptyFirstNameError()

        fun showEmptyLastNameError()
    }

    interface presenter {

        fun saveProfile(firstName: String, lastName: String, bio: String, email: String, city: City, gender: String)
    }
}

但是,在 MVVM 架構中,ViewModel 不再持有 View 的引用,而是通過 LiveDataRxJava 向 View 層暴露數據。一旦 View 訂閱了 ViewModel,它就開始接收數據更新。這看似很完美,但當 ViewModel 想要更新 View 狀態,比如顯示和取消 Loading,將數據校驗或服務器結果反饋到 UI 界面上,會變得非常困難。

解決方案

ViewModel 中的 LiveData 或 Observable 越少越好。因此我們最好找到一種方法,可以封裝需要傳遞給 View 層的數據和信息。在多數情況下,ViewModel 需要向 View 層暴露以下三種數據:

  • Data
  • Status
  • State
    下面將依次介紹。

Data

Data -- 就是需要在 View 上展示的內容,比如用戶信息的 User 實體類,或社交 Feed 流中的列表項。

val user = MutableLiveData<User>()
val feeds = MutableLiveData<List<Feed>>()

Status

Status -- 可以是任何僅需傳遞一次的信息,如校驗錯誤,網絡異常,或者服務器錯誤。
Status.Kt

enum class Status {
    SUCCESS,
    ERROR,
    NO_NETWORK,
    EMPTY_FIRST_NAME,
    EMPTY_LAST_NAME,
    EMPTY_CITY,
    INVALID_URI
}

LiveData 沒有提供任何開箱即用的方法,但在 Google 的官方示例中,有一個 SingleLiveEvent 的實現,可以解決這個問題。

一個生命周期感知的被觀察者,僅在訂閱后發送新的更新,常用於導航和 Snackbar 消息等事件。
這可以避免一些常見問題:在配置變更(如屏幕旋轉)期間,如果觀察者處於活動動態,SingleLiveEvent 將會發送更新事件。
它繼承於 MutableLiveData,是一個被觀察者,即使對外暴露了 SingleLiveEvent#setValue()SingleLiveEvent#call() 方法,
注意:只有一個觀察者會受到更新通知。

新建一個 SingleLiveEvent 用來向 View 層暴露 Status 數據。
EditProfileViewModel.Kt

private val status = SingleLiveEvent<Status>()

fun getStatus(): LiveData<Status> {
    return status
}

fun handleImage(intent: Intent?) {
    intent?.data?.let {
        avatar.value = it.toString()
    } ?: run { status.value = Status.INVALID_URI }
}

View 只關心 Status 數據,並根據不同的狀態或錯誤執行對應的邏輯。如下實例,我們能很方便地根據每個錯誤顯示不同的 Toast 或 Snackbar。
EditProfileFragment.Kt

viewModel.getStatus().observe(this, Observer { handleStatus(it) })

private fun handleStatus(status: Status?) {
    when (status) {
        Status.EMPTY_FIRST_NAME -> Toast.makeText(activity, "Please enter your first name!", Toast.LENGTH_SHORT).show()
        Status.EMPTY_LAST_NAME -> Toast.makeText(activity, "Please enter your last name", Toast.LENGTH_SHORT).show()
        Status.EMPTY_CITY -> Toast.makeText(activity, "Please choose your home city", Toast.LENGTH_SHORT).show()
        Status.INVALID_URI -> Toast.makeText(activity, "Unable to load the photo", Toast.LENGTH_SHORT).show()
        Status.SUCCESS -> {
            startActivity(HomeFragment.newIntent(activity))
            activity.finish()
        }
        else -> Toast.makeText(activity, "Something went wrong, please try again!", Toast.LENGTH_SHORT).show()
    }
}

State

State -- 即 UI 狀態,比如加載進度條和 Dialog 等,每次開始訂閱 ViewModel 的數據時,ViewModel 應該把這些 UI 狀態通知給 View 層。一種簡單的做法是,我們可以創建一個數據類來保存這些狀態。
EditProfileState.Kt

data class EditProfileState(
    var isProgressIndicatorShown: Boolean = false,
    var isCityDialogShown: Boolean = false,
    var isGenderDialogShown: Boolean = false)

然后在 ViewModel 中創建一個 MutableLiveData,用來包裝這個 EditProfileState。由於 ViewModel 只會暴露 LiveData 給 View 層,因此我們應該提供 setter 方法,便於 View 更新此狀態。
EditProfileViewModel.kt

private val state = MutableLiveData<EditProfileState>()

fun getState(): LiveData<EditProfileState> {
    return state
}

fun setProgressIndicator(isProgressIndicatorShown: Boolean) {
    state.value?.isProgressIndicatorShown = isProgressIndicatorShown
}

fun setCityDialogState(isCityDialogShown: Boolean) {
    state.value?.isCityDialogShown = isCityDialogShown
}

fun setGenderDialogState(isGenderDialogShown: Boolean) {
    state.value?.isGenderDialogShown = isGenderDialogShown
}

最后,根據上面的 State 狀態數據,決定 Dialog 的顯示和取消。
EditProfileFragment.Kt

viewModel.getState().observe(this, Observer { handleState(it) })

private fun handleState(state: EditProfileState?) {
    if (state?.isCityDialogShown == true) {
        showCitySelectionDialog()
        return
    }
    if (state?.isGenderDialogShown == true) {
        showGenderSelectionDialog()
        return
    }
}

二、EventBus源碼分析

2.1EventBus簡介

2.1.1EventBus

EventBus 是一個 Android 事件發布/訂閱框架,通過解耦發布者和訂閱者簡化Android 事件傳遞,這里的事件可以理解為消息,本文中統一稱為事件。事件傳遞既可用於 Android 四大組件間通訊,也可以用戶異步線程和主線程間通訊等等。

傳統的事件傳遞方式包括:Intent、Handler、BroadCastReceiver、Interface 回調,相比之下 EventBus 的優點是代碼簡潔,使用簡單,並將事件發布和訂閱充分解耦。可簡化 Activities, Fragments, Threads, Services 等組件間的消息傳遞,可替代Intent、Handler、BroadCast、接口等傳統方案,更快,代碼更小,50K 左右的 jar 包,代碼更優雅,徹底解耦。

2.1.2概念

事件(Event):又可稱為消息,本文中統一用事件表示。其實就是一個對象,可以是網絡請求返回的字符串,也可以是某個開關狀態等等。事件類型(EventType)指事件所屬的 Class。

事件分為一般事件和 Sticky 事件,相對於一般事件,Sticky 事件不同之處在於,當事件發布后,再有訂閱者開始訂閱該類型事件,依然能收到該類型事件最近一個Sticky事件。

訂閱者(Subscriber):訂閱某種事件類型的對象。當有發布者發布這類事件后,EventBus 會執行訂閱者的onEvent 函數,這個函數叫事件響應函數。訂閱者通過register 接口訂閱某個事件類型,unregister 接口退訂。訂閱者存在優先級,優先級高的訂閱者可以取消事件繼續向優先級低的訂閱者分發,默認所有訂閱者優先級都為0。

發布者(Publisher):發布某事件的對象,通過 post 接口發布事件。

2.1.3、訂閱者、發布者、EventBus 關系圖
EventBus 負責存儲訂閱者、事件相關信息,訂閱者和發布者都只和 EventBus 關聯。

 
19956127-4cd14714930c50c9.png
 

事件響應流程

訂閱者首先調用 EventBus的 register 接口訂閱某種類型的事件,當發布者通過 post 接口發布該類型的事件時,EventBus 執行調用者的事件響應函數。

 
19956127-b4fa47914f616261.png
 

2.2、EventBus優勢

2.2.1、對比Java監聽器接口(Listener Interfaces)

在Java中,特別是Android,一個常用的模式就是使用“監聽器(Listeners)”接口。在此模式中,一個實現了監聽器接口的類必須將自身注冊到它想要監聽的類中去。這就意味着監聽與被監聽之間屬於強關聯關系。這種關系就使得單元測試很難進行開展。

2.2.2、對比本地廣播管理器(LocalBroadcastManager)

另一項技術就是在組件間通過本地廣播管理器(LocalBroadcastManager)進行消息的發送與監聽。雖然這對於解耦有很好的幫助,但它的API不如EventBus那樣簡潔。此外,如果你不注意保持Intent extras類型的一致,它還可能引發潛在的運行時/類型檢測錯誤。

使用EventBus不僅使代碼變得清晰,而且增強了類型安全(type-safe)。當用Intent傳遞數據時,在編譯時並不能檢查出所設的extra類型與收到時的類型一致。所以一個很常見的錯誤便是你或者你團隊中的其他人改變了Intent所傳遞的數據,但忘記了對全部的接收器(receiver)進行更新。這種錯誤在編譯時是無法被發現的,只有在運行時才會發現問題。

而使用EventBus所傳遞的消息則是通過你所定義的Event類。由於接收者方法是直接與這些類實例打交道,所以所有的數據均可以進行類型檢查,這樣任何由於類型不一致所導致的錯誤都可以在編譯時刻被發現。

另外就是你的Event類可以定義成任何類型。通常會為了表示事件而顯式地創建明確命名的類,你也通過EventBus發送/接收任何類。通過這種方法,你就不必受限於那些只能添加到Intent extras中的簡單數據類型了。例如,你可以發送一個和ORM模型類實例,並且在接收端直接處理與ORM操作相關的類實例。

2.3 EventBus使用

2.3.1基本使用

 
19956127-d6d4c63c1842dc40.png
 

2.3.2具體案例

 
19956127-032e4050f5d49569.png
 
 
19956127-3389bf29cc43bdee.png
 
 
19956127-b773dc116a7b59a3.png
 

2.3.3 onEvent函數使用解析

前一篇給大家裝簡單演示了EventBus的onEventMainThread()函數的接收,其實EventBus還有另外有個不同的函數,他們分別是:

1、onEvent
2、onEventMainThread
3、onEventBackgroundThread
4、onEventAsync

這四種訂閱函數都是使用onEvent開頭的,它們的功能稍有不同,在介紹不同之前先介紹兩個概念:告知觀察者事件發生時通過EventBus.post函數實現,這個過程叫做事件的發布,觀察者被告知事件發生叫做事件的接收,是通過下面的訂閱函數實現的。
onEvent:如果使用onEvent作為訂閱函數,那么該事件在哪個線程發布出來的,onEvent就會在這個線程中運行,也就是說發布事件和接收事件線程在同一個線程。使用這個方法時,在onEvent方法中不能執行耗時操作,如果執行耗時操作容易導致事件分發延遲。
onEventMainThread****:如果使用onEventMainThread作為訂閱函數,那么不論事件是在哪個線程中發布出來的,onEventMainThread都會在UI線程中執行,接收事件就會在UI線程中運行,這個在Android中是非常有用的,因為在Android中只能在UI線程中更新UI,所以在onEvnetMainThread方法中是不能執行耗時操作的。
onEventBackground:如果使用onEventBackgrond作為訂閱函數,那么如果事件是在UI線程中發布出來的,那么onEventBackground就會在子線程中運行,如果事件本來就是子線程中發布出來的,那么onEventBackground函數直接在該子線程中執行。
onEventAsync****:使用這個函數作為訂閱函數,那么無論事件在哪個線程發布,都會創建新的子線程在執行onEventAsync.

2.3.4 EvetntBus3.0使用解析

注冊一般是在 onCreateonStart 里注冊,盡量不要在 onResume,可能出現多次注冊的情況,比如下面這個異常:

 
19956127-c23bbf8c3d3b9481.png
 

取消注冊 要寫到 onDestroy 方法里,不要寫到 onStop 里,有時會出現異常的哦

EventBus 3 和之前版本的 EventBus 不兼容,這里采用注解的方法來接收事件,四種注解 @Subscrible、@Subscrible(threadMode = ThreadMode.ASYNC)、@Subscribe(threadMode = ThreadMode.BACKGROUND)、@Subscribe(threadMode = ThreadMode.MAIN)分別對應之前的 onEvent()、onEventAsync()、onEventBackground()、onEventMainThread()。

EventBus 3 采用注解后,方法名沒有限制了,參數只有一個,和發送者 post 的參數對應配對,在未聲明 threadMode 時,默認的線程模式為 ThreadMode.POSTING,只有在該模式下才可以取消線程。

由於可在任何地方都可以 post 一個事件,那么在不同線程之間傳遞事件,比如在工作線程傳遞一個事件更新UI線程中的一個控件,則需要注意 threadMode 的切換。

如果遇到訂閱事件無法執行的情況,分析后發現是訂閱事件的 Activity 還未執行的原因。找到原因就好辦了,這時候就需要用到 postSticky。

 
19956127-f7d328612b217dda.png
 
 
19956127-c675d02fbc169029.png
 
 
19956127-a6d78e9d669d955c.png
 
 
19956127-ece200eefbd6fc13.png
 

2.4EventBus源碼解析

2.4.1 register

EventBus.getDefault().register(this); EventBus.getDefault()其實就是個單例,和我們傳統的getInstance一個意思:

 
19956127-5d48632003d378b4.png
 

使用了雙重判斷的方式,防止並發的問題,還能極大的提高效率。

register公布給我們使用的有4個:

 
19956127-ae6b3bac11526938.png
 
 
19956127-545ff29547339324.png
 

調用內部類SubscriberMethodFinder的findSubscriberMethods方法,傳入了subscriber 的class,以及methodName,返回一個List<SubscriberMethod>。

那么不用說,肯定是去遍歷該類內部所有方法,然后根據methodName去匹配,匹配成功的封裝成SubscriberMethod,最后返回一個List。

 
19956127-bcdda2b989d3e194.png
 
 
19956127-84921e4d0d78a29c.png
 

繼續回到register:

 
19956127-4419aa34459ded98.png
 
 
19956127-6dd9ba6d1c4fddca.png
 
 
19956127-44985598d2aecbcb.png
 

到此,我們register就介紹完了。

你只要記得一件事:掃描了所有的方法,把匹配的方法最終保存在subscriptionsByEventType(Map,key:eventType ;value:CopyOnWriteArrayList<Subscription> )中;

eventType是我們方法參數的Class,Subscription中則保存着subscriber, subscriberMethod(method, threadMode, eventType), priority;包含了執行改方法所需的一切。

2.4.2 post

 
19956127-f8a76cba411dc22a.png
 
 
19956127-7502fd1d15dccab3.png
 
 
19956127-08b534a1fa780fde.png
 
 
19956127-4a21157637480121.png
 

到此,我們完整的源碼分析就結束了,總結一下:register會把當前類中匹配的方法,存入一個map,而post會根據實參去map查找進行反射調用。分析這么久,一句話就說完了~~

其實不用發布者,訂閱者,事件,總線這幾個詞或許更好理解,以后大家問了EventBus,可以說,就是在一個單例內部維持着一個map對象存儲了一堆的方法;post無非就是根據參數去查找方法,進行反射調用。

 
19956127-73712f635d69fbf6.png
 
 
19956127-7cc2d137c3fd6722.png
 

 

 
19956127-583b8b39882058f7.png
 

 

阿里P7Android高級架構進階視頻免費學習請點擊:https://space.bilibili.com/474380680
參考https://www.cnblogs.com/fuyaozhishang/p/9944653.html
https://www.jianshu.com/p/36cb80026dc9


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM