NSNotification在iOS开发中常用到,使用起来很简单,但你是不是真的完全掌握了呢?上一篇文章主要讲多重代理的实现,那这篇文章就来看看NSNotification有哪些值得研究的东西。
NSNotification
通知封装了一个事件的信息,比如窗口获取了焦点或者网络连接关闭事件。需要知道一个事件的对象(例如,一个文件时,需要知道它的窗口即将关闭)注册通知中心,当这个事件发生时对象得到通知。当这个事件确实发生了,通知中心发出通知,立刻广播通知到所有注册的对象。或者,通知被放到通知队列中,这个通知延迟指定的通知并且根据指定的标准聚合类似的通知,然后会发布通知到通知中心。
NSNotification原理
对象间传递信息的标准方法是 消息传递――一个对象调用另一个对象中方法。但是消息传递机制中,发送消息的对象需要知道接收消息的对象以及接收对象要相应什么方法。有时,这种两个对象的紧密耦合是不可取的――尤其是两个原本独立的子系统因此联系起来。由于这些情况,引入了广播模型:一个对象发布通知,通知通过NSNotification对象被派到相应的观察者去,或者只是送到通知中心。一个NSNotification对象(称为通知)包含一个名字,一个对象,和一个可选的字典。名字是识别通知的标记。对象是任何发布通知的对象,典型的,就是发布通知本身的对象。字典包含了事件的额外信息。
MachPort向特定线程发送通知
以下是我们假定你已经掌握了MachPort的一些基本使用,如果你还没接触过,建议你看看我之前的文章深入理解RunLoop,里面有举例怎样使用MachPort配合RunLoop进行线程保活。
通常通知在通知被发布的线程上传递通知。分布式通知中心在主线程传递通知。有时候,你也许希望在你希望的线程上传递通知。例如,如果一个运行在后台的对象正在监听用户界面的通知,比如窗口关闭,这时候你希望在后台线程接收通知而不是在主线程。在这种情况下,你必须在默认线程截住通知,然后发送到合适的线程。
MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该MachPort对象发消息时会在MachPort所关联的线程中执行相关的代理方法。
为了实现这个技术,你的观察者对象需要以下值的实例变量:一个保存通知的可变数组,一个通知正确线程的MachPort,一个防止通知数组内多线程处理冲突的锁,一个标示正确线程(一个NSThread对象的值。你也需要方法去设置变量,去处理通知,去接收MachPort消息。
lock是对通知队列加锁,避免多个线程同时操作该队列所出现的数据不一致问题
/**
notifications 存储子线程发出的通知的队列
thread 处理通知事件的预期线程
lock 用于对通知队列加锁的锁对象,避免线程冲突
port 用于向期望线程发送信号的通信端口
*/
var notifications: [NSNotification]!
var thread: Thread!
var lock: NSLock!
var port: NSMachPort!
在注册通知之前,你需要初始化这些属性,下面的方法初始化了队列和锁对象,保持了一个对当前线程对象的引用,以及创建了一个MachPort,它将被添加到当前线程的RunLoop中。
/**
对相关的成员属性进行初始
*/
func setUpThreadingSupport() {
guard notifications == nil else { return }
notifications = [NSNotification]()
thread = Thread.current
lock = NSLock()
port = NSMachPort()
port.setDelegate(self)
RunLoop.current.add(port, forMode: .commonModes)
}
在这个方法运行之后,任何发送到notificationPort的消息将会在首先运行这个方法的线程的RunLoop中被收到。如果MachPort消息到达时,接收者线程的RunLoop没有运行,内核被保持这个消息直到下次RunLoop进入。接收者线程的RunLoop发送未到达的消息到端口代理的handleMachMessage
方法。
在这个实现中,发送到notificationPort的消息没有包含任何信息。 相反,通知数组包含线程间传递的信息。当Mach信息到达时,handleMachMessage
方法忽略了消息的内容,只是检查通知数组中需要处理的通知。通知从数组中移除,然后传到真正处理通知的方法中。因为如果同时发送太多端口信息,信息可能会丢失。handleMachMessage
方法会遍历数组,直到它为空。当访问通知数组时必须获取一个锁,以防止一个线程添加通知而另一个线程把通知从数组移除。
/**
从子线程收到MachPort发出的消息后所执行的方法
在该方法中从队列中获取子线程中发出的NSNotification
然后使用当前线程来处理该通知
RunLoop收到MacPort发出的消息时所执行的回调方法。
*/
func handleMachMessage(_ msg: UnsafeMutableRawPointer) {
lock.lock()
while notifications.count > 0 {
let notification = notifications[0]
notifications.remove(at: 0)
lock.unlock()
processNotification(notification)
lock.lock()
}
lock.unlock()
}
当通知传递给对象时,接收通知的方法必须确定它是否正在正确的线程中运行。 如果它是正确的线程,通知正常处理。 如果它是错误的线程,则将通知添加到队列中,并通知通知端口。
func processNotification(_ notification: NSNotification) {
if Thread.current == thread {
//处理出队列中的通知
}else{//将通知转发到正确的线程
lock.lock()
//将其他线程中发过来的通知不做处理,入队列暂存
notifications.append(notification)
lock.unlock()
//通过MachPort给处理通知的线程发送通知,使其处理队列中所暂存的队列
port.send(before: Date(), msgid: 100, components:nil, from: nil, reserved: 0)
}
}
最后,要注册想要在当前线程上传递的通知,无论它可能在哪个线程中发布,都必须通过调用setUpThreadingSupport
来初始化对象的通知属性,然后正常注册通知,指定特殊通知处理方法作为selector。
self.setUpThreadingSupport()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.processNotification(note:)), name: NSNotification.Name(rawValue: "NotificationName"), object: nil)
这个实现在几个方面是有限的:
- 由该对象处理的所有线程通知都必须通过相同的方法
processNotification
- 每个对象都必须提供自己的实现和通信端口
如果以后有时间并且能力所及,会尝试通过封装来改善这些缺陷
最后最后如果还不知道怎么使用,请看Demo DeliverNotificationToThread
参考文章
iOS开发之线程间的MachPort通信与子线程中的Notification转发
Delivering Notifications To Particular Threads