NSNotification
NSNotificationCenter是Cocoa上实现观察者模式的中间桥梁,NSNotification抽象了观察者和被观察者之间的通信。因为观察者和被观察者没有类型限制的广播性,所以一般作为程序中用来解耦模块之间纵深通信的方法(MVC中主要用作M向C通信)。NSNotificationCenter是线程安全的,意味着可以在多线程中不用担心同步问题而轻松访问和使用,例如注册观察者,移除观察者,发送与执行等。
添加观察者
// A
- (void)addObserver:(id)notificationObserver
selector:(SEL)notificationSelector
name:(NSString *)notificationName
object:(id)notificationSender
// B
- (id<NSObject>)addObserverForName:(NSString *)name
object:(id)obj
queue:(NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block
A是传统的方法,B是支持block之后的方法。需要注意的是,NSNotificationCenter不会retain方法A的观察者notificationObserver(目前看来也不是weak,我个人认为是为了更本质地暴露问题,而是unsafe_unretained类型的引用),但会retain方法B的返回值id
在涉及到block的方法B中,需要注意到不要产生循环引用的内存问题。因为block参数为返回的观察对象(__NSObserver类型)拥有,如果观察对象是成员变量而block中又引用了类本身,就构成了循环引用。
移除观察者
// A
- (void)removeObserver:(id)notificationObserver
name:(NSString *)notificationName
object:(id)notificationSender
// B
- (void)removeObserver:(id)
方法A移除了观察者、被观察者和通知名都匹配的映射关系,方法B移除了匹配观察者的所有映射关系。良好的编码习惯,就像allloc和dealloc,open和close,需要添加和移除配对出现。否则,当观察者已经无效而被观察者继续发送通知时,通知中心中还记录者映射关系(此时为野指针),调用方法A的观察者的处理方法就会crash;方法B虽然不会crash,但是block内部引用的对象永远不会释放。
方法A适合于在阶段性周期性的方法中调用,以避免不知情的情况下移除了可视范围外其他与对象关联的映射关系,即具体地移除自己控制的映射关系。方法B适合于在终极性一次性的方法中调用,这样可以确保清除与对象关联的映射关系;
通知与执行
- (void)postNotification:(NSNotification *)notification
- (void)postNotificationName:(NSString *)notificationName
object:(id)notificationSender
- (void)postNotificationName:(NSString *)notificationName
object:(id)notificationSender
userInfo:(NSDictionary *)userInfo
可以根据需要指定通知名(notificationName)、发送者(notificationSender)和额外信息(userInfo),所有这些信息封装在一个NSNotification中,由postNotification:发送,其中参数notification不能为空,否则会引发异常。添加观察者的方法A和B都会同步地通知观察者并等待其执行完毕,只不过A在通知发生当前的线程中,而B则是在注册时指定队列的线程中。
但是,当有通知发送时,NSNotificationCenter是通过遍历所有的观察者映射关系,来找到每一个需要通知的观察者的。所以一旦注册到NSNotificationCenter的观察者过多,那么整个发送到响应通知的过程的效率就比较低,也不是什么需求都通过通知来解决,还是需要仔细思考适不适合。如果的确适合,也尽可能晚地注册,不再使用时也尽可能早地移除,这样使得内部的映射关系保持在最低量。
通知重定向
添加观察者的方法B已经很好地解决了这个问题,如果有需要尽量采用方法B,不过实现的原理还是值得研究一下的。下面的方法针对方法A(有可能方法B就是这么在NSNotificationCenter内部实现的)。Apple的官方文档这么说的:
For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.
思路是自定义一个缓存需要重定向NSNotification的集合(比如数组)。当通知来了时,先检查发送这个通知的线程是不是期望的线程,如果不是,则将这个通知缓存到集合中,并告诉期望的线程有通知需要处理。期望的线程得知后,从集合中移除通知,并进行处理。
@interface AppDelegate () <NSMachPortDelegate>
@property (nonatomic,readwrite,strong) NSMutableArray *notifications;
@property (nonatomic,readwrite,strong) NSThread *notificationThread;
@property (nonatomic,readwrite,strong) NSLock *notificationLock;
@property (nonatomic,readwrite,strong) NSMachPort *notificationPort;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSLog(@"current thread = %@", [NSThread currentThread]);
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
self.notificationPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:(__bridge NSString *)kCFRunLoopCommonModes];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"TestNotification" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:nil userInfo:nil];
});
}
- (void)handleMachMessage:(void *)msg {
[self.notificationLock lock];
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:notification];
[self.notificationLock lock];
};
[self.notificationLock unlock];
}
- (void)processNotification:(NSNotification *)notification {
if ([NSThread currentThread] != _notificationThread) {
[self.notificationLock lock];
[self.notifications addObject:notification];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
} else {
NSLog(@"current thread = %@", [NSThread currentThread]);
NSLog(@"process notification");
}
}
@end
多线程安全
NSNotificationCenter之所以在同一个线程中同步发送和执行通知,应该是Apple出于线程安全的原因,但是并不代表没有线程安全的问题。因为通知的发送和执行没有限制必须在某一个线程(例如主线程),所以在线程A执行通知的期间,会有线程B释放观察者的可能性。如果观察者的执行方法里访问了观察者自己,那么这种情况下就会因访问野指针而crash:
@interface Poster : NSObject
@end
@implementation Poster
- (instancetype)init {
self = [super init];
if (self){
[self performSelectorInBackground:@selector(postNotification) withObject:nil];
}
return self;
}
- (void)postNotification {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Test" object:nil];
}
@end
@interface Observer : NSObject {
Poster *_poster;
}
@property (nonatomic, assign) NSInteger i;
@end
@implementation Observer
- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"Test" object:nil];
_poster = [[Poster alloc] init];
}
return self;
}
- (void)handleNotification:(NSNotification *)notification {
NSLog(@"handle notification begin");
sleep(1);
self.i = 10;
NSLog(@"handle notification end");
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSLog(@"Observer dealloc");
}
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
__autoreleasing Observer *observer = [[Observer alloc] init];
}
@end
总之一句话,如何才能保证方法所有涉及到的对象,在方法执行期间有效?不过这类问题已经很典型了,在方法开始时确保所有对象的有效性后再执行,ARC下使用strong,MRC下使用retain+autorelease。今天写这篇文章的时候(XCode7.1.1),我检测了引用文章的例子,惊奇的是已经没有问题了。我认为Apple解决了这个问题,应该是在执行通知方法前,内部strong或者retain+autorelease观察者。这能做到通知方法执行期间至少不会产生观察者都无效,如果执行前观察者都已经无效就会直接crash。引用文的例子是方法A,我认为方法B本身就不会有问题,因为它是个block对象,内部strong了观察者,而__NSObserver必须通过NSNotificationCenter的removeObserver方法才能移除。