輕量級事件總線框架 - SwiftyEventBus

LOGO.png

前言

由于最近劃水的厲害,CodeReview竟然不知道拿什么代碼比較好,于是乎翻起了一年多以前寫的框架SwiftyEventBus來向全部門的同事謝罪。

在iOS開發(fā)中NSNotificationCenter一定是作為入門級別所要掌握的框架,然而當我們嘗試過其他技術平臺類似的框架,或者說對事件通知機制進行一定的思考之后,我們發(fā)現(xiàn)NSNotificationCenter這個陳年老框架實在是不能符合新時代的標準。更何況,Swift發(fā)布已然許久,Swift如此強大的類型系統(tǒng)NSNotificationCenter竟然不能受益半分,實在是讓人捶胸頓足,仰天長嘆。

于是乎,在偷窺了隔壁安卓的EventBus以及結合Swift本身的特性之后,嘗試構建了一套屬于Swift自己的EventBus,也就是上述所提到的SwiftyEventBus。當然,在開始著手構建框架之前我也在github上搜了一下類似的框架,發(fā)現(xiàn)了一款SwiftEventBus,這個項目的star有800之多,我的眼神瞬間暗淡了下來,“原來我的想法已經(jīng)被人實現(xiàn)了”,然而在我翻看完源碼之后發(fā)現(xiàn)事情并沒有那么簡單,原來這個項目僅僅只是對NSNotificationCenter的簡單100多行的Wapper!世上安能有如此“欺名盜世”之徒?!于是更加堅定了我自己做框架的決心,并且最后運行在自己公司的項目上。

再見,NSNotificationCenter

EventBus的核心概念就是事件的接收以及分發(fā),常用的場景是在App中跨組件的通信以及一對多的通知等。然而核心的概念雖然簡單,但是設計到的框架設計還是要考慮方方面面,比如事情如何接收,如何區(qū)分不同的事件,不同線程下事件的處理,事件的優(yōu)先級等等。在此之前我們先來看看NSNotificationCenter存在最主要的問題?

1. 事件區(qū)分

在NSNotificationCenter中,事件的區(qū)分是通過NotificationName來進行區(qū)分的,也就是說只有NotificationName相同才能在觀察所注冊到的地方進行方法調用,也就說我們每次進行NSNotificationCenter進行分發(fā)事件的時候都需要取一個符合規(guī)則的且不重復的名字。這里存在兩個問題:一,命名永遠是開發(fā)中最難以抉擇的事情,不命名或者少命名可以明顯提高開發(fā)的幸福感,更不用說蘋果所推崇的NotificationName的命名規(guī)則,諸如“com.apple.myapp.home.finish”之類的命名實在是冗長,而且對于新手來說極其容易造成硬編碼的散落,后期維護困難。二,所謂的不重復的名字也只是大多數(shù)情況的不重復,如果一旦使用不當選取了重復的事件名,那么就會造成非常難以排查的bug。

2. 事件分發(fā)

我們都知道,NSNotificationCenter的事件派發(fā)機制使用的是iOS開發(fā)中非常傳統(tǒng)的target-action的方法,這在傳統(tǒng)iOS開發(fā)中也是非常合乎常理的方式,然而在Swift發(fā)布之后這一切似乎不是那么合理。因為Swift并不是只能用于開發(fā)iOS,理論上我們也可以在Linux上運行我們的Swift項目,然而在這樣的環(huán)境之下是沒有OC的運行時,也就是說沒有target-action的,換句話說這樣的事件分發(fā)在這個情況下是缺失的。

3. 類型安全

在OC中我們對這樣的代碼會覺得非常的平常:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:MyName object:nil];
    [[NSNotificationCenter defaultCenter] postNotificationName:MyName object:nil userInfo:@{@"foo": @"foo-value"}];
}

- (void)handleNotification:(NSNotification *)notification {
    NSString *foo = [notification userInfo][@"foo"];
    NSLog(@"%@", foo);
}

然而這樣的代碼卻存在潛在的類型安全隱患,最主要的根源是,NSNotificationCenter是通過NSDictionary來傳遞信息的,而NSDictionary其實是NSDictionary<NSString, id>類型,如果發(fā)送方傳遞的不是字符串類型而是其他的類型,而接收方依舊按照字符串的類型來處理,那么很有可能程序就會發(fā)生crash。

