实现一个优雅的iOS事件总线

栏目: IOS · 发布时间: 5年前

内容简介:订阅登录事件LoginEvent,当self dealloc时候自动取消订阅订阅通知NSNotification,当self dealloc的时候自动取消订阅并且XCode可以自动推断类型

目标

订阅登录事件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<NSObject> 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层。

总线

总线本质上是”发布-订阅”这种消息范式:订阅者不关心消息由谁发送;接收者也不关系消息由谁接收。

总线是为了解决模块或者类之间消息通信而存在的,如果我们要实现一个总线,我们我们希望它能有哪些特点呢?

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

不需要手动取消监听

参数少,方法短,阅读起来一目了然

基于block的回调,降低上下文理解难度

兼容Notification

效率高

支持强类型/弱类型

定义事件

Notification用字符串来唯一事件,用一个类就代表了所有通知。而我们需要同时支持强类型和弱类型事件,怎么办呢?

用类名来区分事件,从而实现强类型:订阅者subscribe类名,发布者dispatch类。

用字符串eventType来对类事件进行二级划分,从而实现弱类型。

协议定义如下

@protocol QTEvent<NSObject>

@optional

- (NSString *)eventType;

@end

这样,我们就可以兼容Notification了

@interface NSNotification (QTEvent)<QTEvent>

@end

@implementation NSNotification (QTEvent)

- (NSString *)eventType{

    return self.name;

}

@end

然后强类型事件client自己定义类,弱类型事件可以采用框架提供的统一类,比如:

@interface QTJsonEvent : NSObject<QTEvent>

+ (instancetype)eventWithId:(NSString *)uniqueId jsonObject:(NSDictionary *)data;

@end

接口

由于我们的事件是用类来定义的,所以接口不难定义:

@interface QTEventBus : NSObject

- (...)on:(Class)eventClass; //订阅事件

- (void)dispatch:(id )event; //发布事件

@end

取消监听

手动取消

我们需要返回给client一个数据结构来取消监听,我们选择抽象的协议作为返回

@protocol QTEventToken<NSObject>

//取消监听

- (void)dispose;

@end

用协议作为返回值的好处是隐藏了内部的实现,这样内部实现就可以独立的变化,而对外透明。

然后内部创建一个具体的类,并且在dispose调用一个传入的block,在传入的block取消订阅

这是函数式的编程思想,把dispose抽象成一个传入的函数。

@interface _QTEventToken: NSObject<QTEventToken>

...

- (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;

}

效率

在分析效率之前,我们下来看看总线的数据模型:

一个ClassName对应着多个监听者: Name -> [subscribers],而总线维护着多个这种映射关系。

最直接想到的数据结构:字典嵌套数组,但是我们都知道数组删除一个元素的时候是需要额外的s时间消耗的,平均O(n)。

为了实现增加和删除效率是O(1),可以选择另外一种数据结构:双线链表。

所以,最后我们的数据结构是:字典 + 双向链表,这样我们在增加和删除元素的时间消耗都是O(1)的。

当然由于总线有可能在多个线程被调用,所以这个数据结构应该是线程安全的。

链式参数

我们来思考下注册Event的时候,有哪些变量:

回调block执行的队列: queue

和哪个对象的生命周期绑定在一起:object

事件的二级划分:eventType

回调的代码块:next

这四个变量除了next是必须的,其他的都是可选的。一种很笨的做法是穷举法:

[bus subscribeNext:]

[bus subscribeOnQueue:next:]

[bus subscribeOnQueue:freeWith:next]

...

这种复杂对象的创建,我们可以用一个工厂来一步步创建:

typedef void (^QTEventNextBlock)(Value event);

@interface QTEventSubscriberMaker<Value> : 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<Value> : 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三部曲:

定义事件

@interface QTLoginEvent : NSObject<QTEvent>

@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];

--------------------- 

作者:黄文臣 

原文:https://blog.csdn.net/Hello_Hwc/article/details/81023561 


以上所述就是小编给大家介绍的《实现一个优雅的iOS事件总线》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

高扩展性网站的50条原则

高扩展性网站的50条原则

[美] Martin L. Abbott、[美]Michael T. Fisher / 张欣、杨海玲 / 人民邮电出版社 / 2012-6-3 / 35.00元

《高扩展性网站的50条原则》给出了设计高扩展网站的50条原则,如不要过度设计、设计时就考虑扩展性、把方案简化3倍以上、减少DNS查找、尽可能减少对象等,每个原则都与不同的主题绑定在一起。大部分原则是面向技术的,只有少量原则解决的是与关键习惯和方法有关的问题,当然,每个原则都对构建可扩展的产品至关重要。 主要内容包括: 通过克隆、复制、分离功能和拆分数据集提高网站扩展性; 采用横向......一起来看看 《高扩展性网站的50条原则》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具