Anda di halaman 1dari 10

Asynchronous Design Patterns with Blocks, GCD and

XPC
This is my notes/interpretation of WWDC 2012 Session 712.
Blocks
Blocks are anonymous functions in C.
The caret ^ is used to indicate that this is a block pointer rather than a function pointer.
typedef void (^callback_block)(char *arg); callback_block b = ^(char *str) { printf("%S\n", str); }
b("Hello World"); // this is the same as invoking a function pointer in c.
The block is declared in the context where it is needed. The block can access variables in
the enclosing scope plus the block can modify variables in the enclosing scope if they are
given the __block qualifier.
Three common types of blocks:
Completion
Comparison
Enumeration
Completion
typedef void (^MyCompletionBlock)(NSData *data);
So if for example you downloaded image, you might want to update your ui by
displaying a picture created from the image data youve downloaded. In this case you use
the completion handler to trigger update of the UI with the image.
void MyUpdateImageWithURL(NSImageView *view, NSURL *url) { MyDownloadAsync(url,
^(NSData *data) { NSImage *image = [[NSImage alloc] initWithData:data]; [view setImage:image];
[image release]; }); }
Comparison
NSComparisonResult = (^MyComparatorBlock)(NSString *val1, NSString *val2); extern void
MySort(NSMutableArray *array, MyComparatorBlock comparator); void
MySortAlphabetically(NSMutableArray *array, Boolean ignoreCase) { NSStringCompareOptions options
= 0; if (ignoreCase) options = NSCaseInsensitiveSearch; MySort(array, ^(NSString *val1, NSString
*val2) { return [val1 compare:val2 options:options]; }); }
Iteration
void (^MyApplierBlock)(NSNumber *value); extern void MyApply(NSSet *set, MyApplierBlock applier);
NSNumber *getMaximum(NSSet *set) { __block NSNumber *result = @INT_MIN; MyApply(set,
^(NSNumber *value) { if ([value compare:result] > 0) result = value; }); return result; }
GCD takes blocks and treats them like data objects and allows you to do powerful things
with them.
GCD Enqueues blocks for invocation. Asynchronous execution.
There is a particular type of block that GCD uses. They are called Dispatch Blocks and
take no arguments, no return value, and rely on capturing variables from the context
within which they have been implemented in to manipulate state.
They take the form:
^{ ... }
We can treat the signature of every block to be the same.
Dispatch Queues (Serial queues and concurrent queues)
dispatch_queue_t queue; queue = dispatch_queue_create("com.example.queue",
DISPATCH_QUEUE_SERIAL); printf("Before async\n"); dispatch_async(queue, ^ { printf("Hello
World\n"); }); printf("After async\n");
The output will be:
Before async
After async
Hello World
dispatch_async is a fundamental piece of gcd. It enqueues a block for asynchronous
execution. It puts the block onto the queue and returns immediately. It doesnt wait of the
block to execute. That happens later.
The Serial dispatch queue.
FIFO
Atomic enqueue
Automatic dequeue
Asynchronous blocks allow you to easily execute work independently of main thread.
This makes it easy to keep the main thread responsive to UI events.
Updating the UI
The Call-Callback Pattern. This is the method for taking the result of the work done by
blocks on threads and letting the main thread know that the UI needs to be updated.
So we add to the image download task above the Call-Callback pattern which involves
dispatching the completion block to the main thread. Ive also made the blocks into
variables so that the code is easier to visualise, or at least for me.
typedef void (^MyCompletionBlock)(NSData *data); extern void MyDownloadAsync(NSURL *url,
MyCompletionBlock completion); void MyUpdateImageWithURL(NSImageView *view, NSURL *url) {
// implement the download completion block. MyCompletionBlock myCompletionBlock = ^ {
NSImage *image = [[NSImage alloc] initWithData:data]; // Now we've got the image update implement
the update the UI block. void (^updateUIBlock)(void) = ^ { [view setImage:image]; }; // Add
to the main queue the update the UI block. dispatch_async(dispatch_get_main_queue(), updateUIBlock);
[image release]; }; MyDownloadAsync(url, myCompletionBlock); }
XPC
Communicate between processes.
XPC makes it easy to do:
Simple interface to look up services by name.
Send and receive message asynchronously.
Delivers replies as blocks submitted to queues.
Services that you provide are contained within your main application bundle.
When messages are sent to a service the service will be launched on demand.
Privilege separation. Network access can be palmed off onto a service that all it does is
one privileged activity like network access leaving the main application safe if anything
goes wrong with the network service.
Privilege separation. Run with different sandbox entitlements.
When doing the Call-Callback pattern with xpc we create a queue and a connection.
These work as a pair.
We create the queue:
dispatch_queue_t queue; queue = dispatch_queue_create("com.example.queue",
DISPATCH_QUEUE_SERIAL);
We can look up a service by name, for example:
xpc_connection_t connection; connection = xpc_connection_create("com.example.render", queue);
The function xpc_connection_create takes a gcd queue as its second parameter. The
queue is the one which the callback block will be submitted to. This parameter can be nil
in which case the target queue is lib dispatchs default target queue.
After creating the connection you set an event handler on it. The event handler is a block
that is called when the service replies to the request.
xpc_connection_set_event_handler(connection, ^(xpc_object_t reply) { if (xpc_get_type(reply)) !=
XPC_TYPE_ERROR) { size_t len; void *bytes = xpc_dictionary_get_data(reply, "decoded", &len);
callback([NSData dataWithBytes:bytes length:len]); } });
Once we have fully configured our connection we need to call xpc_connection_resume to
let the system know that the connection is ready to receive events on it.
xpc_connection_resume(connection);
Now putting all this together:
void MyDecodeImageRemote(NSData *data, MyCallbackBlock callback) { dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_SERIAL);
xpc_connection_t connection = xpc_connection_create("com.example.render", queue);
xpc_connection_set_event_handler(connection, ^(xpc_object_t reply) { if (xpc_get_type(reply)) !=
XPC_TYPE_ERROR) { size_t len; void *bytes = xpc_dictionary_get_data(reply, "decoded",
&len); callback([NSData dataWithBytes:bytes length:len]); } });
xpc_connection_resume(connection); // Now interacting with the service is a simple as creating a xpc
dictionary. xpc_dictionary_t message; message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_data(message, "encoded", [data bytes], [data length]);
xpc_connection_send_message(connection, message); xpc_release(message); }
If the service has not already started then it will be started when
xpc_connection_send_message call is made.
After sending the message we can release the message.
Blocks & GCD pre 10.8
Blocks are created on the stack. May be copied to the heap.
What happens to the variables that are captured by the block.
Scalars are const copied. Objective-C objects are retained and other pointers copied as
values, Not their storage.
The following crashes in Lion but is fine in Mountain Lion:
-(void)performAsyncWorkWithCallback:(id)obj onQueue:(dispatch_queue_t)q { // Perform work on a
queue internal to our object. dispatch_async(self.queue, ^ { [self performWork]; dispatch_async(q,
^ { [obj callback]; }); }); }
The queue object is not retained because it is not an objective-c object. It is necessary to
retain the queue and then release it at the appropriate time.
Once dispatch_async is called taking the queue, the queue will remain alive at least until
after the block on the queue has been run. So if we have retained the queue we can then
release the queue immediately after we have called dispatch_async.
So to fix the above for Lion:
-(void)performAsyncWorkWithCallback:(id)obj onQueue:(dispatch_queue_t)q { dispatch_retain(q); //
First retain the queue. // Perform work on a queue internal to our object. dispatch_async(self.queue, ^ {
[self performWork]; dispatch_async(q,^ { [obj callback]; }); dispatch_release(q); //
dispatch_async has been called. The queue will stay alive // at least until the block that has been placed on
the queue is complete }); }
In iOS 6 and mountain lion this has been fixed and fixed by making gcd and xpc
objects Objective-C objects.
so the fix above is no longer needed, and the code that would crash for Lion above now
behaves correctly.
Blocks can be weak or copy properties and their memory management is handled
appropriately.
Minimum deployment target for blocks and gcd to behave as Objective-C objects
including working with ARC is 10.8 and iOS 6.
A couple of special considerations are needed to be considered for Objective-C and ARC
when using xpc, gcd and blocks.
Blocks and strong reference cycles, and interior pointers.
@interface MyClass () @property (readonly) int val; @property (strong) dispatch_block_t work; @end -
(void)setup { self.work = ^ { NSLog(@"%d", val); }; }
The block has to be copied to the heap because the block clearly needs to live longer than
the scope of the setup method. Shown here the block captures the instance variable val of
the object but it does it indirectly via self so the block actually captures self. The
increments the reference count of self.
There are three ways to break the strong reference cycle between the block and Objective
C object.
Scoping
Programmatically
Compiler attributes
Scoping
Simply setup a local variable of the one that we want to use:
-(void)setup { int local = self.val; self.work = ^ { NSLog(@"%d", local); }; }
This can change the behaviour of your block to the above though. The original setup
method which creates the strong reference captures self as a const but if val changes
between when the block was implemented and when it was run then when the block runs
it uses the modified value of val. However if you use the scoping method to fix the strong
reference cycle the value captured is the one that is generated at implementation time not
run time.
Programmatically
Change the example so that we are dealing with an objective-c object not an int.
@interface MyClass () @property (strong) id obj; @property (strong) dispatch_block_t work; @end -
(void) setup { self.work = ^ { [self.obj perform]; }; }
We can setup a manual cancel method that simply nils out the block property. We release
the block. This means the block can be deallocated and in doing so releases its reference
to self. This is exactly what is recommended when your using the gcd or xpc api. You
just need to remember to call the cancel method.
-(void)cancel { self.work = nil; }
The functions to call using the gcd and xpc api are:
dispatch_release(source); xpc_release(connection);
=====
dispatch_source_t source = dispatch_source_create(...); dispatch_source_set_event_handler(source, ^ {
NSLog(@"%d", dispatch_source_get_data(source)); }); ... dispatch_source_cancel(source);
dispatch_release(source); // Not needed if using ARC. ----------------------- xpc_connection_t connection =
xpc_connection_create(); xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
NSLog(@%p, xpc_connection_get_context(connection)); }); ... xpc_connection_cancel(connection);
xpc_release(connection); // Not needed if using ARC.
Attributes
@interface MyClass () @property (strong) id obj; @property (strong) dispatch_block_t work; @end -
(void)setup { self.work = ^ { [self.obj perform]; }; }
If youre using ARC then you can just take advantage of the __weak attribute.
-(void)setup { MyClass * __weak weakSelf = self self.work = ^ { MyClass * __strong strongSelf =
weakSelf; if (strongSelf) [strongSelf.obj perform]; }; }
Interior pointers and APIs that expose interior pointers.
-(void)logWithData:(dispatch_data_t)data { void *buf; dispatch_data_t map; map =
dispatch_data_create_map(data, &buf, NULL); NSLog(@%@, [NSString stringWithUTF8String:buf]);
}
ARC recognizes that map is an Objective-C object of type dispatch_data_t and the
compiler doesnt see that the map object is used after it is created. The compiler therefore
immediately disposes of the object. The buffer buf becomes invalid. It is necessary to
make sure that the map object is kept alive until execution leaves the scope that map was
created in. This is achieved by using the compile attribute so:
-(void)logWithData:(dispatch_data_t)data { void *buf; dispatch_data_t map
__attribute__((objc_precise_lifetime)); map = dispatch_data_create_map(data, &buf, NULL);
NSLog(@%@, [NSString stringWithUTF8String:buf]); }
Asynchronous Design Patterns
Make code more asynchronous
Avoid common trouble spots
Apply patterns to Apple APIs or your own.
Most important issue is dont block the main thread. Main thread really should only
ever handle UI.
If for example we have a method that is computationally expensive and we call it so:
[self renderThumbnails];
and then this is followed by informing the appropriate view that it needs to update itself:
[self.thumbnailView setNeedsDisplay:YES];
then we need to move the computationally expensive code to be executed off the main
thread and we do that so:
dispatch_queue_t queue; queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^ {
[self renderThumbnails]; dispatch_async(dispatch_get_main_queue(), ^ { [self.thumbnailView
setNeedsDisplay:YES]; }); });
This is a common pattern, I need to write a routine that can be called from any thread and
calls the views setNeedsDisplay from the main thread.
MainThreadSetNeedsDisplay(NSView *theView) { dispatch_async(dispatch_get_main_queue(), ^ {
[theView setNeedsDisplay:YES]; }); }
This will convert the above to:
dispatch_async(queue, ^ { [self renderThumbnails]; MainThreadSetNeedsDisplay(self.thumbnailView);
}
Dont block too many background threads.
The following could easily result in the system being flooded with far too many threads.
dispatch_queue_t queue; queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (NSURL *url in
[self.imageStore URLs]) { dispatch_async(queue, ^ { NSData *data = [NSData
dataWithContentsOfURL:url]; dispatch_async(dispatch_get_main_queue(), ^ { [self.imageStore
setImageData:data forURL:url]; }); }); }
You can use dispatchIO instead to perform the download tasks.
for (NSURL url in [self.imageStore URLs]) { dispatch_io_t io =
dispatch_io_create_with_path(DISPATCH_IO_RANDOM, [[url path]
fileSystemRepresentation], O_RDONLY, 0, NULL, NULL); dispatch_io_set_low_water(io,
SIZE_MAX); dispatch_io_read(io, 0, SIZE_MAX, dispatch_get_main_queue(), ^(bool done,
dispatch_data_t data, int error) { [self.imageStore setImageData:data forURL:url]; }); }
One queue per subsytem
Subdivide the app into independent subsystems
Control access to subsystems with serial dispatch queues
Main queue is access queue for UI subsystem
This can be demonstrated with the following code, where we have one subsystem
for downloading the data, one subsystem for managing the storage of the data,
one subsystem for rendering the images and finally the main queue for keeping
the UI up to date. The netServiceDidResolveAddress is a delegate methods for the
netServive api and it is called when the address of the network service has been
resolved in this example on the main thread.
-(void)netServiceDidResolveAddress:(NSNetService *service { dispatch_async(self.downloadQueue, ^
{ NSData *data = pelf downloadFromRemoteService:service]; dispatch_async(self.storeQueue, ^ {
int img = [self.imageStore addImage:data]; dispatch_async(self.renderQueue, ^ { [self
renderThumbnail:img]; dispatch_async(dispatch_get_main_queue(), ^ { [[self
thumbnailViewForId:img] setNeedsDisplay:YES]; }); }); }); }); }
Improve performance with subsystems which have reader/writer access.
You can improve performance by using a concurrent subsystem queue.
DISPATCH_QUEUE_CONCURRENT.
This type of queue supports multiple concurrent operations. You can call dispatch_sync()
for a single task on one of these types of queue and that will not stop other tasks from
running.
Reader-Writer access
For more details see mastering grand central dispatch WWDC 2011 for more on Read-
Writer access.
Quick intro:
self.storeQueue = dispatch_queue_create(com.example.imageviewer.store,
DISPATCH_QUEUE_CONCURRENT); dispatch_barrier_async(self.storeQueue, ^ { int img =
[self.imageStore addImage:data]; dispatch_async(self.renderQueue, ^ { [self renderThumbnail:ing];
}); }); -(void)renderThumbnail:(int)img { __block NSData *data; dispatch_sync(self.storeQueue, ^ {
data = [self.imageStore copyImageData:img]; }); // ... doing something with the data. }
Update State Asynchronously
If you want to maintain the state of a progress bar indicating how many image thumbnails
have been rendered.
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
dispatch_get_main_queue()); dispatch_source_set_event_handler(self.source, ^ { self.progress +=
dispatch_source_get_data(self.source); [self.progressView setProgress:(self.progress/self.total)
animated:YES]; }); dispatch_resume(self.source);
Now from anywhere in the app we can update the progress bar with:
dispatch_async(self.renderQueue, ^ { // ... dispatch_source_merge_data(self.source, 1); });
You can also cancel the progress bar:
dispatch_source_cancel(self.source);
Move out of Process with XPC
Reliability and security
Fault isolation and privilege separation
XPC connection as remote queue
We can think of using an xpc connection to replace a queue. So we could have a
subsystem based around a queue and move that over to the subsystem being implemented
as a service and accessing it via a xpc connection. If you have already designed your app
to use a queue based subsystems then it is relatively simple to replace any of those queue
subsystems with a xpc connection subsystem.
Summing up
1. Dont block the main thread.
2. Use GCD and blocks to move heavy lifting from the main thread.
3. Dont block on too many background threads, for example for io operations.
4. Integrate with the main runloop. (keep ui up to date).
5. Use one queue per subsystem
6. Improve performance with reader-writer access
7. Separate control and data flow and why it is important.
8. Update state asynchronously with dispatch sources
9. Move operations out of process with XPC.
Further documentation and videos to read/watch
Documentation: Concurrency Programming Guide
Documentation: Daemons and Services Programming Guide.
Video: Cocoa Interprocess communication with XPC WWDC 2012
Video: Introducing XPC from WWDC 2011.

Anda mungkin juga menyukai