December 15, 2012
Working with NSOperations in iOS
In every software development cycle you will encounter the optimization phase, where your app is now doing what it should be doing but the UI is clunky or freezes at some points while the app is doing some heavy lifting work like parsing data, working on database, manipulating images or files etc. These heavy operations if done on the main thread in an iOS app life cycle will block the UI updates and responsiveness of your app. This problem is solved using different methods. You could send some blocks to the background using Grand Central Dispatch (GCD) or spawn your own NSTheads.
NSOperation represents a single unit of computation. It provides you with a thread-safe way to model your workload in aspects of state, priority dependencies and cancelation. So you will be able to subclass NSOperation to customize your own pieces of work in any order/dependency you need to suite your needs for every app you build. It gives an OOP perspective on computation which is a great deal of benefit for today’s complicated requirement and data handling.
The class structure of the NSOperation is straight forward.
Each Queue type NSOperationQueue will hold a number of operations (NSOperation).
NSOperation: is an abstract class which you would subclass or use any of its subclasses to execute your work in. Although it is an abstract class it already implements the thread safe features which allow you to focus on the actual work that needs to be done.
NSBlockOperation: is a concrete subclass of NSOperation which allows you to execute several blocks at once without having to create multiple operations objects. These blocks are dispatched with default priority to an appropriate queue.
NSInvocationOperation: another concrete subclass of NSOperation that allows you to create an operation from a message call or an invocation.
UIActivityItemProvider: a new subclass of NSOperation introduced in iOS 6. When using UIActivityViewController you could pass it a UIActivityItemProvider to provide the item that the activity will be working on. You’re expected to sublclass this proxy object in order to implement your own item for the activity view controller.
All operation objects support the following features:
- support graph-based dependencies between operations.
- Completion block
- Prioritizing operations
- Cancelling operations
NSOperation state life cycle would normally be:
isReady -> isExecuting -> isFinished
isReady: return YES to indicate the operation is ready to start executing unless there is unfinished initialization work that hasn’t been completed yet or a dependant operation hasn’t finished executing yet (more on dependencies later)
isExecuting: return YES if the operation is currently working on its task other wise NO.
isFinished: return YES to indicate that the work has been done.
NSOperation class supports KVC & KVO so you could observe the following keys to control you other parts of your app.
isCancelled, isConcurrent, isExecuting, isFinished, isReady, dependencies
if you choose to provide a custom implementation of any of these properties remember that you mush implement the Key-Value-Coding and Key-Value-Observing for these properties.
At some point the user might want to cancel the operation or navigate to some other view controller which at that points renders the operations unnecessary. To cancel the operation you could call [myOperation cancel] a built in method provided by the sdk or you can cancel all the operations on your queue by calling
[myQueue cancelAllOperations] which is also provided by the sdk .
But when the operation starts executing its out of your hands and to enforce the cancelation of an executing operation you need to check your op’s isCancelled property and at that point stop the work and any necessary cleaning up after your code.
Priorities & Dependencies
Another cool feature of NSOperation is dependencies. You should use dependency when your operation can start only after another operation is finished. For example lets say you have image download operation and image processing operation. You want the processing to begin after the image is downloaded. So you can make the image processing operation dependant on the download operation.
[operationQueue addOperation imageProcessingOp];
Always set dependencies before adding the operations to the queue or running them.
This way the imageProcessingOp wont be ready to start until the imageDownloadOp or any other dependencies before it are finished.
For priority management iOS SDK provides this set of priorities to use:
NSOperationQueuePriorityVeryLow = -8,
NSOperationQueuePriorityLow = -4,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
The default priority value is NSOperationQueuePriorityNormal.
You can set the priority of an operation in the Queue by calling the setQueuePriority: method on the op. If you pass a value that isn’t defined the default value will be used (normal priority).
Priorities help order the operations execution order within the queue by promoting high priority operations over low priority ones from the currently ready ops. For example, if you have 2 ops one in Hight priority and the other is Low priority, if the Low priority op is ready before the High priority op it will be executed first. If you want to prevent the low priority op to execute after the high priority op you should make it dependant on the High priority op instead of setting their priority.
Another aspect is the thread priority. Where you can set the thread’s system priority while the op’s main method is executing. Notice that this doesn’t apply for the completion block. You can specify a floating-point value to the threadPriority property before adding it to the queue or executing it manually.
NSOperation object is non-concurrent by default, that means if you run it manually with out adding it to a NSOperationQueue it will run on the same thread where the call was made.
If you run the operations from with in a queue, they will run on background threads synchronously to that thread.
It is possible to make operations run concurrently when ran manually you will have to implement some code on your own to check if the op is ready to run and check if its cancelled periodically and spawn the work on a separate thread on your own.
In this case if the op is not ready you will reschedule a timer to check again later in order to ensure the op actually start executing.
In case you want your op to notify listeners or do something at the end that is not considered part of the main method you could set a completion block to be executed once the op is finished.
Call the native method on NSOperation object:
- (void)setCompletionBlock:(void (^)(void))block
the block you pass should take no arguments and should not return any value.
Completion block is also used to generate final KVO notifications.
The completion block will execute on the same thread that the operation was running on. So if you want to modify UI make sure to send the call to the main thread.
NSOperationQueue objects are the concrete objects that manage the NSOperation objects you add to the queue.
Creating a queue:
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
Adding operations to the queue is the easiest way to ensure that the operations start. You do so by adding an op to the queue.
Queues can also be suspended and resumed by your app. Suspending the Queue will prevent new operations from starting, but operations that are currently running will continue until they’re done.
Operations are removed from the Queue when they’re finished.
You also can determine the maximum number or concurrent operations you want your queue to run simultaneously. This helps you better improve your apps performance, especially since operations could potentially consume a lot of memory and processing power. So even though your queuing ops on the queue if the ops are using a lot of CPU and memory your app will suffer from performance issues.
When Subclassing NSOperation class you should at least implement the main method. Its recommended to wrap your code there with try..catch clause to be safe of exceptions happening while your operation is running in the background.
While your code is executing, you need to check periodically for any cancellation events in order to stop gracefully.
If your operation must return values to the main thread, it’s a good habit to declare a delegate protocol in order to pass these values back to the main thread and update UI if needed.
[(NSObject *)self.delegate performSelectorOnMainThread:(@selector(delegateMethod:)) withObject:object waitUntilDone:NO];
Note that you will have to cast your operation to NSObject first.
Keep in mind when you change the op’s default properties to execute the appropriate KVC calls to notify listeners about values being changed.
finished = YES;
you should consider using an autorelease pool in the main function to catch any autoreleased objects that you might have used while executing the op.
Many demos are provided by Apple showing how to subclass NSOperation and things you could do with it.
Please checkout the Apple documentation/Demos at:
NSOperation Class Reference
NSOperationQueue Class Reference
Lazy Table Images ( by Apple)
Demo image downloads and filters (by Soheil Moayedi Azarpour)
NSOperations are a powerful tool for you to model your background work. It’s a little extra overhead than GCD but it comes with the benefits of KVO, suspend/resume, priorities and dependencies.
I recommend reading Concurrency Programming Guide to learn more details about NSOperations and NSOperationQueue tweaks and details.