Android輕量級事件通訊方案

Horizon757發表於2018-11-03

開發過程中,總會遇到一些需要通訊的場景。
如果邏輯比較簡單,通過常規的傳參,回撥,返回值等即可實現。
而如果呼叫層次較深(如跨模組,跨執行緒等),光靠傳參和回撥等手段,耦合度較高,
對於需要主動通知,通知多個元件等場景,更是捉襟見肘。
為解耦事件的釋出與訂閱主體,簡化元件間通訊,可引入事件通訊機制。

事件通知包含哪些內容?
事件的定義,註冊/登出,通知。
事件框架如何實現?
一個介面,一個事件管理類,足矣。
程式碼不足50行,名副其實的“輕量級”。

方案實現

定義介面
interface Observer {
    fun onEvent(event: Int, vararg args : Any?)
    fun listEvents(): IntArray
}

介面定義了兩個方法:
listEvents: 返回關注的事件;
onEvent: 發生事件時回撥此介面並返回事件和引數(可預設)。

事件管理
object EventManager {
    private val HANDLER = Handler(Looper.getMainLooper())
    private val OBSERVER_ARRAY = SparseArray<LinkedList<Observer>>(16)

    @Synchronized
    fun register(observer: Observer?) {
        observer?.listEvents()?.forEach { event ->
            var observerList = OBSERVER_ARRAY.get(event)
            if (observerList == null) {
                observerList = LinkedList()
                OBSERVER_ARRAY.put(event, observerList)
            }
            if (observer !in observerList) {
                observerList.add(observer)
            }
        }
    }

    @Synchronized
    fun unregister(observer: Observer?) {
        observer?.listEvents()?.forEach { event ->
            OBSERVER_ARRAY.get(event)?.removeLastOccurrence(observer)
        }
    }

    @Synchronized
    fun notify(event: Int, vararg args: Any?) {
        OBSERVER_ARRAY.get(event)?.forEach { observer ->
            HANDLER.post { observer.onEvent(event, *args) }
        }
    }
}

HANDLER:事件通知器,使事件介面統一在UI執行緒回撥;
OBSERVER_ARRAY:關聯事件和觀察者的容器。
SparseArray> 等價於 Map>,其key為事件, value為關注此事件的觀察者。
register: 將Observer放入其所關注的事件對應的list;
notify: 遍歷指定事件對應的list, 回撥Observer的onEvent()。

如下圖所示,將其展開,是一個多對多的十字結構:

簡而言之,就是:一個事件可被多個觀察者關注,一個觀察者可關注多個事件。

使用方法

以一個簡單的登入/登出場景為例:

1、定義事件
object Events {
    const val LOGIN = 1
    const val LOGOUT = 2
}
2、註冊/登出
abstract class BaseActivity : AppCompatActivity(), Observer {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        EventManager.register(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        EventManager.unregister(this)
    }

    override fun onEvent(event: Int, vararg args: Any?) {
    }

    override fun listEvents(): IntArray {
        return IntArray(0)
    }
}

像 Activity 和 Fragment 這種有固定生命週期的類,可以在Base新增註冊和登出程式碼;
但並非每個子類都需要註冊事件,所以可以先實現空方法,需要註冊的子類自行過載即可。

3、訂閱事件&處理回撥
class MainActivity : BaseActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val switchAccountBtn: Button = findViewById(R.id.switch_account_btn)
        switchAccountBtn.setOnClickListener {
            if (!AccountManager.isLogin) {
                startActivity(LoginActivity::class.java)
            } else {
                AccountManager.logout()
            }
        }
    }

    private fun setViews(account: String?) {
        // update views
    }

    override fun listEvents(): IntArray {
        return intArrayOf(Events.LOGIN, Events.LOGOUT)
    }

    override fun onEvent(event: Int, vararg args: Any?) {
        when (event) {
            Events.LOGIN -> setViews(args[0] as String?)
            Events.LOGOUT -> setViews(null)
        }
    }
}

