paint-brush
A Guide to NSOperation and NSOperationQueue in iOS Developmentby@mniagolov
3,405 reads
3,405 reads

A Guide to NSOperation and NSOperationQueue in iOS Development

by Maksim NiagolovJuly 4th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Apple provides two multithreading methods for iOS development, Grand Central Dispatch (GCD) and NSOperation. Both encapsulate units of work and dispatch them to a queue for execution. Because these APIs have the same goal, developers are often confused about which one they should utilize for which use case.
featured image - A Guide to NSOperation and NSOperationQueue in iOS Development
Maksim Niagolov HackerNoon profile picture

In order for your iPhone to remain responsive even when a large amount of data is being downloaded or a call to an external interface is made in the background, iOS applications use a technique called multithreading.


Apple provides two multithreading methods for iOS development, Grand Central Dispatch (GCD) and NSOperation. In this article, we will learn about NSOperation. If you need a refresher on the basics of multithreading and concurrency in iOS development, check out this article. I also recommend learning about GCD before diving into NSOperation.


NSOperation provides some benefits that GCD does not, such as a more user-friendly interface for dependencies, the Observable property as well as the ability to pause, cancel and resume operations and to specify the number of tasks that can be executed concurrently.


Differences between NSOperation and GCD

NSOperation has been available since iOS 2. Both NSOperation and GCD encapsulate units of work and dispatch them to a queue for execution. Because these APIs have the same goal, developers are often confused about which one they should utilize for which use case.

When Apple introduced GCD, NSOperation was refactored to work on top of GCD. The NSOperation API is a high-level abstraction of GCD, meaning that if you are using NSOperation, you are using GCD below the surface.


GCD is a low-level C API that interacts directly with the Unix level of the system. NSOperation is an Objective-C API, which introduces some overhead.


Before an instance of NSOperation can be used, it needs to be allocated and then deallocated when it is not needed anymore. This process is optimized but slower than GCD.

NSOperation

NSOperation is an abstract class that offers a thread-safe structure for modeling state, priority, dependencies, and management. It is part of the Foundation framework and can be used to encapsulate work that should be performed as part of an iOS application’s logic.


Developers can build custom subclasses of NSOperation, however, this is not always necessary. Foundation provides the concrete implementations NSBlockOperation and NSInvocationOperation.


NSBlockOperation manages the concurrent execution of one or more blocks of code. It can execute several blocks at once without needing to create separate operations for each of them. When more than one block is executed, the operation is considered finished once the execution of all blocks has been completed.


NSInvocationOperation manages the execution of a single, non-concurrent encapsulated task. It is used to initiate an operation that invokes a selector on a specified object.

In many cases, utilizing one of these predefined implementations will suffice.

NSOperationQueue

NSOperation is designed to be used in conjunction with NSOperationQueue, a class that manages a queue of NSOperation objects. When adding an NSOperation to an NSOperationQueue, the queue takes care of starting the operation once it is ready and keeping track of its progress.


NSOperationQueue manages the concurrent execution of tasks. It functions as a priority queue that roughly follows the FIFO (first in, first out) algorithm, but also considers the priority of a task if it is given.


Similarly to Grand Central Dispatch, NSOperation may additionally use the Quality of Service (QOS) classification to describe the urgency of a task. The values userInteractive, userInitiated, utility, and background are defined in NSQualityOfService.


// Create a queue
let queue = NSOperationQueue()

// Create operations
let operation1 = NSBlockOperation {    
    print("Performing task 1...")
}

let operation2 = NSBlockOperation {    
    print("Performing task 2...")
}

// Set the priority and quality of service
operation1.queuePriority = .high
operation1.qualityOfService = .userInteractive

operation2.queuePriority = .low
operation2.qualityOfService = .background

// Add the operations to the queue
queue.addOperations([operation1, operation2], waitUntilFinished: false)


In this example, an NSOperationQueue is created and two NSBlockOperations are added to it.

