帶你手擼一個Kotlin版的EventBus

前言

EventBus在前兩年用的人還是非常多的,它是由greenrobot 組織貢獻的,該組織還貢獻了GreenDao(目前不建議使用,建議使用官方的Room數據庫框架)。EventBus的功能很簡單,通過解耦發佈者和訂閱者簡化Android事件傳遞,簡單來說就是可以替代安卓傳統的Intent、Handler、Broadcast或接口函數,在Activity、Fragment、Service之間進行數據傳遞。但是後來出現了RxBus(依賴於RxJava和RxAndroid),只通過短短几十行代碼就撼動了EventBus江湖大哥的地位,可以好景不長,RxBus高興了沒幾天官方又有了JetPack中的LiveData,也是幾十行代碼就能實現,而且無需依賴第三方包,還跟隨生命週期,更加方便了。。。。但這些都不是本文的重點,雖然RxBus和LiveDataBus是目前首選,但EventBus統治了江湖這麼久肯定有它的過人之處,所以本文就來手擼一個EventBus,來扒一扒江湖大哥EventBus。

EventBus介紹

這是EventBus的Github地址:https://github.com/greenrobot/EventBus。

EventBus的優點有很多(現在來看也並不是優點):代碼簡潔,是一種發佈訂閱設計模式(觀察者設計模式),簡化了組件之間的通訊,分離了事件的發送者和接收者,而且可以隨意切換線程,避免了複雜的和易錯的依賴關係和生命週期問題。

大家應該都使用過EventBus,咱們使用的時候一般需要先寫好註冊和解註冊,然後定義好接收方法,在接收方法上寫好註解,在發送的地方通過Post方法將需要傳遞的對象傳遞出去,咱們剛纔定義的接收方法就可以接收到傳遞的對象,並且咱們可以在接收方法上通過註解來修改線程。

說了這麼多優點咱們來看一下EventBus的實現原理吧,先來看一下EventBus的原理圖吧:

  • EventBus底層採用的是註解和反射的方式來獲取訂閱方法信息(首先是註解獲取,若註解獲取不到,再用反射)
  • 當前訂閱者是添加到Eventbus 總的事件訂閱者的subscriptionByEventType集合中
  • 訂閱者所有訂閱的事件類型添加到typeBySubscriber 中,方便解註冊時,移除事件

開始實現

說了這麼多,該開始正文了。首先咱們模仿EventBus也來一個單例,Kotlin中實現單例非常簡單,直接用object關鍵字修飾類,那麼這個類就已經是最簡單的懶漢式的單例了,當然也可以加雙重檢查鎖等加鎖算法,這裏不做詳解。來看一下代碼吧:

object EventBus {}

簡單吧,太簡單了,接下來需要做的就是寫上咱們需要的幾個方法,想一下,平時咱們調用的時候一般只有三個方法:註冊、解註冊和發送方法,很明確,那就再來定義上這三個方法:

object EventBus {

    // 所有未解註冊的緩存
    private val cacheMap: MutableMap<Any, List<SubscribeMethod>> = HashMap()

    /**
     * 註冊
     * @param subscriber 註冊的Activity
     */
    fun register(subscriber: Any) {}

    /**
     * 發送消息
     * @param obj 具體消息
     */
    fun post(obj: Any) {}

    /**
     * 取消註冊
     * @param subscriber /
     */
    fun unRegister(subscriber: Any) { }

}

上面的代碼還定義了一個Map,這裏有一個小細節,我用的是MutableMap而不是Map,這是因爲MutableMap是可變的map,而Map不可變(List在使用時也一樣)。添加這個Map是爲了保存註冊了的類和類中的需要接收方法的集合,嗯,沒錯,SubscribeMethod就是訂閱方法的類,下面就看一下SubscribeMethod的代碼:

class SubscribeMethod(
    //註冊方法
    var method: Method,
    //線程類型
    var threadModel: ThreadModel,
    //參數類型
    var eventType: Class<*>
)

上面代碼就是Kotlin中實體類的寫法(還可以爲參數寫默認值,加了默認值的參數在構造類時就可以不進行傳遞),這樣就直接實現類get、set、toString方法,很簡單吧?

接下來就該完善上面寫的EventBus類中的方法了,先來思考一下注冊方法,註冊方法首先會去緩存中尋找是否存在,如果存在就證明已經註冊,則不做操作,如果不存在那麼就進行註冊,嗯,看看代碼吧:

    /**
     * 註冊
     * @param subscriber 註冊的Activity
     */
    fun register(subscriber: Any) {
        var subscribeMethods = cacheMap[subscriber]
        // 如果已經註冊,就不需要註冊
        if (subscribeMethods == null) {
            subscribeMethods = getSubscribeList(subscriber);
            cacheMap[subscriber] = subscribeMethods;
        }
    }

