作者丨黃文臣; 
來源:https://blog.csdn.net/Hello_Hwc/article/details/81023561


實現一個優雅的iOS事件總線


目標


訂閱登錄事件LoginEvent,當self dealloc時候自動取消訂閱

[QTSub(self, LoginEvent) next:^(LoginEvent *event) {
}];


訂閱通知NSNotification,當self dealloc的時候自動取消訂閱

//訂閱通知name
[QTSubNoti(self,"name") next:^(NSNotification *event) {
}];
//訂閱App將要關閉
[[self subscribeAppWillTerminate] next:^(NSNotification *event) {
}];


並且XCode可以自動推斷類型


實現一個優雅的iOS事件總線



好了,開始囉裏八嗦講原理和設計了,做好準備,文章挺長的。不想看我囉嗦,代碼在這裏。

Notification的痛點

Cocoa Touch提供了一種消息中心機制:NSNotificationCenter,相信iOS開發者都很熟悉了,

addObserver 訂閱通知

postNotification 發送通知

removeObserver 取消訂閱

當然,還有一個接口是比較容易忽略的,就是利用block註冊訂閱

NSNotificationCenter * center = [NSNotificationCenter defaultCenter];
id token = [center addObserverForName:@"name"
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
}];
[center removeObserver:token]


實際開發中,Notification又有哪些痛點呢?

Name如何管理?

方式一:hardcode在代碼裏

[center addObserverForName:@"UserLoginNotification" ...]


優點:無需額外的import,鬆耦合。

缺點:修改和版本管理麻煩

方式二:在相關模塊的源文件裏,比如登錄成功的通知放在登錄模塊裏。

//.h文件
extern NSString * const UserLoginNotification; //登錄成功
//.m文件
NSString * const UserLoginNotification = @"UserLoginNotification";


優點:便於修改和版本管理

缺點:需要import引入對應的模塊,導致強耦合模塊,但是得到的卻是弱類型。

有些同學喜歡把name堆到一個頭文件裏,這種設計理念是不符合軟件設計原則的:“接口隔離原則,不應該強制客戶端依賴那些他們不需要的接口”。想想也有道理:我不需要的通知爲啥讓我引入進來對吧?。

弱類型

常見的用Notification傳遞消息的方式是UserInfo,然後聲明各種key

extern NSString * const kUserId; //用戶id


接收者取出信息

NSString * userId = [notification.userInfo objectForKey: kUserId];


缺點:必須看文檔或者源代碼才知道通知裏具體有什麼,字典是弱類型的,不易做接口的版本管理。

弱類型還有個明顯的劣勢就是無法在編譯期找到類型不匹配的問題。

優點:只要userInfo是JSON,就是鬆耦合的。

膠水代碼

使用Notificaton不得不寫很多膠水代碼

取消監聽,不然會crash

- (void)dealloc{
[center removeObserver:self];
}


取出通知內容

NSString * userId = [notification.userInfo objectForKey:@"userId"]



小結


總的來說,NotificationCenter的通信方式在完全鬆耦合的場景下是很適用的:發送者不用關心接收者,發送者和接受者統一按照JSON等協議通信。

而實際開發中,很多時候我們並不需要鬆耦合的通信。

業務層代碼的通信需要鬆耦合,因爲兩個業務通常是獨立開發迭代,通信按照指定協議即可,不可能開發的時候強制要import另一個業務代碼進來。

像登錄這種基礎服務代碼,本質上不屬於業務,開發的時候往往需要import對應的framework進來,這時候強類型的通信方式往往更好。

相信我,和基礎服務代碼通信的頻率要遠高於業務之間通信,甚至業務之間的通信很多時候也可以沉入到Service層。

實現一個優雅的iOS事件總線

總線

總線本質上是”發佈-訂閱”這種消息範式:訂閱者不關心消息由誰發送;接收者也不關係消息由誰接收。

總線是爲了解決模塊或者類之間消息通信而存在的,如果我們要實現一個總線,我們我們希望它能有哪些特點呢?