Multiple operations can be added to the queue at once using the addOperations method. The waitUntilFinished parameter determines whether the queue should wait for the operations to finish before returning control to the caller.


An NSOperation can be started using the start() method, but since many of the benefits of NSOperation come from utilizing NSOperationQueue, it is preferable to add a task to a queue like in the given code example rather than stating it directly.

Control

The behavior of the NSOperationQueue can be customized by setting its maxConcurrentOperationCount property. This property determines the maximum number of operations that can be executed concurrently. If it is set to 1, the queue will execute one operation at a time. If it is set to a higher value, the queue will execute multiple operations concurrently.

Observable

The NSOperation and NSOperationQueue classes have properties that can be observed using KVO. KVO stands for Key-Value Observing and is a pattern used to notify objects about changes to the properties of other objects. When an operation is ready for execution, it sends a KVO notification for the ready keypath whose corresponding property then returns true. Each property is mutually exclusive from one another.


The possible states are:

  • Ready: true if the operation is ready to execute, false if some initialization steps still need to be taken

  • Executing: true if the operation is currently executing, otherwise false

  • Finished: true if the task was finished successfully or if it was canceled


An NSOperationQueue does not dequeue any tasks until the finished state returns true.


Pause, Resume, Cancel

An NSOperation can be paused, resumed, and canceled with the methods pause(), resume() and cancel(). Once a task is dispatched using GCD, you no longer have any control or insight into the execution of that task. The NSOperation API gives the developer control over the operation’s lifecycle.


For instance, if it is no longer necessary to carry out a task because it was canceled by the user, NSOperation allows for this cancellation and communicates it through KVO.

Dependencies

NSOperation provides support for dependencies. This allows developers to execute tasks in a specific order. An operation is ready once every dependency has finished its execution.


For instance, if an image should be downloaded and then resized, it is sensible to split these operations up. This way, the downloading operation can be reused to download other data and the resizing operation can be reused for other images that are already available in the cache. Since an image cannot be resized before it is downloaded, the downloading operation is a dependency of the resizing operation.


let downloadOperation: NSOperation = ...
let resizeOperation: NSOperation = ...
resizeOperation.addDependency(downloadOperation)
let operationQueue = NSOperationQueue.mainQueue()
operationQueue.addOperations([downloadOperation, resizeOperation], waitUntilFinished: false)


A task will not start until all of its dependencies return true for the finished property. It is important to not create any cycles where two or more tasks depend on each other as this will create a deadlock.

Completion Block

When an NSOperation is finished, it executes its completion block once. This way, the behavior of an operation can be customized.


For instance, the completion block of a network operation could do something with the response data from the server once the loading is finalized.

let operation = NSOperation()

operation.completionBlock = {    
   print("Finished")    
   // do other things...
}

NSOperationQueue.mainQueue().addOperation(operation)


Conclusion

The NSOperation API provides an object-oriented model for encapsulating data around structured and repeatable tasks which gives the developer a lot of flexibility.


An NSOperation can be scheduled with dependencies and with a particular priority and quality of service. Unlike a block scheduled on a GCD dispatch queue, an NSOperation can be canceled and its state can be queried. Through subclassing, the result of an NSOperation can be associated with itself for future reference.


It is important not to add too many operations in a short timeframe as this can lead to performance problems due to NSOperation’s overhead.


Apple generally recommends that developers use the highest level of abstraction available. This is because, with every release, the frameworks which power the iOS operating system are optimized. When lower-level APIs are improved, higher-level APIs built on top of them improve as well. According to this philosophy, NSOperation should be used instead of GCD.


However, there are cases where GCD is a better fit. GCD should be used for trivial tasks such as dispatching a block of code to a serial or a concurrent queue, speeding up an existing method, or some one-off computation. For these kinds of scenarios, it is not necessary to create an NSOperation subclass and it is generally more convenient to use a lightweight GCD dispatch queue.


Therefore a combination of both frameworks is ideal for most projects.


The lead image for this article was generated by HackerNoon's AI Image Generator via the prompt "iOS development".