4.8 Notifications
Notifications are another example of loose coupling in Cocoa. They provide a layer of indirection between events and event handlers. Rather than every object keeping a list of objects that need to receive events, they go via the notification center, embodied by the NSNotificationCenter class. Objects can request notifications with a specific name, from a specific object, or both. When the specified object posts the named notification, the observer will receive a message with an NSNotification object as an argument.
This mechanism makes it very easy to join parts of your application together without explicitly hard-coding the connections. It is also a good example of the layering of Foundation and AppKit. Foundation defines notifications, but AppKit uses them heavily. A lot of view objects will post notifications when some user interaction has occurred, as well as delivering the message to their delegate. This allows multiple objects to be notified when some part of the user interface changes, with very little code.
By default, notifications are delivered synchronously. An object sends the notification to the notification center, and the notification center passes it on to all observers. Sometimes this is not the desired behavior. For asynchronous delivery, the NSNotificationQueue class integrates with the run loop and allows delivery of notifications to be deferred until the run loop is idle, or until the next iteration of the run loop.
4.8.1 Requesting Notifications
There are, generally speaking, two things you can do with notifications: send them and receive them. Receiving a notification is a two-step process. First, you tell the notification center what you want to receive, and then you wait.
Listing 4.18 shows a simple class for receiving notifications. This implements a single method, receiveNotification:, which takes a notification as an object. This is a very simple method that logs the name of the notification and the value associated with a key in the user info dictionary. The user info dictionary is what makes notifications so flexible. Any notification sender can attach an arbitrary set of key-value pairs to any notification. Because it is a dictionary, a receiver doesn't even need to know the specific keys; it has the option of enumerating every single key and doing something with all of the items in the dictionary, or it can simply ignore any keys it doesn't know about.
When the object is created, the default notification center is told to deliver notifications with the name "ExampleNotification" to its receiveNotification: method, from any object. It is also possible to specify a specific object to receive notifications from. In this case, you may leave the name: argument as nil and get every notification sent by that object.
Listing 4.18. Notification receiver object. [from: examples/Notications/notify.m]
3| @interface Receiver : NSObject {} 4| - (void) receiveNotification: (NSNotification*)aNotification; 5| @end 6| 7| @implementation Receiver 8| - (id) init 9| { 10| if (nil == (self = [super init])) 11| { 12| return nil; 13| } 14| // register to receive notifications 15| NSNotificationCenter *center = 16| [NSNotificationCenter defaultCenter]; 17| [center addObserver: self 18| selector: @selector(receiveNotification:) 19| name: @"ExampleNotification" 20| object: nil]; 21| return self; 22| } 23| - (void) receiveNotification: (NSNotification*)aNotification 24| { 25| printf("Received_notification:_%s", 26| [[aNotification name] UTFString]); 27| printf("Received_notification:_%s", 28| [[[aNotification userInfo] objectForKey: @"message"] UTFString]); 29| } 30| - (void) dealloc 31| { 32| NSNotificationCenter *center = 33| [NSNotificationCenter defaultCenter]; 34| [super dealloc]; 35| } 36| @end
There is one remaining part of handling notifications that is very important. You must remember to send the notification center a removeObserver: message when your object is destroyed. For this reason, it is good practice to ensure that the object that is registering as an observer in notifications is always self. This makes it very easy to make sure you call removeObserver: at the right time; just put the call in the -dealloc method. In a garbage-collected environment, the notification center should keep weak references to observers, so they will be automatically removed when no longer valid.
In this simple example, notifications are identified by literal strings. It is more common when creating public notifications to use shared global objects, initialized in one file and declared extern in a header.
4.8.2 Sending Notifications
The other half of the equation, sending messages, is even simpler. Listing 4.19 shows a simple object that sends a notification in response to a sendMessage: message. The string argument is inserted into the user info dictionary and delivered via a notification.
Listing 4.19. Sending a notification. [from: examples/Notications/notify.m]
38| @interface Sender : NSObject {} 39| - (void) sendMessage: (NSString*)aMessage; 40| @end 41| @implementation Sender 42| - (void) sendMessage: (NSString*)aMessage 43| { 44| NSNotificationCenter *center = 45| [NSNotificationCenter defaultCenter]; 46| 47| NSDictionary *message = 48| [NSDictionary dictionaryWithObject: aMessage 49| forKey: @"message"]; 50| [center postNotificationName: @"ExampleNotification" 51| object: self 52| userInfo: message]; 53| } 54| @end 55| 56| int main(void) 57| { 58| [NSAutoreleasePool new]; 59| // Set up the receiver 60| Receiver *receiver = [Receiver new]; 61| // Send the notification 62| [[Sender new] sendMessage: @"A_short_message"]; 63| return 0; 64| }
This is the companion of the Receiver class from the Listing 4.18. The these two classes communicate without either having a direct reference to the other. The main() method creates an instance of each class and calls the sendMessage: method on the sender. This posts a notification that is received by the receiver:
$ gcc -framework Foundation notify.m && ./a.out Received notification: ExampleNotification Message is: A short message
4.8.3 Sending Asynchronous Notification
Normally, sending a notification is a synchronous operation. You send a -postNotification: message to the notification center, it iterates over all of the objects that have registered to receive that notification, sends the notification to them, and then returns.
Every thread has its own notification center, and it also has a notification queue, an instance of NSNotificationQueue. This functions in a similar way to the notification center, but defers delivery of notifications until a future run-loop iteration.
Notification queues are particularly useful if you are generating a lot of the same sort of notification in quick succession. Often, the observers do not need to run immediately. Consider something like the spell checker in a text box. This could send a notification every time the user types a character. The spell checker could register for this notification, receive it, see if the typed word is valid, and update the display. This has two drawbacks. First, a word will be checked several times as it is typed, which is not always needed. Second, the spell checking will interrupt the typing.
A better design would flag the text box as needing spell checking as soon as the user typed something, but defer the actual checking until later. The notification queue does two things that help here. First, it performs notification coalescing, turning multiple identical notifications into a single one. This means that you can send a notification for every key press, but the spell checker will only receive one. The other useful feature is deferred delivery. When you post a notification via a queue, you can decide whether it should be delivered now, soon, or when the thread has nothing else to do. A typical program spends most of its time doing nothing. The notification queue allows you to defer handling of notifications until one of these times, for example, only running the spell checker when the user stops typing. In practice, no user types fast enough to keep a modern CPU busy, but this system applies equally to other data sources, such as the disk or network, which can provide data fast enough to keep a processor busy for a while.
A notification queue is a front end to a notification center. You insert notifications into the queue and it then sends them to the notification center, which sends them to the observers. This combination means that objects listening for a notification do not have to know whether the sender is using a notification queue or sending notifications synchronously.
There are two ways of getting a pointer to a notification queue. The most common way is to send a +defaultQueue message to the class. This will return the notification queue connected to the thread's default notification center. Notifications posted to this queue will be delivered in the thread that sent them.
Alternatively, you can explicitly create a queue for a given center. You can have different queues, as shown in Figure 4.1. Each object can send notifications to one or more notification queues, or to the notification center directly. The notification queues will coalesce the notifications and then pass them on to the notification center, which then distributes them to all of the registered observers.
Figure 4.1 The flow of notifications in a Cocoa program.
Having multiple notification queues allows you to control how notifications are coalesced. You may want to have all notifications from every instance of a particular class to be coalesced, but to be delivered separately from notifications of the same kind delivered from other objects, you can create a new notification queue for the class. You must attach the queue to a notification center when you initialize it, by sending it an -initWithNotificationCenter: message.
You send a notification through a queue by sending it either this message or one of the simpler forms that omits one or more argument:
- (void)enqueueNotification: (NSNotification*)notification postingStyle: (NSPostingStyle)postingStyle coalesceMask: (NSUInteger)coalesceMask forModes: (NSArray*)modes;
The first argument is a notification. You have to create this yourself; there are convenience methods for constructing them as there are on the notification center. You will typically do this by sending a +notificationWithName:object:userInfo: message to the notification class.
The second argument defines when the notification should be delivered. You have three options here. If you specify NSPostNow, then the behavior is similar sending the message directly to the notification center. The notification will be posted synchronously, but it will first be coalesced. You can use this to flush a set of notifications. If you have a set of operations that may all trigger a particular kind of notification, then you can have them all send their notifications into a queue and then send a notification with the posting style set to NSPostNow to ensure that exactly one notification will be sent.
The other options defer posting of the notification until a future run-loop iteration. If you specify NSPostASAP, then the notification will be posted as soon as possible. This may not be at the start of the next run-loop iteration, because there may be other notifications with the same priority already waiting for delivery, but it will be soon. If the notification is not very important, then you can set the posting style to NSPostWhenIdle. This will cause it to be delivered only when there are no more pressing events waiting for the run loop's attention.
Coalescing works best the longer notifications are allowed to stay in the queue. If everything is posted with NSPostNow, then the queue never has a chance to coalesce them. If everything is posted with NSPostWhenIdle, then notifications may stay in the queue for a long time, and will have a lot more opportunities for being combined.
The coalescing behavior is configured with the third argument. This is a mask formed by combining flags specifying whether notifications should be coalesced if they are from the same sender or of the same type. Most often you will specify either both of these flags, or neither. Coalescing notifications from the same sender but with different types may cause some very strange behavior. You can coalesce notifications of the same type from different senders, but this is generally not recommended either.
The final argument specifies the run-loop modes in which the notification will be delivered. You can use this to prevent notifications from being handled in certain modes, or to define new modes for handling certain kinds of notification and keep them queued until you explicitly tell the run loop to enter that mode. This provides a fairly simple way of deferring notification delivery until you explicitly decide you want to handle them.
Notification queues are very powerful, but rarely need to be used. They are most commonly treated as an optimization technique. If you profile your code and find that it is spending a lot of time handling duplicate notifications, then consider adding a notification queue.
4.8.4 Distributed Notifications
Notifications aren't solely subject to use in a single process. The NSDistributedNotificationCenter class is a subclass of NSNotificationCenter built using the distributed objects (DO) mechanism. This makes them the simplest form of interprocess communication (IPC) to use on OS X.
Although less flexible than using DO directly, distributed notifications provide a very simple way of communicating between different processes. In principle, distributed notifications could be integrated with Bonjour for sending notifications across the local network segment. The -notificationCenterForType: constructor hints at future possibilities for distributed notifications; however, it only supports NSLocalNotificationCenterType on OS X. The GNUstep implementation also supports GSNetworkNotificationCenterType for delivering notifications over the network using distributed objects, but there is currently no equivalent provided by Apple.
Registering to receive a distributed notification is almost the same as registering to receive a normal one. The main difference is the last parameter. Note the different method prototypes for registering observers in a notification center and a distributed notification center:
// NSNotificationCenter - (void)addObserver: (id)notificationObserver selector: (SEL)notificationSelector name: (NSString*)notificationName object: (id)notificationSender; // NSDistributedNotificationCenter - (void)addObserver: (id)notificationObserver selector: (SEL)notificationSelector name: (NSString*)notificationName object: (NSString*)notificationSender;
The last argument is a pointer to the object from which you want to receive the notifications. In a distributed notification center, senders are identified by name, rather than pointer. The reason for this should be obvious: Distributed notifications can come from other processes, and pointers are only valid in the current process's address space.
Sending distributed notifications has the same change. The same method can be used, but the sender must be a string instead of an object. Typically, this is the name of the application sending the notification.
There are also some restrictions placed on the user info dictionary when sending distributed notifications. Because the notification is sent over DO, all of the objects in the dictionary must conform to the NSCoding protocol, allowing them to be serialized and deserialized in the remote process. Since the deserialization can potentially be done in a large number of listening processes, it is a good idea to keep notifications small.