后端的啟發(fā) && 前端的尷尬
最近一直在看React Native的一些相關(guān)設(shè)計耗跛,對其Redux的設(shè)計模式很感興趣因苹。Redux其實是一種響應(yīng)式的設(shè)計寻馏,跟移動端中的MVVM有點類似兆龙,都是基于對狀態(tài)的監(jiān)聽和綁定驱闷。當然耻台,Redux跟MVVM還是有很大區(qū)別的,Redux的數(shù)據(jù)流是單向的空另,MVVM的不是盆耽。然而,無論是Redux還是MVVM扼菠,都離不開模塊間的消息傳遞摄杂,無論是傳遞數(shù)據(jù)還是傳遞變化狀態(tài)。對后端有一定了解的都知道循榆,后端架構(gòu)非常復雜析恢,其中就包含了消息分發(fā)的功能。
隨著移動端項目規(guī)模越來越大秧饮,模塊間狀態(tài)管理越來越復雜映挂,各個組件間通訊成本越來越高,如果還是采用傳統(tǒng)的Delegate,target-action,Notification,KVO來進行狀態(tài)管理盗尸,那么會使得狀態(tài)管理非常離散柑船,到處都是Delegate代碼。而對于全局狀態(tài)的管理泼各,Delegate就顯得力不從心了鞍时。所以,為了解決越來越高的組件間通訊成本历恐,需要引入一種類似于后端架構(gòu)中的消息分發(fā)器寸癌,用來做消息轉(zhuǎn)發(fā)的中介者,而不是組件和組件間的直接通訊弱贼。簡單說就是組件間通訊的統(tǒng)一管理。
怎么設(shè)計
在iOS中磷蛹,如何設(shè)計一個消息分發(fā)中心呢袄膏?首先要思考以下幾個問題:
- 一個消息體由什么組成
- 使用什么方式進行發(fā)送
- 如何對消息進行訂閱
- 分發(fā)中心如何分發(fā)消息
- 是否需要先注冊消息才能發(fā)送
- 消息太多是否會阻塞
- 如何減少對代碼的入侵
Dispatch Cener 的設(shè)計
一個消息體柑肴,除了要包含消息內(nèi)容外,還需要一個消息標識,作為消息的唯一標識赌朋,區(qū)分不同消息體。
YKMessage.h
@interface YKMessage : NSObject
/**
消息唯一標識
*/
@property (nonatomic, readonly) NSString *identify;
/**
消息內(nèi)容
*/
@property (nonatomic, readonly) id context;
/**
消息初始化函數(shù)
@param identify 消息唯一標識
@param context 內(nèi)容
@return 消息
*/
- (instancetype)initWithIdentify: (NSString *)identify context: (id)context;
YKMessage.m
@interface YKMessage()<NSCopying>
@property (nonatomic, readwrite) NSString *identify;
@property (nonatomic, readwrite) id context;
@end
@implementation YKMessage
- (instancetype)initWithIdentify: (NSString *)identify context: (id)context {
self = [super init];
if (self) {
_identify = [identify copy];
_context = [context copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
YKMessage *message = [[[self class]allocWithZone:zone]init];
message.identify = self.identify;
message.context = self.context;
return message;
}
消息的發(fā)送:首先構(gòu)造消息體审洞,然后通過分發(fā)中心進行發(fā)送
YKMessage *message = [[YKMessage alloc]initWithIdentify:@"test" context:@[@"1",@"2"]];
[[YKDispatchCenter shared]dispatchMessage:message];
在需要接收消息的地方訂閱消息:
[[YKDispatchCenter shared]subscribeWithBinder:self messageIdentify:@"test" handler:^(YKMessage *message, id ext) {
NSLog(@"dd");
}];
這里有人會好奇為什么要加入binder這個參數(shù)蚯瞧,綁定self。我解析一下:
每一個消息的訂閱者掂铐,都有自己的生命周期和作用域罕拂,當這個消息訂閱者被釋放后揍异,基于它的消息回調(diào)也應(yīng)該被釋放掉,不應(yīng)再被執(zhí)行爆班。因此回調(diào)是否被執(zhí)行衷掷,就要看綁定者是否被釋放了。
那傳入binder后柿菩,分發(fā)中心怎么知道binder是否已經(jīng)被釋放了戚嗅?
這里就要說一下一個弱應(yīng)用可變數(shù)組NSPointerArray
。
平時使用NSMutableArray
和NSArray
的時候枢舶,其數(shù)組元素是不能為一個空值的懦胞,這是因為當數(shù)組元素被add進去的時候,該元素就被數(shù)組持有凉泄,內(nèi)存引用計數(shù)會+1医瘫,而如果元素是空值的話,就會crash或報錯旧困。
但是醇份,使用弱引用數(shù)組就不會有這種問題。弱引用數(shù)組添加元素的時候吼具,會對元素進行一次弱引用僚纷,不會持有該元素,所以不會使元素的內(nèi)存引用發(fā)生變化拗盒,因此即使add進一個空值怖竭,也不會crash或報錯。
所以binder不會被消息分發(fā)中心持有陡蝇,當binder被回收后痊臭,消息中心持有的弱引用數(shù)組中的binder弱引用也會變成空值,在執(zhí)行回調(diào)前就可以通過這個來判斷回調(diào)是否應(yīng)該被執(zhí)行了登夫。
回到第四個問題:分發(fā)中心如何分發(fā)消息广匙?
- (void)dispatchMessage: (YKMessage *)message {
if (message == nil) {
return;
}
if (![self.registerDictionary.allKeys containsObject:message.identify]) {
return;
}
[self dealMessage:[message copy]];
}
- (void)dealMessage: (YKMessage *)message {
// 實現(xiàn)異步發(fā)送通知
dispatch_async(self.serialQueue, ^{
[[NSNotificationCenter defaultCenter]postNotificationName:message.identify object:message];
});
}
- (BOOL)registerMessageWithIdentify: (NSString *)messageIdentify {
if ([self.registerDictionary.allKeys containsObject:messageIdentify]) {
return NO;
}
[self.registerDictionary setValue:@"" forKey:messageIdentify];
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(observerHandler:) name:messageIdentify object:nil];
return YES;
}
- (BOOL)unRegisterMessageWithIdentify: (NSString *)messageIdentify {
if (![self.registerDictionary.allKeys containsObject:messageIdentify]) {
return NO;
}
[self.registerDictionary removeObjectForKey:messageIdentify];
[self.actionDictionary removeObjectForKey:messageIdentify];
[[NSNotificationCenter defaultCenter]removeObserver:self name:messageIdentify object:nil];
return YES;
}
消息分發(fā)中心是基于通知來實現(xiàn),在注冊的時候?qū)⑼ㄖ陌l(fā)送者和接收者都綁定到自身上恼策。通過發(fā)送通知和接收通知鸦致,來實現(xiàn)消息分發(fā)。
那為什么需要先注冊消息才能發(fā)送呢涣楷?
首先分唾,消息不是隨便發(fā)就能發(fā)的。例如支付模塊中狮斗,支付成功的消息必須是在支付模塊中注冊后才能發(fā)送绽乔,不能隨便哪個模塊就能直接發(fā)送支付成功的消息。在支付模塊加載后注冊支付成功的消息碳褒,在支付模塊卸載后反注冊支付成功消息折砸,這樣就能夠控制消息發(fā)送的權(quán)限了看疗。
其次,性能問題鞍爱。有注冊就有反注冊鹃觉,通過反注冊銷毀不需要維護的消息列表和通知觀察者,減少性能消耗睹逃。
再次盗扇,業(yè)務(wù)問題。比如在某種情況下沉填,不再需要某個消息了疗隶,所有這個消息的回調(diào)都不需要了。這時翼闹,通過反注冊斑鼻,就可以做到。
那消息太多是否會阻塞猎荠?
NSNotificationCenter在主線程中是同步的坚弱,當通知產(chǎn)生時,通知中心會一直等待所有觀察者都收到且處理通知完畢后关摇,才會返回發(fā)送通知的地方繼續(xù)執(zhí)行后面的代碼荒叶。通常來說,如果消息太多输虱,NSNotificationCenter會變慢些楣。然而,這里通過創(chuàng)建一個serialQueue串行隊列宪睹,并將消息的發(fā)送和接收放到這隊列中執(zhí)行愁茁,從而避免主隊列的阻塞等待。
- (void)dealMessage: (YKMessage *)message {
// 實現(xiàn)異步發(fā)送通知
dispatch_async(self.serialQueue, ^{
[[NSNotificationCenter defaultCenter]postNotificationName:message.identify object:message];
});
}
- (void)observerHandler: (NSNotification *)notification {
// 實現(xiàn)異步接收通知
dispatch_async(self.serialQueue, ^{
YKMessage *object = (YKMessage *)notification.object;
if (object != nil) {
NSString *messageIdentify = object.identify;
[self actionAndCleanWithMessageIdentify:messageIdentify message:object doHandler:YES];
}
});
}
如果消息實在太多亭病,還是會對性能有一定影響鹅很,但是這里對發(fā)送和接收通知進行異步操作,不會阻塞主線程命贴。
那如何減少對代碼的入侵道宅?
// 訂閱
[[YKDispatchCenter shared]subscribeWithBinder:self messageIdentify:@"test" handler:^(YKMessage *message, id ext) {
NSLog(@"dd");
}];
// 發(fā)送
YKMessage *message = [[YKMessage alloc]initWithIdentify:@"test" context:@[@"1",@"2"]];
[[YKDispatchCenter shared]dispatchMessage:message];
簡潔的API設(shè)計,簡單的使用胸蛛,是減少入侵和耦合的最好方式。
代碼
更多的問題
然而樱报,這個消息分發(fā)中心并不完善葬项,還有不少其他問題需要考慮:
- 如何做消息優(yōu)先級區(qū)分
- 消息發(fā)送失敗怎么辦,是否支持重發(fā)
- ......