大家可以看到上面代碼中寫了一個getSubscribeList方法,這個方法就是通過傳進來的類來進行循環反射獲取裏面符合條件的方法(即有註解的接收方法),這裏要注意,循環是因爲有可能會將註冊與解註冊放在BaseActivity中,那麼就需要循環便利子類和父類,通過分析,可以得出以下代碼:

		private fun getSubscribeList(subscriber: Any): List<SubscribeMethod> {
        val list: MutableList<SubscribeMethod> = arrayListOf()
        var aClass = subscriber.javaClass
        while (aClass != null) {
            aClass = aClass.superclass as Class<Any>
        }
        return list
    }

通過上面代碼咱們已經獲取到了所有的註冊的類,但是這裏需要將系統的類給過濾掉,系統的類肯定不可能註冊EventBus啊,所以就有了下面代碼:

//判斷分類是在那個包下,(如果是系統的就不需要)
val name = aClass.name
if (name.startsWith("java.") ||
   name.startsWith("javax.") ||
   name.startsWith("android.") ||
   name.startsWith("androidx.")
) {
   break
}

過濾掉系統的類之後就需要判斷方法了,通過反射獲取到類中的所有方法,然後根據註解判斷是否爲咱們定義的接收方法,然後構造爲咱們剛纔定義的訂閱方法類並放入List中:

val declaredMethods = aClass.declaredMethods
            declaredMethods.forEach {
                val annotation = it.getAnnotation(Subscribe::class.java) ?: return@forEach
                //檢測是否合格
                val parameterTypes = it.parameterTypes
                if (parameterTypes.size != 1) {
                    throw RuntimeException("EventBus只能接收一個參數")
                }
                //符合要求
                val threadModel = annotation.threadModel

                val subscribeMethod = SubscribeMethod(
                    method = it,
                    threadModel = threadModel,
                    eventType = parameterTypes[0]
                )

                list.add(subscribeMethod)
            }

到這裏獲取類中的有註解咱們定義的方法就都獲取到了,直接返回一個List給上面的註冊方法,將List保存在緩存cacheMap中。

上面代碼中進行判斷是否有咱們定義的註解Subscribe,但是註解還沒有定義,在Java中定義註解是在interfac關鍵字前添加@符號,在kotlin中則不然,代碼如下:

@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class Subscribe(val threadModel: ThreadModel = ThreadModel.POSTING)

Target、Retention和Java中一樣,都是作用域和執行時間。大家肯定注意到上面有一個ThreadModel咱們還沒有定義,這個咱們放在後面再說(切換線程)。

註冊方法就寫完了,下面來寫一下解註冊,解註冊很簡單,如果Map中有對應的值,只需要將Map中對應的值remove掉即可,如果沒有,則無需操作:

    /**
     * 取消註冊
     * @param subscriber /
     */
    fun unRegister(subscriber: Any) {
        val list = cacheMap[subscriber]
        //如果獲取到
        if (list != null) {
            cacheMap.remove(subscriber)
        }

    }

接下來就該今天的核心代碼了,通過Post方法將參數傳遞給接收方法並執行。思路很簡單,直接在緩存中查找所有類,然後在循環中獲取添加了註解的方法, 然後根據參數類型來判斷方法是否應該接收事件:

    /**
     * 發送消息
     * @param obj 具體消息
     */
    fun post(obj: Any) {
        val keys = cacheMap.keys
        val iterator = keys.iterator()
        while (iterator.hasNext()) {
            // 拿到註冊類
            val next = iterator.next()
            //獲取類中所有添加註解的方法
            val list = cacheMap[next]
            list?.forEach {
                //判斷這個方法是否應該接收事件
                if (it.eventType.isAssignableFrom(obj::class.java)) {
                    //invoke需要執行的方法
                    invoke(it, next, obj)
                }
            }

        }
    }

上面代碼在上面都分析過了,裏面還有一個invoke方法需要來編寫,這個方法很簡單,就是來執行接收方法:

    /**
     * 執行接收消息方法
     * @param it 需要接收消息的方法
     * @param next 註冊類
     * @param obj 接收的參數(即post的參數)
     */
    private fun invoke(it: SubscribeMethod, next: Any, obj: Any) {
        val method = it.method
        method.invoke(next, obj)
    }

到這裏基本的EventBus功能就已經實現了,咱們可以在下面進行測試。

測試

測試就來個簡單的例子吧,只有兩個Activity,先來看第一個,第一個裏面只放一個TextView和一個Button,TextView用來顯示一會傳進來的值,Button用來跳轉到第二個Activity,在Activity中進行註冊與解註冊,來看一下代碼吧:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        EventBus.register(this)//註冊
        initView()
    }

    private fun initView() {
        btnJump.setOnClickListener {
            startActivity(Intent(this, TwoActivity::class.java))
        }
    }

    override fun onDestroy() {
        super.onDestroy()//解註冊
        EventBus.unRegister(this)
    }

}