由於事件是在UI執行緒回撥,所以可以直接更新UI;若需要做耗時處理,需要另起執行緒。

4、傳送事件
object AccountManager {
    fun login(account: String, password: String) {
        // process login
        EventManager.notify(Events.LOGIN, account)
    }

    fun logout() {
        // process logout
        EventManager.notify(Events.LOGOUT)
    }
}

傳送事件可以只傳送事件,也可以同時攜帶引數。
引數型別,最初嘗試用Bundle, 但是Bundle傳參需要封裝和拆解;
後來換用vararg,所以可以新增任意引數,使用也相對方便。

原理分析

從各種事件框架,到系統廣播,到View的listener回撥,思路都是類似的,但有各種不同的形式,從各種各樣名字就可見一斑。
其中被對比的最多的是 觀察者模式事件監聽模式
若非要分類,該方案應該是後者。

作為對比,我們以 《JAVA與模式》之觀察者模式 中介紹的個例子為參考,其類圖如下:

其中,Subject為被觀察物件,有的地方也用Observable。
被觀察物件的狀態(state)變化改變時, attach到此物件的觀察者的update()會被回撥。

前面舉例的方案,結構圖如下:

兩者的共性在於,都是“訂閱->通知”的一種模式。
兩者的區別在於:一個關注物的變化,一個關注事的發生。

上面的例子中,大概流程如下:

整體還是比較簡單的,頁面建立時訂閱訊息,銷燬時取消訂閱;
從MainActivity到LoginActivity,再到AccountManager, 既跨頁面也跨執行緒,
但由於EventManager持有MainActivity引用,所以可以方便地通知。
不單是Activity, Fragment等UI元件,任何物件都可以通過實現Observer介面成為觀察者,然後通過EventManager訂閱自己感興趣的事件。

其他事項

生命週期

引用觀察者的是個靜態物件,所以觀察者生命週期結束時需要確保取消訂閱,以免記憶體洩漏。
如果觀察者也是靜態物件,則不用取消訂閱。
如果不確定物件生命週期結束前是否可以取消訂閱,可藉助弱應用來防止記憶體洩漏:

class WeakObserver(target: Observer) : Observer {
    private val reference: WeakReference<Observer> = WeakReference(target)
    private val events: IntArray = target.listEvents()

    override fun onEvent(event: Int, vararg args: Any?) {
        reference.get()?.onEvent(event, *args)
    }

    override fun listEvents(): IntArray {
        return events
    }
}

WeakObserver和WeakHandle類似,都是通過實現介面以及弱應用來進行包裝。

重複檢查

當事件定義變多後,有可能一個不小心,事件的value就重複了。

object Events {
    const val LOGIN = 1
    const val LOGOUT = 2
    // ...
    const val SOMETHING = 2
}

若重複定義,可能會相互干擾。
為此,可編寫單元測試檢查事件的value是否重複:

    fun testDuplicate() {
        val fields = Events::class.java.declaredFields
        val events = fields.filter { it.type == Int::class.java }
        val eventSet = events.map { it.getInt(Events::class.java) }.toSet()
        Assert.assertEquals(events.size, eventSet.size)
    }

如上定義, 單元測試會報錯:

junit.framework.AssertionFailedError: expected:<3> but was:<2>

檢視事件

通過“Find Usages”快捷鍵,可以檢視所有訂閱者和事件傳送的地方。

結語

說到事件框架,大家可能會想到EventBus。
相比而言,EventBus有粘性事件,可指定回撥執行緒等特性;
而此方案則是輕量,以及清晰的事件管理。

本文主要是介紹事件通訊的思想和一些實現技巧,旨在拋磚引入,歡迎各位讀者批評指正。

附上演示Demo, 感興趣的讀者可以下載回來Run一下。
github地址: LigntEvent


相關文章