4. 線程切換

NSNotificationCenter在設計上并沒有對線程切換做任何的考慮,但是在現(xiàn)實世界中這樣的使用場景非常常見,比如在某個子線程中發(fā)送通知,然后在主線程更新UI。我們都知道,NSNotificationCenter的收發(fā)通知都是在同一個線程的,也就是說對于新手來說,很容易發(fā)生在子線程發(fā)送通知,但是忘記了在處理通知的時候將操作切換為主隊列,從而造成相關的問題。

你好,SwiftyEventBus

SwiftyEventBus旨在解決上述的問題,并且引入了對于事件分發(fā)的高級操作,以下是SwiftyEventBus最簡單的一個例子:

class DemoViewController: UIViewController {
    var ob: EventSubscription<String>!
    override func viewDidLoad() {
        super.viewDidLoad()
        ob = EventBus.`default`.register { (x: String) in
            print(x)
        }
    }
}

// anywhere
EventBus.default.post("Foo")

1. 事件區(qū)分

首先從使用上來說更加方便使用了,我們不需要再為通知取名而費盡心思,只需要簡單的registerpost就可以完成整套的操作,當然在現(xiàn)實世界中不建議直接傳遞基本數(shù)據(jù)類型,這樣會損壞通知的表達能力,建議可以這么做:

struct DoSomeThingMessage {
    let foo: String
}

EventBus.`default`.register { (x: DoSomeThingMessage) in
    // handle DoSomeThingMessage
    print(x)
}

可能有人會說,啥玩意,這不就是把NSNotificationName的職責讓這個MessageWapper來做了么?有什么區(qū)別?最大的區(qū)別是,我們利用MessageWapper來實現(xiàn)消息類型的區(qū)分之后,可以利用Swift本身的類型系統(tǒng)來確保事件的唯一性以及在保持可讀性的前提之下規(guī)避了冗雜繁復的NSNotificationName。如果有人在同一個模塊內取名了同一個消息類型,那么編譯器可以告訴你發(fā)生了重復,這是原來字符串所做不到的事情嗎,而如果在不同的模塊內取名了同一個消息類型,那么由于是在不同的Swift模塊中定義的,因此這兩個消息類型也會被完美的區(qū)分開來。

2. 事件分發(fā)

SwiftyEventBus中,事件分發(fā)的方式采用了閉包的方式,這樣的好處是SwiftyEventBus可以在純Swift的環(huán)境之下運行,不會出現(xiàn)如果沒有OC運行環(huán)境而造成的通知機制的缺失。值得注意的是,在EventBus.default.register方法調用之后,會返回一個類型為EventSubscription的存根,它的作用是將自身觀察者的生命周期綁定在一個實例之上,如果這個實例被銷毀了,那么也就沒有再繼續(xù)觀察的必要了,這樣的好處是,我們不需要像NSNotification那樣顯示的調用remove方法來移除觀察者,雖然新版本的iOS已經(jīng)不需要顯示的移除,但是為了對老版本iOS的兼容,一般情況之下我們還是需要進行顯示的移除,這樣的設計也是非常的不友好的。

3. 類型安全

除此之外,由于閉包所接受的類型在編譯的時候已經(jīng)確定了,所以我們也不需要顯示的類型轉換,我們也可以保證代碼的類型安全,不會存在NSNotificationCenterid類型的困惑,這也是Swift所倡導的類型安全機制,SwiftyEventBus充分的利用了這一點,即便在后期的代碼重構過程中變更了數(shù)據(jù)的類型,我們也可以在編譯期就提前發(fā)現(xiàn)問題,從而保障代碼的魯棒性。

4. 高級特性

SwiftEventBus還提供了一系列的高級特性:

1.線程切換
EventBus.default.register(on: .main, messageEvent: { (x: String) in
    /// do something with x
    print(x)
})

這樣我們無論在哪個線程進行消息的發(fā)送,我們總能在主線程進行相關的處理。

