iOS NSNotificationCenter does not receive notification messages

 

By Gintok
Link: https://www.jianshu.com/p/e368a18ca7c2
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.

Use of notices

NSNotificationCenter notification center is an implementation mechanism of message broadcast within iOS program, which can send notifications among different objects to realize communication. The notification center adopts one to many mode, and the notifications sent by one object can be received by multiple objects, which is similar to KVO mechanism. The callback function triggered by KVO can also be responded to by one object, but the proxy is used Delegate mode is a one-to-one mode. There can only be one delegate object, and the object can only communicate with the delegate object through proxy.

Two core classes in notification mechanism: NSNotification and NSNotificationCenter

NSNotification

NSNotification is the basis of notification center. All notifications sent by notification center will be encapsulated as objects of this class and then passed between different objects. Class is defined as follows:

 

//The name of the notice. Different notices can be distinguished according to the name
@property (readonly, copy) NSNotificationName name;
//nil is often used for notification objects. If the value is set, the object of the registered notification listener needs to match the object of the notification, otherwise the notification will not be received
@property (nullable, readonly, retain) id object;
//The user information of dictionary type, in which the user can put the data to be transferred
@property (nullable, readonly, copy) NSDictionary *userInfo;

//The following three are the constructors of NSNotification, which generally do not need to be constructed manually
- (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;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

NSNotificationCenter

The notification center adopts the single example mode, and there is only one notification center in the whole system. You can get objects through [NSNotificationCenter defaultCenter].
The core methods of notification center are as follows:

 

/*
Register notification listeners, the only way to register notifications
observer Listener
aSelector Is the processing function after receiving the notification
aName Is the name of the listening notification
object In order to receive the notification object, it needs to match the object of postNotification, otherwise the notification cannot be received
*/
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

/*
To send a notification, you need to manually construct an NSNotification object
*/
- (void)postNotification:(NSNotification *)notification;

/*
Sending notice
aName Notification name registered for
anObject This method can be used when a notification does not pass a parameter to an object that accepts the notification
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;

/*
Sending notice
aName Notification name registered for
anObject Is the subject of the notification
aUserInfo Data of dictionary type can be transferred
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

/*
Delete listener for notification
*/
- (void)removeObserver:(id)observer;

/*
Delete listener for notification
aName Name of the listening notification
anObject The sending object of the monitored notification
*/
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

/*
Register the notification listener as a block
*/
- (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));

Let's take a look at the actual use of the notification example. There are two pages, ViewController and NextViewController. There is a button and a label in ViewController. Click the button to jump to NextViewController view. NextViewController includes an input box and a button. The user clicks the button to exit the view and jump back to ViewController after completing the input The data filled in by the user is shown in the troller's label. The code is as follows

 

//ViewController part code

- (void)viewDidLoad
{
    //Register the listener of the notification. The notification name is inputTextValueChangedNotification, and the processing function is inputTextValueChangedNotificationHandler:
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputTextValueChangedNotificationHandler:) name:@"inputTextValueChangedNotification" object:nil];

}

//Button click event handler
- (void)buttonClicked
{
    //Click the button to create NextViewController and display
    NextViewController *nvc = [[NextViewController alloc] init];
    [self presentViewController:nvc animated:YES completion:nil];
}

//Notification listener handler
- (void)inputTextValueChangedNotificationHandler:(NSNotification*)notification
{
    //Get data from the userInfo dictionary and display it in the tag
    self.label.text = notification.userInfo[@"inputText"];
}

- (void)dealloc
{
    //Remove notification listener before ViewController is destroyed
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"inputTextValueChangedNotification" object:nil];
}

//NextViewController part code
//Event handler when the user clicks the button after input
- (void)completeButtonClickedHandler
{
    //Send the notification and construct a dictionary data type of userInfo to save the user input text
    [[NSNotificationCenter defaultCenter] postNotificationName:@"inputTextValueChangedNotification" object:nil userInfo:@{@"inputText": self.textField.text}];
    //Exit view
    [self dismissViewControllerAnimated:YES completion:nil];
}

The program is relatively simple. Here are the steps to use the notification:

  1. Register a notification listener where you need to listen
  2. Implementation of callback function of notification listener
  3. Delete notification listener before listener object is destroyed
  4. If the notification needs to be sent, use the postNotification method of NSNotificationCenter to send the notification

After IOS 9, apple no longer sends notifications to the destroyed listener. When the listener object is destroyed, sending notifications will not cause the error of the wild pointer, which is more secure than KVO. KVO will still trigger a callback function after the listener object is destroyed, which may cause the error of the wild pointer. Therefore, you can use notifications without deleting the listener manually, but if you need to adapt to IOS 9 Previous systems still need to develop the habit of manually deleting listeners.