接口友好,接口友好,接口友好,重要的事情說三遍

不需要手動取消監聽

參數少,方法短,閱讀起來一目瞭然

基於block的回調,降低上下文理解難度

兼容Notification

效率高

支持強類型/弱類型

定義事件

Notification用字符串來唯一事件,用一個類就代表了所有通知。而我們需要同時支持強類型和弱類型事件,怎麼辦呢?

用類名來區分事件,從而實現強類型:訂閱者subscribe類名,發佈者dispatch類。

用字符串eventType來對類事件進行二級劃分,從而實現弱類型。

協議定義如下

@protocol QTEvent
@optional
- (NSString *)eventType;
@end


這樣,我們就可以兼容Notification了

@interface NSNotification (QTEvent)
@end
@implementation NSNotification (QTEvent)
- (NSString *)eventType{
return self.name;
}
@end


然後強類型事件client自己定義類,弱類型事件可以採用框架提供的統一類,比如:

@interface QTJsonEvent : NSObject
+ (instancetype)eventWithId:(NSString *)uniqueId jsonObject:(NSDictionary *)data;
@end


接口

由於我們的事件是用類來定義的,所以接口不難定義:

@interface QTEventBus : NSObject
- (...)on:(Class)eventClass; //訂閱事件
- (void)dispatch:(id)event; //發佈事件
@end



取消監聽


手動取消

我們需要返回給client一個數據結構來取消監聽,我們選擇抽象的協議作爲返回

@protocol QTEventToken
//取消監聽
- (void)dispose;
@end


用協議作爲返回值的好處是隱藏了內部的實現,這樣內部實現就可以獨立的變化,而對外透明。

然後內部創建一個具體的類,並且在dispose調用一個傳入的block,在傳入的block取消訂閱

這是函數式的編程思想,把dispose抽象成一個傳入的函數。

@interface _QTEventToken: NSObject
...
- (void)dispose{
@synchronized(self){
if (_isDisposed) {
return;
}
_isDisposed = YES;
}
if (self.onDispose) {
self.onDispose(self.uniqueId);
}
}


自動取消

如何實現自動取消訂閱呢?根據二八原則,我們來思考下百分之八十的情況下在什麼時候取消監聽?

在對象釋放的時候。

如果回調方式選擇target/action,可以選擇支持弱引用的集合(NSMapTable等)。但是我們設計的回調接口是基於block的,總線必須強持有這個block,所以就不能簡單的使用這些弱引用集合了。

那麼,如何知道一個對象被釋放了呢?

關聯對象。

由於一個對象可能多次調用,所以我們的關聯對象應該支持一次取消多個註冊。QTDisposeBag接收多個id,然後在dealloc的時候調用他們的dispose。

- (void)dealloc{
for (id token in self.tokens) {
if ([token respondsToSelector:@selector(dispose)]) {
[token dispose];
}
}
}


然後,用關聯對象的方式,綁定到指定對象上,這樣它的生命週期就和指定對象綁定在一起了

- (QTDisposeBag *)eb_disposeBag{
QTDisposeBag * bag = objc_getAssociatedObject(self, &event_bus_disposeContext);
if (!bag) {
bag = [[QTDisposeBag alloc] init];
objc_setAssociatedObject(self, &event_bus_disposeContext, bag, OBJC_ASSOCIATION_RETAIN);
}
return bag;
}


實現一個優雅的iOS事件總線

效率

在分析效率之前,我們下來看看總線的數據模型:

一個ClassName對應着多個監聽者: Name -> [subscribers],而總線維護着多個這種映射關係。

最直接想到的數據結構:字典嵌套數組,但是我們都知道數組刪除一個元素的時候是需要額外的s時間消耗的,平均O(n)。

爲了實現增加和刪除效率是O(1),可以選擇另外一種數據結構:雙線鏈表。

所以,最後我們的數據結構是:字典 + 雙向鏈表,這樣我們在增加和刪除元素的時間消耗都是O(1)的。

當然由於總線有可能在多個線程被調用,所以這個數據結構應該是線程安全的。

