【iOS】Notification Principle

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 NCTblstructure named, which contains three important member variables, namely two GSIMapTabletables: named, nameless, and a singly linked list wildcard.
    • namedIs a table that stores notifications whose notification names are passed in hash.
    • namelessobjectIt is a table that stores notifications that do not have a notification name but a message sender hash.
    • wildcardIs 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 NCTblstructure GSIMapTableaccording to these three types wildcard.
  • 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 array GSIArray.
    1. Get all wildcardnotifications that match the criteria and add them to an array GSIArray.
    2. namelessFind notifications that match the criteria in the table and add them to the array GSIArray.
    3. namedFind notifications that match the criteria in the table and add them to the array GSIArray.
  • Finally, after all the notifications that meet the conditions are added, the entire array is traversed and the notification messages are sent GSIArrayin sequence .performSelector:withObject

Notification principle

data structure

_GSIMapTableThe mapping table data structure diagram is as follows:

Related data structures:

  • _GSIMapTableThe mapping table contains nodeChunks, bukets, buketCount, chunkCount.
  • nodeChunks: nodeChunksis a GSIMapNodepointer 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: Record nodethe 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 GSIMapTabletables: named, nameless, and singly linked listwildcard

  • namedhash, a notification table that holds the name of the incoming notification
  • nameless, save the table without passing in the notification name but passing in the message objectsenderhash
  • wildcard, holds a singly linked list with neither notification names nor incoming objectnotifications

Saving the notification table containing the notification name namedrequires a registered objectobject, so the table structure is passed in nameas key. And it valueis also a GSIMapTabletable, which is used to store objects objectof corresponding objects observer;

For the notification table that does not pass in the notification name but only objectthe object , only the corresponding relationship between and namelessneeds to be saved , so it is used as .objectobserverobjectkeyobservervalue

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 blockthe form of creating GSNotificationObserveran object, the object will be Block_copycopied blockand the notification operation queue will be determined. The notification reception processing function didReceiveNotificationis passed addOperationto 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:withObjectsend notification messages, so the threads that send notifications and receive notifications are the same thread (in blockthe form of specifying queue processing by operating the queue).

sections in notifications

NSNotification

NSNotificationContains some information about message sending, including namemessage name, objectmessage sender, and userinfoadditional 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)initThis method is an initialization method, but in NSNotificationthe class, it is marked API_UNAVAILABLEas unavailable. Therefore, this method cannot be used directly to initialize NSNotificationthe object.)

NSNotificationCenter

NSNotificationCenterMessage 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: selectorand block, for the method of adding a specified observer object, observerit cannot be nil; blockthe method will execute copythe method, return the anonymous observer object used, and specify the operation object of the observer to process the message NSOperationQueue.

The specified message name nameand sender object objectcan 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 blockspecified 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.queuenil

Note: Registering observer notification messages should avoid repeated registration, which will result in repeated processing of notification messages and blockholding 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 NSNotificationa 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 blockis 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 dealloccalled after the observer object is released, but there is no need to call manually after that, as iOS9it has been automatically processed.macos10.11dealloc

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

NSNotificationQueueThe 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 defaultQueueget 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 idle
  • NSPostNow: 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-merger
  • NSNotificationCoalescingOnName: Merge notifications according to notification names
  • NSNotificationCoalescingOnSender: Merge notifications according to the incoming object

If NSNotificationQueuethe notification queue does not specify NSPostNowthe immediate sending mode, it can be runloopsent asynchronously by

NSNotification and multithreading

NSNotificationThe 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 MyThreadedClassincludes 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, machinstance methods for processing messages and processing notification messages

The t method is as setUpThreadSupporfollows:

- (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 NSMachPortthe agent and adds it to the processing thread runloop; if machthe message arrives and the receiving thread runloopis not running, the kernel will save the message until the next runlooprun; it can also be performSelectro:inThread:withObject:waitUtilDone:modesimplemented, but it needs to be turned on for the child thread. runloop, otherwise the method will be invalid and waitUtilDonethe parameters need to be specified to be NOcalled asynchronously.

NSMachPortDelegateThe 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];
}

NSMachPorThe 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 NSMachPortinternal 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 machsolution It is to NSMachPortnotify 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.

Guess you like

Origin blog.csdn.net/m0_63852285/article/details/132017911