2.優(yōu)先級控制
EventBus.default.register(priority: .low, messageEvent: { (x: String) in
    /// do something with x
    print(x)
})

EventBus.default.register(priority: .high, messageEvent: { (x: String) in
    /// do something with x
    print(x)
})

對于上述的例子來說,如果發(fā)送了一個消息都將被兩個觀察者接收,那么優(yōu)先級高的觀察者將率先接收到數(shù)據(jù),處理完成之后才能輪到低優(yōu)先級的觀察者接受。

3.粘性事件(Sticky Event)

在安卓的EventBus中有一個非常常用的特性就是Sticky Event,在SwiftyEventBus中也實現(xiàn)了這一特性,簡單的說,EventBus會記錄下最近一次的事件,然后在觀察者進行訂閱的時候進行回放。

let bus = EventBus(domain: stickyFlag)
bus.stick.post("foo")
bus.stick.register(on: .main, priority: .default, messageEvent: { (x: String) in
    /// do something with x
    print(x)
}).release(by: self.box)

按照正常來說,如果在注冊觀察者之前進行消息的發(fā)送,那么在之后注冊觀察者的時候不會接收到相關的信息,但是由于是sticky事件,所以我們可以接收到這個事件,從而進行相關的處理。

4.Rx拓展

此外,如果你使用的是RxSwift,你也可以無縫的接入到Rx的世界中:

EventBus.default.rx.register(String.self)
    .subscribe(onNext: { (x) in
       expect(x).to(equal("foo"))
       done()
    })
    .disposed(by: bag!)

內存優(yōu)化

在最開始的實現(xiàn)版本中,使用的是原生的Dictionary來實現(xiàn)類型以及觀察者的對應,這樣簡單的實現(xiàn)固然是可以實現(xiàn)功能,但是如果對Swift原生的Dictionary有一定的了解的話,我們會發(fā)現(xiàn)這樣可能存在一些內存浪費的問題。

Swift中Dictionay的擴容

在Swift中,Dictionay的原生實現(xiàn)內部是采用了_HashTable這個數(shù)據(jù)結構,當這個_HashTable是可變數(shù)據(jù)類型的時候,一旦達到容量的閾值就會發(fā)生一次擴容操作,而擴容操作會進行大量的內存拷貝,此外,也會造成無意義的內存的占用,這種情況在大量的數(shù)據(jù)的情況之下更為明顯,因此我們可以進行一些內存上的優(yōu)化。

internal static func capacity(forScale scale: Int8) -> Int {
    let bucketCount = (1 as Int) &<< scale
    return Int(Double(bucketCount) * maxLoadFactor)
}

最后的優(yōu)化方案參考了JDK中的HashTable的方案,也就是說通過列表以及哈希表的方式來防止Swift中可變容器類型的內存突變,從而達到減少內存使用的目的,當然如果想更加進一步的優(yōu)化查找的性能,也可以使用紅黑樹。

總結

SwiftyEventBus主要滿足了我對事件總線分發(fā)機制在Swift中的幻想,當然可能有很多不成熟的地方,歡迎大家指正以及交流,項目的開源地址在這里。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,632評論 1 32
  • 項目到了一定階段會出現(xiàn)一種甜蜜的負擔:業(yè)務的不斷發(fā)展與人員的流動性越來越大,代碼維護與測試回歸流程越來越繁瑣。這個...
    fdacc6a1e764閱讀 3,330評論 0 6
  • EventBus源碼分析(一) EventBus官方介紹為一個為Android系統(tǒng)優(yōu)化的事件訂閱總線,它不僅可以很...
    蕉下孤客閱讀 4,090評論 4 42
  • 第二次世界大戰(zhàn)期間,德國有一家名不見經(jīng)傳的信托公司叫巴比納信托行,專為顧客保管貴重物品。 戰(zhàn)爭爆發(fā)后,人們紛紛取走...
    牛犁閱讀 1,981評論 36 46
  • 本不想寫這段特別囧的經(jīng)歷,但是考慮很久,覺得還是要記錄一下第一次誤機的深刻教訓! 狗血的事情通常都有著美好的開始!...
    健人姐姐閱讀 983評論 0 9

友情鏈接更多精彩內容