NSOperation
In life, there’s always work to be done. Every day brings with it a steady stream of tasks and chores to fill the working hours of our existence.
Yet, no matter how burdened one’s personal ToDo list becomes, it pales in comparison to the workload of an iOS app, of which millions of computations are expected, all while managing to draw a frame every 16 milliseconds.
Productivity is, as in life as it is in programming, a matter of scheduling and prioritizing and multi-tasking work in order to keep up appearances.
The secret to making apps snappy is to offload as much unnecessary work to the background as possible, and in this respect, the modern Cocoa developer has two options: Grand Central Dispatch and NSOperation
. This article will primarily focus on the latter, though it’s important to note that the two are quite complementary (more on that later).
NSOperation
represents a single unit of work. It’s an abstract class that offers a useful, thread-safe structure for modeling state, priority, dependencies, and management.
For situations where it doesn’t make sense to build out a custom
NSOperation
subclass, Foundation provides the concrete implementationsNSBlock
andOperation NSInvocation
.Operation
Examples of tasks that lend themselves well to NSOperation
include network requests, image resizing, text processing, or any other repeatable, structured, long-running task that produces associated state or data.
But simply wrapping computation into an object doesn’t do much without a little oversight. That’s where NSOperation
comes in:
NSOperationQueue
NSOperation
regulates the concurrent execution of operations. It acts as a priority queue, such that operations are executed in a roughly First-In-First-Out manner, with higher-priority (NSOperation.queue
) ones getting to jump ahead of lower-priority ones. NSOperation
can also limit the maximum number of concurrent operations to be executed at any given moment, using the max
property.
NSOperationQueue itself is backed by a Grand Central Dispatch queue, though that’s a private implementation detail.
To kick off an NSOperation
, either call start
, or add it to an NSOperation
, to have it start once it reaches the front of the queue. Since so much of the benefit of NSOperation
is derived from NSOperation
, it’s almost always preferable to add an operation to a queue rather than invoke start
directly.
State
NSOperation
encodes a rather elegant state machine to describe the execution of an operation:
ready
→executing
→finished
In lieu of an explicit state
property, state is determined implicitly by KVO notifications on those keypaths. When an operation is ready to be executed, it sends a KVO notification for the ready
keypath, whose corresponding property would then return true
.
Each property must be mutually exclusive from one another in order to encode a consistent state:
-
ready
: Returnstrue
to indicate that the operation is ready to execute, orfalse
if there are still unfinished initialization steps on which it is dependent. -
executing
: Returnstrue
if the operation is currently working on its task, orfalse
otherwise. -
finished
Returnstrue
if the operation’s task finished execution successfully, or if the operation was cancelled. AnNSOperation
does not dequeue an operation untilQueue finished
changes totrue
, so it is critical to implement this correctly in subclasses to avoid deadlock.
Cancellation
It is often useful to cancel operations early to prevent needless work from being performed, whether due to a failure in a dependent operation or explicit cancellation by the user.
Similar to execution state, NSOperation
communicates cancellation through KVO on the cancelled
keypath. When an operation is cancelled, it should clean up any internal details and arrive in an appropriate final state as quickly as possible. Specifically, the values for both cancelled
and finished
need to become true
, and executing
needs to become false
.
One thing to watch out for are the spelling peculiarities of the word “cancel”. Although spelling varies across dialects, when it comes to NSOperation
:
-
cancel
: use one L for the function (verb) -
cancelled
: use two L’s for the property (adjective)
Priority
All operations may not be equally important. Setting the queue
property will promote or defer an operation in an NSOperation
according to the following rankings:
NSOperationQueuePriority
public enum NSOperation Queue Priority : Int {
case Very Low
case Low
case Normal
case High
case Very High
}
typedef NS_ENUM(NSInteger, NSOperation Queue Priority) {
NSOperation Queue Priority Very Low = -8L,
NSOperation Queue Priority Low = -4L,
NSOperation Queue Priority Normal = 0,
NSOperation Queue Priority High = 4,
NSOperation Queue Priority Very High = 8
};
Quality of Service
Quality of Service is a new concept in iOS 8 & OS X Yosemite that creates consistent, high-level semantics for scheduling system resources. APIs were introduced for both XPC and NSOperation
that use this abstraction.
For NSOperation
, the thread
property has been deprecated in favor of this new quality
property. (And good riddance—thread
was too unwieldy to be anything but a liability to most developers.)
Service levels establish the system-wide priority of an operation in terms of how much CPU, network, and disk resources are allocated. A higher quality of service means that more resources will be provided to perform an operation’s work more quickly.
QoS appears to use the XNU kernel task policy feature introduced in OS X Mavericks under the hood.
The following enumerated values are used to denote the nature and urgency of an operation. Applications are encouraged to select the most appropriate value for operations in order to ensure a great user experience:
NSQualityOfService
@available(i OS 8.0, OSX 10.10, *)
public enum NSQuality Of Service : Int {
case User Interactive
case User Initiated
case Utility
case Background
case Default
}
typedef NS_ENUM(NSInteger, NSQuality Of Service) {
NSQuality Of Service User Interactive = 0x21,
NSQuality Of Service User Initiated = 0x19,
NSQuality Of Service Utility = 0x11,
NSQuality Of Service Background = 0x09,
NSQuality Of Service Default = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);
-
.User
:UserInteractive QoS is used for work directly involved in providing an interactive UI such as processing events or drawing to the screen.Interactive -
.User
: UserInitiated QoS is used for performing work that has been explicitly requested by the user and for which results must be immediately presented in order to allow for further user interaction. For example, loading an email after a user has selected it in a message list.Initiated -
.Utility
: Utility QoS is used for performing work which the user is unlikely to be immediately waiting for the results. This work may have been requested by the user or initiated automatically, does not prevent the user from further interaction, often operates at user-visible timescales and may have its progress indicated to the user by a non-modal progress indicator. This work will run in an energy-efficient manner, in deference to higher QoS work when resources are constrained. For example, periodic content updates or bulk file operations such as media import. -
.Background
: Background QoS is used for work that is not user initiated or visible. In general, a user is unaware that this work is even happening and it will run in the most efficient manner while giving the most deference to higher QoS work. For example, pre-fetching content, search indexing, backups, and syncing of data with external systems. -
.Default
: Default QoS indicates the absence of QoS information. Whenever possible QoS information will be inferred from other sources. If such inference is not possible, a QoS between UserInitiated and Utility will be used.
let background Operation = NSOperation()
background Operation.queue Priority = .Low
background Operation.quality Of Service = .Background
let operation Queue = NSOperation Queue.main Queue()
operation Queue.add Operation(background Operation)
NSOperation *background Operation = [[NSOperation alloc] init];
background Operation.queue Priority = NSOperation Queue Priority Low;
background Operation.quality Of Service = NSOperation Quality Of Service Background;
[[NSOperation Queue main Queue] add Operation:background Operation];
Asynchronous Operations
Another change in iOS 8 / OS X Yosemite is the deprecation of the concurrent
property in favor of the new asynchronous
property.
Originally, the concurrent
property was used to distinguish between operations that performed all of their work in a single main
method, and those that managed their own state while executing asynchronously. This property was also used to determine whether NSOperation
would execute a method in a separate thread. After NSOperation
was changed to run on an internal dispatch queue rather than manage threads directly, this aspect of the property was ignored. The new asynchronous
property clears away the semantic cobwebs of concurrent
, and is now the sole determination of whether an NSOperation
should execute synchronously in main
, or asynchronously.
Dependencies
Depending on the complexity of an application, it may make sense to divide up large tasks into a series of composable sub-tasks. This can be done with NSOperation
dependencies.
For example, to describe the process of downloading and resizing an image from a server, one might divide up networking into one operation, and resizing into another (perhaps to reuse the networking operation to download other resources, or also use the resizing operation for images already cached in memory). However, since an image can’t be resized until it’s downloaded, then the networking operation is a dependency of the resizing operation, and must be finished before the resizing operation can be started.
Expressed in code:
let networking Operation: NSOperation = ...
let resizing Operation: NSOperation = ...
resizing Operation.add Dependency(networking Operation)
let operation Queue = NSOperation Queue.main Queue()
operation Queue.add Operations([networking Operation, resizing Operation], wait Until Finished: false)
NSOperation *networking Operation = ...
NSOperation *resizing Operation = ...
[resizing Operation add Dependency:networking Operation];
NSOperation Queue *operation Queue = [NSOperation Queue main Queue];
[operation Queue add Operation:networking Operation];
[operation Queue add Operation:resizing Operation];
An operation will not be started until all of its dependencies return true
for finished
.
Make sure not to accidentally create a dependency cycle, such that A depends on B, and B depends on A, for example. This will create deadlock and sadness.
completion Block
When an NSOperation
finishes, it will execute its completion
exactly once. This provides a really nice way to customize the behavior of an operation when used in a model or view controller.
let operation = NSOperation()
operation.completion Block = {
print("Completed")
}
NSOperation Queue.main Queue().add Operation(operation)
NSOperation *operation = ...;
operation.completion Block = ^{
NSLog("Completed");
};
[[NSOperation Queue main Queue] add Operation:operation];
For example, you could set a completion block on a network operation to do something with the response data from the server once it’s finished loading.
NSOperation
remains an essential tool in an iOS or OS X developer’s bag of tricks. Whereas GCD is ideal for in-line asynchronous processing, NSOperation
provides a more comprehensive, object-oriented model of computation for encapsulating all of the data around structured, repeatable tasks in an application.
Developers should use the highest level of abstraction possible for any given problem, and for scheduling consistent, repeated work, that abstraction is NSOperation
. Other times, it makes more sense to sprinkle in some GCD (including within an NSOperation
subclass implementation).
When to Use Grand Central Dispatch
Dispatch queues, groups, semaphores, sources, and barriers comprise an essential set of concurrency primitives, on top of which all of the system frameworks are built.
For one-off computation, or simply speeding up an existing method, it will often be more convenient to use a lightweight GCD dispatch
than employ NSOperation
.
When to Use NSOperation
NSOperation
can be scheduled with a set of dependencies at a particular queue priority and quality of service. Unlike a block scheduled on a GCD queue, an NSOperation
can be cancelled and have its operational state queried. And by subclassing, NSOperation
can associate the result of its work on itself for future reference.
Just remember: NSOperation and Grand Central Dispatch are not mutually exclusive. Creative and effective use of both are key to developing robust and performant iOS or OS X applications.