還需要添加接收方法,別忘了添加註解:

    @Subscribe
    fun zhu(person: Person) {
        tvText.text = "name=${person.name}   age=${person.age}"
    }

第一個Activity就寫完了,下面來寫第二個,第二個更簡單,只有一個Button,用來Post一個對象:

class TwoActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_two)
        initView()
    }

    private fun initView() {
        btnSendMessage.setOnClickListener {
            EventBus.post(Person(name = "朱江",age = 23))
        }
    }
}

再來看一下Person類吧:

class Person(var name: String, var age: Int)

好了,萬事俱備,只欠運行,開整:
在這裏插入圖片描述
可以發現基本功能咱們已經實現了,但是還有瑕疵,咱們接着往下看。

擴展

基本功能是實現了,但是EventBus還有一個非常重要的功能—切換線程,咱們可以在接收方法中進行指定線程來執行,咱們現在並沒有實現。大家可以用上面的代碼進行測試,測試方法很簡單,直接把Post方法放在子線程中,然後在接收方法中彈一個吐司:

    private fun initView() {
        btnSendMessage.setOnClickListener {
            Thread {
                EventBus.post(Person(name = "朱江",age = 23))
            }.start()
        }
    }
    @Subscribe
    fun zhu(person: Person) {
        Toast.makeText(this,"name=${person.name}age=${person.age}",Toast.LENGTH_LONG).show()
    }

然後來看一下運行結果:
在這裏插入圖片描述
可以看到應用直接崩潰了,奔潰原因很簡單,因爲咱們沒做線程切換,Post的時候放在了子線程,但接收方法中做了更新UI操作,所以肯定會崩潰。那麼下面就來加一個線程切換吧。

上面代碼中也有提到,ThreadModel類,上面註解中有提到,這是一個枚舉類,裏面定義了一些需要的線程,直接來看代碼吧:

enum class ThreadModel {

    // 默認模式,無論post是在子線程或主線程,接收方法的線程爲post時的線程。
    // 不進行線程切換
    POSTING,

    // 主線程模式,無論post是在子線程或主線程,接收方法的線程都切換爲主線程。
    MAIN,

    // 主線程模式,無論post是在子線程或主線程,接收方法的線程都切換爲主線程。
    // 這個在EventBus源碼中與MAIN不同, 事件將一直排隊等待交付。這確保了post調用是非阻塞的。
    // 此處不做其他處理,直接按照主線程模式處理
    MAIN_ORDERED,

    // 子線程模式,無論post是在子線程或主線程,接收方法的線程都切換爲子線程。
    ASYNC

}

那麼接下來咱們需要思考一下線程切換該怎麼搞?其實線程切換隻是接收方法存在的線程,咱們其實只需要更改Post方法中的invoke的執行就可以了啊,說來就來:

when (it.threadModel) {
        ThreadModel.POSTING -> {
            //默認情況,不進行線程切換,post方法是什麼線程,接收方法就是什麼線程
            EventBus.invoke(it, next, obj)
        }
        // 接收方法在主線程執行的情況
        ThreadModel.MAIN, ThreadModel.MAIN_ORDERED -> {
            // Post方法在主線程執行的情況
            if (Looper.myLooper() == Looper.getMainLooper()) {
                EventBus.invoke(it, next, obj)
            } else {
                // 在子線程中接收,主線程中接收消息
                EventBus.handler.post { EventBus.invoke(it, next, obj) }
            }
        }
        //接收方法在子線程的情況
        ThreadModel.ASYNC -> {
            //Post方法在主線程的情況
            if (Looper.myLooper() == Looper.getMainLooper()) {
                EventBus.executorService.execute(Runnable {
                    EventBus.invoke(
                        it,
                        next,
                        obj
                    )
                })
            } else {
                //Post方法在子線程的情況
                EventBus.invoke(it, next, obj)
            }
        }
    }

上面代碼邏輯並不難,這裏簡單說一下吧,默認線程直接執行;主線程的話需要判斷當前線程是否爲主線程,如果是,直接執行,如果不是,通過Handler轉爲主線程再執行;子線程的話和主線程類似,不過需要將執行方法放入線程池,這樣就是子線程中執行了。

接下來修改一下剛纔的代碼,在接收方法中添加上更換爲主線程的註解:

    @Subscribe(threadModel = ThreadModel.MAIN)
    fun zhu(person: Person) {
        //tvText.text = "name=${person.name}   age=${person.age}"
        Toast.makeText(this,"name=${person.name}   age=${person.age}",Toast.LENGTH_LONG).show()
    }

再來運行看一下效果:
在這裏插入圖片描述

可以發現已經成功了,咱們也實現了線程切換的功能,使用方法和EventBus一樣,在註解中進行註明即可。

結尾

文章到這裏基本結束了,上面的代碼量其實並不多,大家可以進我的Github下載代碼並運行,可以試着切換註解的線程試試,最後放一個本文所有代碼的地址吧:https://github.com/zhujiang521/EventBus

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章