Multithreading in notifications

In Apple's official documents, the use of notifications in multithreading is explained as follows:

Regular notification centers deliver notifications on the thread in which the notification was posted. Distributed notification centers deliver notifications on the main thread. At times, you may require notifications to be delivered on a particular thread that is determined by you instead of the notification center. 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.

The simple understanding is

In multithreaded applications, Notification is forwarded in which thread the post is in, not necessarily in the thread where the observer is registered.

That is to say, the sending and receiving processes of Notification are all in the same thread. You can verify with the following code:

 

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"The current thread is%@", [NSThread currentThread]);
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"Test_Notification" object:nil];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"Test_Notification" object:nil userInfo:nil];
        NSLog(@"The thread to send the notification is%@", [NSThread currentThread]);
    });
}

- (void)handleNotification: (NSNotification *)notification {
    NSLog(@"Threads that forward notifications%@", [NSThread currentThread]);
}

The output result is:

 

The current thread is < nsthread: 0x608000073780 > {number = 1, name = main}
Thread receiving and processing notifications < nsthread: 0x608000261180 > {number = 3, name = (null)}
The thread sending notification is < nsthread: 0x608000261180 > {number = 3, name = (null)}

It can be seen that although we have registered the observer of the Notification in the main thread, the Notification of post in the global queue is not handled in the main thread. Therefore, at this time, we need to note that if we want to handle UI related operations in the callback, we need to ensure that the callback is executed in the main thread.
So how can we make a Notification post thread and a forwarding thread not the same thread? Apple documents offer a solution:

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.

When we talk about "redirection", we capture these distributed notifications in the default thread where Notification is located, and then redirect them to the specified thread.

Mode 1: use block

Apple has provided NSNotification with block since iOS 4. Use as follows:

 

-(id)addObserverForName:(NSString*)name object:(id)obj queue:(NSOperationQueue*)queue usingBlock:^(NSNotification * _Nonnull note);

When using the block method, we can refresh the UI in the main thread by setting [NSOperationQueuemainQueue].
Our code has become a little more concise as a result:

 

[[NSNotificationCenter defaultCenter] addObserverForName:@"Test_Notification" object:nil queue [NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"Threads that receive and process notifications%@", [NSThread currentThread]);
    }];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"Test_Notification" object:nil userInfo:nil];
    NSLog(@"The thread to send the notification is%@", [NSThread currentThread]);
});

Method 2: Custom notification queue

Customize a Notification queue (note that it is not an NSNotificationQueue object, but an array) to maintain the notifications that we need to redirect. We are still as usual to register a Notification observer. When Notification comes, first check whether the thread of post Notification is the one we expect. If not, store the Notification in our queue, and send a signal to the expected thread to tell the thread that it needs to handle a Notification. After receiving the signal, the specified thread removes the Notification from the queue and processes it.
In this way, apple officially provides code examples as follows:

 

@interface ViewController () <NSMachPortDelegate>
@property (nonatomic) NSMutableArray    *notifications;         // Notification queue
@property (nonatomic) NSThread          *notificationThread;    // Desired thread
@property (nonatomic) NSLock            *notificationLock;      // Lock object used to lock notification queue to avoid thread conflict
@property (nonatomic) NSMachPort        *notificationPort;      // Communication port for signaling the desired thread
    
@end
    
@implementation ViewController
    
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"current thread = %@", [NSThread currentThread]);
    
    // Initialization
    self.notifications = [[NSMutableArray alloc] init];
    self.notificationLock = [[NSLock alloc] init];
    
    self.notificationThread = [NSThread currentThread];
    self.notificationPort = [[NSMachPort alloc] init];
    self.notificationPort.delegate = self;
    
    // Add port source to run loop of current thread
    // When a Mach message arrives and the receiving thread's run loop is not running, the kernel saves the message until the next time it enters the run loop
    [[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:TEST_NOTIFICATION 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) {
        // Forward the notification to the correct thread.
        [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;
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSLog(@"process notification");
    }
}
    
@end

As you can see, the Notification we threw in the global dispatch queue was received in the main thread as expected. However, there are defects in this way, as Apple's official website said:

This implementation is limited in several aspects. 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.

A better way is to de instantiate an nsnotification center, and then customize the related processing.

Reference resources

  1. Using NSNotification in iOS multithreading
  2. Notification and multithreading
  3. NSNotificationCenter notification usage details
79 original articles published, 9 praised, 70000 visitors+
Private letter follow

Keywords: iOS

Added by JovanLo on Mon, 17 Feb 2020 10:28:21 +0200