實現一個優雅的iOS事件總線

鏈式參數

我們來思考下注冊Event的時候,有哪些變量:

1.回調block執行的隊列: queue

2.和哪個對象的生命週期綁定在一起:object

3.事件的二級劃分:eventType

4.回調的代碼塊:next

這四個變量除了next是必須的,其他的都是可選的。一種很笨的做法是窮舉法:

[bus subscribeNext:]
[bus subscribeOnQueue:next:]
[bus subscribeOnQueue:freeWith:next]
...


這種複雜對象的創建,我們可以用一個工廠來一步步創建:

typedef void (^QTEventNextBlock)(Value event);
@interface QTEventSubscriberMaker : NSObject
- (id)next:(QTEventNextBlock)hander;
@property (readonly) QTEventSubscriberMaker *(^atQueue)(dispatch_queue_t);
@property (readonly) QTEventSubscriberMaker *(^ofType)(NSString *);
@property (readonly) QTEventSubscriberMaker *(^freeWith)(id);
@end


EventBus提供一個接口返回QTEventSubscriberMaker對象,讓client用組合的方式創建:

- (QTEventSubscriberMaker *(^)(Class eventClass))on{
//返回一個block,從而實現點語法
return ^QTEventSubscriberMaker *(Class eventClass){...};
}


接着就可以用點語法任意組合參數了:

bus.on(LoginEvent.class).atQueue(main).next(^(LoginEvent * event{
}));


簡化接口

我們的監聽要跟着某一個對象的生命週期走,這時候添加一個NSObject的Category,讓self成爲一個參數輸入能夠進一步簡化調用流程

@implementation NSObject (QTEventBus)
- (QTEventSubscriberMaker *)subscribe:(Class)eventClass{
return [QTEventBus shared].on(eventClass).freeWith(self);
}
@end


線程模型

事件的派發可以分爲兩個步驟:發送者dispatch,接收者回調block

設計回調的時候,有一些問題不得不考慮:那就是整個通信過程是同步還是異步的?都設計成異步的可以嗎?

當然不可以都設計成異步的,舉個簡單的例子:在某些事件的時候,你需要完成某些初始化工作,這些初始化工作未完成的時候,當前線程是不可以走下去的。

所以線程模型默認的設計成了同步,也就是說:發送方dispatch -> eventbus分發 -> 執行回調block這些都是同步的。

通過提供方法,來實現dispatch和回調block的異步

//在總線內部隊列上dispatch
- (void)dispatchOnBusQueue:(id)event;
//主線程異步回調
bus.on(LoginEvent.class).atQueue(main)


神奇的宏定義

爲了在編譯期支持強類型,所以被QTEventSubscriberMaker定義成了範型類型

@interface QTEventSubscriberMaker : NSObject
typedef void (^QTEventNextBlock)(Value event) NS_SWIFT_UNAVAILABLE("");
- (id)next:(QTEventNextBlock)hander;
@end


但是這就有一個問題,我必須這麼寫,XCode才能自動推斷出類型

QTEventSubscriberMaker * event = self.eventBus.on(QTMockIdEvent.class).ofType(_id).freeWith(self)
[event next:...]


毫無疑問這種接口是及其不友好的,並且這個代碼還有個大問題:代碼很長。

這時候一個強大工具可以幫助我們來解決這個問題:宏定義。

比如這樣的一個宏定義:

#define QTSub(_object_,_className_) ((QTEventSubscriberMaker<_classname_> *)[_object_ subscribe:[_className_ class]])



總結


QTEventBus三部曲:

https://github.com/LeoMobileDeveloper/QTEventBus

定義事件

@interface QTLoginEvent : NSObject
@property (copy, nonatomic) NSString * userId;
@end


訂閱事件

//注意eventBus會持有這個block,需要弱引用self
[QTSub(self,QTLoginEvent) next:^(QTLoginEvent * event) {
NSLog(@"%ld",event.userId);
}];


發佈事件

QTLoginEvent * event;
[QTEventBus.shared dispatch:event];
相关文章