通知发送线程和通知接收线程是一致的。
由此看来,如果当我们不是百分之百确认通知的发送队列是在主队列中时,我们最好加上如下代码从而对我们的UI进行处理。
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL),
dispatch_queue_get_label(dispatch_get_main_queue()) ) == 0) {
//UI处理
} else {
dispatch_async(dispatch_get_main_queue(), ^{
//UI处理
});
}
苹果之所以采取通知中心在同一个线程中post和转发同一消息这一策略,应该是出于线程安全的角度来考量的。
官方文档告诉我们,NSNotificationCenter是一个线程安全类,我们可以在多线程环境下使用同一个NSNotificationCenter对象而不需要加锁。
“重定向”,就是我们在Notification所在的默认线程中捕获这些分发的通知,然后将其重定向到指定的线程中。
NSNotificationQueue对象,而是一个数组),让这个队列去维护那些我们需要重定向的Notification。我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程,如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification。指定的线程在收到信号后,将Notification从队列中移除,并进行处理。所以为了避免通知因为线程阻塞,最好是主线程发通知,需要耗时操作的逻辑在接收到通知以后自己开线程处理。
同步or异步
同步:
[NotificationCenter defaultCenter] postNotification],这种方式是同步的,并且在哪个线程发,就在哪个线程收。
异步:
创建一个NSNotificationQueue队列(first in-first out),将定义的NSNotification放入其中,并为其指定三种状态之一:
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // 当runloop处于空闲状态时post
NSPostASAP = 2, // 当 当前runloop完成之后立即post
NSPostNow = 3 // 立即post,同步(为什么需要这种type,且看三.3)
};这样,将NSNotification放入queue,然后根据其type,NSNotificationQueue在合适的时机将其post到NSNotificationCenter。这样就完成了异步的需求。
当然,如果将type改成NSPostNow,则还是同步执行,效果和不用NSNotificationQueue相同。
3、Notification Queues的合成作用
NSNotificationQueue除了有异步通知的能力之外,也能对当前队列的通知根据NSNotificationCoalescing类型进行合成(即将几个合成一个)。
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 不合成
NSNotificationCoalescingOnName = 1, // 根据NSNotification的name字段进行合成
NSNotificationCoalescingOnSender = 2 // 根据NSNotification的object字段进行合成
};是否需要移除通知
可以验证,通过NSNotification的Category,我将[self _removeObserver:observer];注释掉了,意味着该方法已经被我截取了,我们再向该“移除通知未遂”的响应者observer发送通知,直接崩溃。当去除注释,正常运行,无需手动移除。
结论:如果iOS支持版本在iOS8以上,多数情况理论上可以不用移除通知,
自己实现NSNotification
它的基本实现思路并不复杂。我们要做的无非是“添加”、“发送”、“移除”三件事。
但是在具体实现中,还是有些比较麻烦的地方
首先,创建了一个YBNotificationCenter类,属性如下:
@property (class, strong) YBNotificationCenter *defaultCenter;
@property (strong) NSMutableDictionary *observersDic;
defaultCenter类属性不用说,它是唯一单例(具体实现看代码);observersDic即为用来存储添加通知相关信息的字典。
然后创建了一个YBObserverInfoModel类,属性如下:
@property (weak) id observer;
@property (strong) id observer_strong;
@property (strong) NSString *observerId;
@property (assign) SEL selector;
@property (weak) id object;
@property (copy) NSString *name;
@property (strong) NSOperationQueue *queue;
@property (copy) void(^block)(YBNotification *noti);
该类就是响应者信息存储模型类,也就是会放在上面observersDic字典内的元素。先回忆一下,当我们使用- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;或- (id<NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(YBNotification *note))block;方法时,这些配置的变量是不是在YBObserverInfoModel都有体现呢?
是的,添加通知的操作不过就是将我们需要配置的变量统统存储起来,但是注意几点:一是对observer和object不能强持有,否则其无法正常释放;二是对name属性最好使用copy修饰,保证其不会受外部干扰;三是observer_strong属性是在使用代码块回调的那个添加通知方法时,需要使用到的强引用属性;四是observerId属性会比较陌生,它的作用大家可以先不管,之后会有用处。
添加通知核心代码
- (void)addObserverInfo:(YBObserverInfoModel *)observerInfo {
//添加进observersDic
NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;
@synchronized(observersDic) {
NSString *key = (observerInfo.name && [observerInfo.name isKindOfClass:NSString.class]) ? observerInfo.name : key_observersDic_noContent;
if ([observersDic objectForKey:key]) {
NSMutableArray *tempArr = [observersDic objectForKey:key];
[tempArr addObject:observerInfo];
} else {
NSMutableArray *tempArr = [NSMutableArray array];
[tempArr addObject:observerInfo];
[observersDic setObject:tempArr forKey:key];
}
}
}
我们传入一个配置好的YBObserverInfoModel模型进入方法,构建一个树形结构,用传入的name作为key(如果name为空使用key_observersDic_noContent常量代替),把所有使用相同name的通知放进同一个数组作为value,并且添加了线程锁保证observersDic数据读写安全。
这么做的理由:在通知的整个功能体系中,“添加”、“发送”、“移除”哪一步对效率的要求最高?毫无疑问是“发送”的时候,我们通常使用- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject方法发送通知,aName参数将是我们找到对应通知的第一匹配点。如果我们将其它参数作为observersDic的key,我们发送通知的时候不得不遍历整个observersDic;而如上代码实现,发送通知的时候,直接就能通过key直接找到对应的通知信息了,有效降低了时间复杂度。
使用代码块回调通知方法的实现
- (id<NSObject>)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(YBNotification * _Nonnull))block {
if (!block) {
return nil;
}
YBObserverInfoModel *observerInfo = [YBObserverInfoModel new];
observerInfo.object = obj;
observerInfo.name = name;
observerInfo.queue = queue;
observerInfo.block = block;
NSObject *observer = [NSObject new];
observerInfo.observer_strong = observer;
observerInfo.observerId = [NSString stringWithFormat:@"%@", observer];
[self addObserverInfo:observerInfo];
return observer;
}
这里有个地方需要提出来谈谈,在使用系统的这个方法的时候,一经实验就能发现,不管我们强引用或者弱引用这个返回值id<NSObject>时,都能在业务类dealloc释放的时候有效的移除该通知。
由于使用该方法添加通知的时候不会传入observer参数,这里创建了一个observer,如果这里使用observerInfo.observer = observer;,而业务类没有强引用这个返回值observer,它将会自然释放。所以,这里做了一个特殊处理,让observerInfo实例强持有observer。
值得注意的是,外部如果强引用返回的id<NSObject>类型的observer,会造成observer无法及时的释放,但是这点内存我认为还是可以接受的,当然业务类使用弱引用该observer是最好的选择。
2、发送通知
和系统通知一样,同样创建了一个类YBNotification发送通知消息体,属性就我们熟悉的几个:
@property (copy) NSString *name;
@property (weak) id object;
@property (copy) NSDictionary *userInfo;
然后将<NSCopying, NSCoding>两个协议实现一下就好了,具体看demo。
发送通知核心代码
- (void)postNotification:(YBNotification *)notification {
if (!notification) {
return;
}
NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;
NSMutableArray *tempArr = [observersDic objectForKey:notification.name];
if (tempArr) {
[tempArr enumerateObjectsUsingBlock:^(YBObserverInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.block) {
if (obj.queue) {
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
obj.block(notification);
}];
NSOperationQueue *queue = obj.queue;
[queue addOperation:operation];
} else {
obj.block(notification);
}
} else {
if (!obj.object || obj.object == notification.object) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
obj.observer?[obj.observer performSelector:obj.selector withObject:notification]:nil;
#pragma clang diagnostic pop
}
}
}];
}
}
发送通知相对简单,只需要分清是使用代码块回调,还是通过执行SEL回调。在使用代码块回调时,如果传入了队列queue,就让该代码块在该队列中执行,否则正常执行。!obj.object || obj.object == notification.objectif语句中这个判断值得注意。
3、移除通知
移除通知本身简单,有些麻烦的是自动移除。先贴上移除代码:
- (void)removeObserverId:(NSString *)observerId name:(NSString *)aName object:(id)anObject {
if (!observerId) {
return;
}
NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;
@synchronized(observersDic) {
if (aName && [aName isKindOfClass:[NSString class]]) {
NSMutableArray *tempArr = [observersDic objectForKey:[aName mutableCopy]];
[self array_removeObserverId:observerId object:anObject array:tempArr];
} else {
[observersDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSMutableArray *obj, BOOL * _Nonnull stop) {
[self array_removeObserverId:observerId object:anObject array:obj];
}];
}
}
}
- (void)array_removeObserverId:(NSString *)observerId object:(id)anObject array:(NSMutableArray *)array {
@autoreleasepool {
[array.copy enumerateObjectsUsingBlock:^(YBObserverInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj.observerId isEqualToString:observerId] && (!anObject || anObject == obj.object)) {
[array removeObject:obj];
}
}];
}
}
所有移除通知的方法,最终落脚点都是在这里。
上面方法中,如果aName不是合理的,就需要遍历observersDic移除对应的通知;如果aName是合理的,就直接查找对应的数组移除内容。
下面方法中,由于array.copy造成了局部变量,而上级可能是一个遍历,所以得加上自动释放池避免内存泄露。
使用observerId属性移除通知,而不用observer响应者来直接比较移除:
还记得添加通知时YBObserverInfoModel类的@property (strong) NSString *observerId;属性么?在添加通知的时候,我将响应者的地址信息作为该属性的值(保证其唯一性):
observerInfo.observerId = [NSString stringWithFormat:@"%@", observer];
然后在移除的时候通过比较进行相应的操作。
实现自动移除通知(解释为何使用observerId移除通知而不用observer)
实现自动移除通知,思路是在响应者observer走dealloc的时候移除对应的通知,难点就是在ARC中是不允许对dealloc做继承和交换方法等操作的,所以我使用了一个缓兵之计——动态给observer添加一个属性,我们监听这个属性的dealloc方法移除对应的通知,代码如下:
- (void)addObserverInfo:(YBObserverInfoModel *)observerInfo {
//为observer关联一个释放监听器
id resultObserver = observerInfo.observer?observerInfo.observer:observerInfo.observer_strong;
if (!resultObserver) {
return;
}
YBObserverMonitor *monitor = [YBObserverMonitor new];
monitor.observerId = observerInfo.observerId;
const char *keyOfmonitor = [[NSString stringWithFormat:@"%@", monitor] UTF8String];
objc_setAssociatedObject(resultObserver, keyOfmonitor, monitor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//添加进observersDic
NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;
@synchronized(observersDic) {
NSString *key = (observerInfo.name && [observerInfo.name isKindOfClass:NSString.class]) ? observerInfo.name : key_observersDic_noContent;
if ([observersDic objectForKey:key]) {
NSMutableArray *tempArr = [observersDic objectForKey:key];
[tempArr addObject:observerInfo];
} else {
NSMutableArray *tempArr = [NSMutableArray array];
[tempArr addObject:observerInfo];
[observersDic setObject:tempArr forKey:key];
}
}
}
只不过在添加通知到observersDic之前,添加一个monitor实例,使用objc_setAssociatedObject动态关联方法给resultObserver添加一个强引用的属性,注意objc_setAssociatedObject方法的第二个参数必须保证其唯一性,因为同一个响应者可能添加多个通知。
好了,现在基本工作都完成了,只需要在这个YBObserverMonitor方法中做简单的移除逻辑就OK了,代码如下:
//监听响应者释放类
@interface YBObserverMonitor : NSObject
@property (strong) NSString *observerId;
@end
@implementation YBObserverMonitor
- (void)dealloc {
NSLog(@"%@ dealloc", self);
[YBNotificationCenter.defaultCenter removeObserverId:self.observerId];
}
@end
机智的你发现了什么了么?
类释放的顺序是先自己释放然后其属性释放,也就是说理论上在走YBObserverMonitor的 dealloc时,observer响应者对象已经释放了。
这就是为什么不直接使用observer响应者对象对比做释放操作
关于实现部分,虽然我做了个大致的测试,可能还是会存在一些潜在的问题