We can understand how the notification center implements references to observers by looking at the notification implementation mechanism. Since Apple does not open source the Foundation source code, we will specifically refer to the source code implementation of GNUStep. The source code address of GNUStep is: GNUStep source code GitHub download address , the specific source code can be viewed.
The main process of notification
- The notification global object is a
NCTbl
structure named, which contains three important member variables, namely twoGSIMapTable
tables:named
,nameless
, and a singly linked listwildcard
.named
Is a table that stores notifications whose notification names are passed inhash
.nameless
object
It is a table that stores notifications that do not have a notification name but a message senderhash
.wildcard
Is a linked list that stores notifications that have neither incoming notification names nor incoming message senders.
- Every time we register a notification, the registered notification will be placed in the corresponding
NCTbl
structureGSIMapTable
according to these three typeswildcard
. - Then every time we send a notification (send a message), we first create an array to store all matching notifications
GSIArray
, and follow the following process to add qualified notifications to the arrayGSIArray
.- Get all
wildcard
notifications that match the criteria and add them to an arrayGSIArray
. nameless
Find notifications that match the criteria in the table and add them to the arrayGSIArray
.named
Find notifications that match the criteria in the table and add them to the arrayGSIArray
.
- Get all
- Finally, after all the notifications that meet the conditions are added, the entire array is traversed and the notification messages are sent
GSIArray
in sequence .performSelector:withObject
Notification principle
data structure
_GSIMapTable
The mapping table data structure diagram is as follows:
Related data structures:
_GSIMapTable
The mapping table containsnodeChunks
,bukets
,buketCount
,chunkCount
.nodeChunks
:nodeChunks
is aGSIMapNode
pointer to an array of pointers. It is used to manage dynamically allocated memory blocks that are used to store the nodes of the hash table (GSIMapNode
).bukets
: Record the number of nodes and the head address of each linked list in the singly linked list node pointer array.bucketCount
: Recordnode
the number of nodes.chunkCount
: Record the number of singly linked list node pointer arrays.nodeCount
: The number of nodes currently used in the hash table.
Define source code:
typedef struct _GSIMapBucket GSIMapBucket_t;
typedef struct _GSIMapNode GSIMapNode_t;
typedef GSIMapBucket_t *GSIMapBucket;
typedef GSIMapNode_t *GSIMapNode;
typedef struct _GSIMapTable GSIMapTable_t;
typedef GSIMapTable_t *GSIMapTable;
struct _GSIMapNode {
GSIMapNode nextInBucket; /* Linked list of bucket. */
GSIMapKey key;
#if GSI_MAP_HAS_VALUE
GSIMapVal value;
#endif
};
struct _GSIMapBucket {
uintptr_t nodeCount; /* Number of nodes in bucket. */
GSIMapNode firstNode; /* The linked list of nodes. */
};
struct _GSIMapTable {
NSZone *zone;
uintptr_t nodeCount; /* Number of used nodes in map. */
uintptr_t bucketCount; /* Number of buckets in map. */
GSIMapBucket buckets; /* Array of buckets. */
GSIMapNode freeNodes; /* List of unused nodes. */
uintptr_t chunkCount; /* Number of chunks in array. */
GSIMapNode *nodeChunks; /* Chunks of allocated memory. */
uintptr_t increment;
#ifdef GSI_MAP_EXTRA
GSI_MAP_EXTRA extra;
#endif
};
The specific code to add/delete from the mapping table is as follows:
GS_STATIC_INLINE GSIMapBucket
GSIMapPickBucket(unsigned hash, GSIMapBucket buckets, uintptr_t bucketCount)
{
return buckets + hash % bucketCount;
}
GS_STATIC_INLINE GSIMapBucket
GSIMapBucketForKey(GSIMapTable map, GSIMapKey key)
{
return GSIMapPickBucket(GSI_MAP_HASH(map, key),
map->buckets, map->bucketCount);
}
GS_STATIC_INLINE void
GSIMapLinkNodeIntoBucket(GSIMapBucket bucket, GSIMapNode node)
{
node->nextInBucket = bucket->firstNode;
bucket->firstNode = node;
}
GS_STATIC_INLINE void
GSIMapUnlinkNodeFromBucket(GSIMapBucket bucket, GSIMapNode node)
{
if (node == bucket->firstNode)
{
bucket->firstNode = node->nextInBucket;
}
else
{
GSIMapNode tmp = bucket->firstNode;
while (tmp->nextInBucket != node)
{
tmp = tmp->nextInBucket;
}
tmp->nextInBucket = node->nextInBucket;
}
node->nextInBucket = 0;
}
In fact, it is a hash table structure. You can get the first element of each singly linked list in the form of an array, and then use the linked list structure to add or delete.
The notification global object table structure is as follows:
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages*///获取所有消息
GSIMapTable nameless; /* Get messages for any name.*///获取任何名称的消息
GSIMapTable named; /* Getting named messages only.*///仅获取命名消息
unsigned lockCount; /* Count recursive operations. *///递归运算计数
NSRecursiveLock *_lock; /* Lock out other threads. *///锁定其他线程
Observation *freeList;
Observation **chunks;
unsigned numChunks;
GSIMapTable cache[CACHESIZE];
unsigned short chunkIndex;
unsigned short cacheIndex;
} NCTable;
The most important data structures are two GSIMapTable
tables: named
, nameless
, and singly linked listwildcard
named
hash
, a notification table that holds the name of the incoming notificationnameless
, save the table without passing in the notification name but passing in the messageobject
senderhash
wildcard
, holds a singly linked list with neither notification names nor incomingobject
notifications
Saving the notification table containing the notification name named
requires a registered object
object, so the table structure is passed in name
as key
. And it value
is also a GSIMapTable
table, which is used to store objects object
of corresponding objects observer
;
For the notification table that does not pass in the notification name but only object
the object , only the corresponding relationship between and nameless
needs to be saved , so it is used as .object
observer
object
key
observer
value
The specific core function of adding an observer (the block form is just a wrapper for this function) is roughly coded as follows:
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object
{
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;
//入参检查异常处理
...
//table加锁保持数据一致性
lockNCTable(TABLE);
//创建Observation对象包装相应的调用函数
o = obsNew(TABLE, selector, observer);
//处理存在通知名称的情况
if (name)
{
//table表中获取相应name的节点
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0)
{
//未找到相应的节点,则创建内部GSIMapTable表,以name作为key添加到talbe中
m = mapNew(TABLE);
name = [name copyWithZone: NSDefaultMallocZone()];
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
GS_CONSUMED(name)
}
else
{
//找到则直接获取相应的内部table
m = (GSIMapTable)n->value.ptr;
}
//内部table表中获取相应object对象作为key的节点
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0)
{
//不存在此节点,则直接添加observer对象到table中
o->next = ENDOBS;//单链表observer末尾指向ENDOBS
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
//存在此节点,则获取并将obsever添加到单链表observer中
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
//只有观察者对象情况
else if (object)
{
//获取对应object的table
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n == 0)
{
//未找到对应object key的节点,则直接添加observergnustep-base-1.25.0
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
//找到相应的节点则直接添加到链表中
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
//处理即没有通知名称也没有观察者对象的情况
else
{
//添加到单链表中
o->next = WILDCARD;
WILDCARD = o;
}
//解锁
unlockNCTable(TABLE);
}
The code for block form is as follows:
- (id) addObserverForName: (NSString *)name
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (GSNotificationBlock)block
{
GSNotificationObserver *observer =
[[GSNotificationObserver alloc] initWithQueue: queue block: block];
[self addObserver: observer
selector: @selector(didReceiveNotification:)
name: name
object: object];
return observer;
}
- (id) initWithQueue: (NSOperationQueue *)queue
block: (GSNotificationBlock)block
{
self = [super init];
if (self == nil)
return nil;
ASSIGN(_queue, queue);
_block = Block_copy(block);
return self;
}
- (void) didReceiveNotification: (NSNotification *)notif
{
if (_queue != nil)
{
GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc]
initWithNotification: notif block: _block];
[_queue addOperation: op];
}
else
{
CALL_BLOCK(_block, notif);
}
}
For block
the form of creating GSNotificationObserver
an object, the object will be Block_copy
copied block
and the notification operation queue will be determined. The notification reception processing function didReceiveNotification
is passed addOperation
to implement the specified operation queue processing, otherwise it will be executed directly block
.
The general logic of the core function for sending notifications is as follows:
- (void) _postAndRelease: (NSNotification*)notification
{
//入参检查校验
//创建存储所有匹配通知的数组GSIArray
//加锁table避免数据一致性问题
//获取所有WILDCARD中的通知并添加到数组中
//查找NAMELESS表中指定对应消息发送者对象object的通知并添加到数组中
//查找NAMED表中相应的通知并添加到数组中
//解锁table
//遍历整个数组并依次调用performSelector:withObject处理通知消息发送
//解锁table并释放资源
}
The focus of the above sending is to obtain all matching notifications and performSelector:withObject
send notification messages, so the threads that send notifications and receive notifications are the same thread (in block
the form of specifying queue processing by operating the queue).
sections in notifications
NSNotification
NSNotification
Contains some information about message sending, including name
message name, object
message sender, and userinfo
additional information carried by the message sender. Its class structure is as follows:
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@end
@interface NSNotification (NSNotificationCreation)
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*API_UNAVAILABLE(macos, ios, watchos, tvos)*/; /* do not invoke; not a valid initializer for this class */
@end
NSNotification objects can be constructed through instances or through classes. ( - (instancetype)init
This method is an initialization method, but in NSNotification
the class, it is marked API_UNAVAILABLE
as unavailable. Therefore, this method cannot be used directly to initialize NSNotification
the object.)
NSNotificationCenter
NSNotificationCenter
Message notification center, global singleton mode (each process has a default notification center by default for intra-process communication), obtain the notification center through the following method:
+ (NSNotificationCenter *)defaultCenter
For macOS systems, each process has a default distributed notification center NSDistributedNotificationCenter. For details, see: NSDistributedNotificationCenter .
The specific method of registering notification messages is as follows:
//注册观察者
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
The registered observer method provides two forms: selector
and block
, for the method of adding a specified observer object, observer
it cannot be nil
; block
the method will execute copy
the method, return the anonymous observer object used, and specify the operation object of the observer to process the message NSOperationQueue
.
The specified message name name
and sender object object
can both be empty, which means receiving all messages and messages sent by all sending objects; if one or both of them are specified, it means receiving messages from the specified message name and sender.
For the queue block
specified by the method , it will be processed in the thread that sends the message by default; if the main queue is specified, it will be processed by the main thread to avoid exceptions caused by performing UI operations.queue
nil
Note: Registering observer notification messages should avoid repeated registration, which will result in repeated processing of notification messages and block
holding external objects, so it is necessary to avoid causing circular reference problems.
The message sending method is as follows:
//发送消息
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
You can send a message through NSNotification
a packaged notification message object, or you can specify the message name, sender, and information to send. It is a synchronous execution mode. You need to wait for all registered observers to process the notification message before the method returns to continue. The notification object is executed in the queue specified by the registered message for the block mode, and the notification object block
is processed in the same thread for the non-method.
Note: The message sending type needs to be consistent with the type when registering. That is, if the registered observer specifies both the message name and the sender, the message name and sender must also be specified at the same time when sending the message, otherwise the message cannot be received.
The method to remove an observer is as follows:
//移除观察者
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
All notification messages of the specified observer can be removed, that is, the observer will no longer receive any messages. It is generally dealloc
called after the observer object is released, but there is no need to call manually after that, as iOS9
it has been automatically processed.macos10.11
dealloc
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method. Otherwise, you should call this method or removeObserver:name:object: before observer or any object specified in addObserverForName:object:queue:usingBlock: or addObserver:selector:name:object:is deallocated.
翻译如下:
如果你的应用目标是iOS 9.0及更高版本或macOS 10.11及更高版本,你不需要在dealloc方法中注销观察者。否则,您应该调用此方法或在取消分配addObserverForName:object:queue:usingBlock:或addObserver:selector:name:object:中指定的任何对象之前删除观察者:name:object:方法。
NSNotificationQueue
NSNotificationQueue
The notification queue implements the management of notification messages, such as message sending timing, message merging strategy, and manages messages in a first-in-first-out manner, but the actual message sending is still completed through the notification center NSNotificationCenter
.
@interface NSNotificationQueue : NSObject
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
You can defaultQueue
get the notification message queue bound to the current thread, or you can initWithNotificationCenter:
specify the notification management center. The specific message management strategy is as follows:
NSPostingStyle
: Used to configure when notifications are sent
NSPostASAP
: Send a notification when the current notification is called or the timer ends.NSPostWhenIdle
: Notify when runloop is idleNSPostNow
: Send notification immediately after merge notification is completed
NSNotificationCoalescing
(Note this is a NS_OPTIONS
): for configuring how notifications are merged:
NSNotificationNoCoalescing
:Notification of non-mergerNSNotificationCoalescingOnName
: Merge notifications according to notification namesNSNotificationCoalescingOnSender
: Merge notifications according to the incoming object
If NSNotificationQueue
the notification queue does not specify NSPostNow
the immediate sending mode, it can be runloop
sent asynchronously by
NSNotification and multithreading
NSNotification
The official documentation regarding multi-threading is as follows:
In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.
翻译如下:
在多线程应用程序中,通知始终在发布通知的线程中传递,该线程可能与观察者注册自身的线程不同。
That is to say, the sending and receiving processing of NSNotification are both in the same thread. For the block form, the receiving processing is processed in the specified queue. This has been explained above. Here we focus on how to process the receiving processing in other threads.
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.
翻译如下:
例如,如果在后台线程中运行的对象正在侦听来自用户界面的通知,例如窗口关闭,则您希望在后台线程而不是主线程中接收通知。在这些情况下,您必须在通知在默认线程上传递时捕获通知,并将其重定向到适当的线程。
As stated by the official; for processing notification threads that are not the main thread, such as background threads, this processing scenario exists, and the official also provides a specific implementation plan:
One way to implement redirection is to customize a notification queue (note, not an NSNotificationQueue object, but an array), and let this queue maintain the Notifications we need to redirect. We still register a notification observer as usual. When the Notification comes, first check whether the thread that posted the Notification is the thread we expected. If not, then store the Notification in our queue. And send a mach signal to the desired thread to tell this thread that it needs to process a Notification. After receiving the signal, the specified thread removes the Notification from the queue and processes it.
The official demo is as follows:
@interface MyThreadedClass: NSObject
/* Threaded notification support. */
@property NSMutableArray *notifications;
@property NSThread *notificationThread;
@property NSLock *notificationLock;
@property NSMachPort *notificationPort;
- (void) setUpThreadingSupport;
- (void) handleMachMessage:(void *)msg;
- (void) processNotification:(NSNotification *)notification;
@end
The notification thread definition class MyThreadedClass
includes the notification message queue for recording all notification messages notifications
, recording the current notification receiving thread notificationThread
, the mutex lock required for multi-thread concurrent processing NSLock
, and the notification processing thread for inter-thread communication to process notification messages NSMachPort
; and provides settings Thread properties, mach
instance methods for processing messages and processing notification messages
The t method is as setUpThreadSuppor
follows:
- (void) setUpThreadingSupport {
if (self.notifications) {
return;
}
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
[self.notificationPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(NSString __bridge *)kCFRunLoopCommonModes];
}
It mainly initializes the class attributes, specifies NSMachPort
the agent and adds it to the processing thread runloop
; if mach
the message arrives and the receiving thread runloop
is not running, the kernel will save the message until the next runloop
run; it can also be performSelectro:inThread:withObject:waitUtilDone:modes
implemented, but it needs to be turned on for the child thread. runloop
, otherwise the method will be invalid and waitUtilDone
the parameters need to be specified to be NO
called asynchronously.
NSMachPortDelegate
The protocol method is processed as follows:
- (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];
}
NSMachPor
The t protocol method mainly checks any notification messages that need to be processed and processes them iteratively (to prevent a large number of port messages from being sent concurrently, resulting in message loss). After the processing is completed, it is removed from the message queue synchronously;
The notification handling method is as follows:
- (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 {
// Process the notification here;
}
}
In order to distinguish between NSMachPort
internal calls to protocol methods and notification processing message callbacks, different notification message processing methods need to be processed by determining the current processing thread; for notification observation callbacks, add messages to the message queue and send inter-thread communication messages; in fact, the core of this mach
solution It is to NSMachPort
notify the receiving thread to process the message in the notification queue through asynchronous communication between threads ;
For the receiving thread, you need to call the following method to start notification message processing:
[self setupThreadingSupport];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(processNotification:)
name:@"NotificationName"//通知消息名称,可自定义
object:nil];
The official also gave problems and thoughts on this plan:
First, all threaded notifications processed by this object must pass through the same method (processNotification:). Second, each object must provide its own implementation and communication port. A better, but more complex, implementation would generalize the behavior into either a subclass of NSNotificationCenter or a separate class that would have one notification queue for each thread and be able to deliver notifications to multiple observer objects and methods.
First
, all thread notifications handled by this object must pass the same method (processNotification:). Second, each object must provide its own implementation and communication ports. A better but more complex implementation would generalize the behavior to a subclass of NSNotificationCenter or a separate class, with a notification queue per thread, and the ability to deliver notifications to multiple observer objects and methods
It is pointed out that a better way is to subclass one yourself NSNotficationCenter
(some big guys on github have implemented this solution, please refer to GYNotificationCenter ) or write a separate class to handle this kind of forwarding.