Day by day our applications have to do more and more complicated tasks without compromising their responsiveness. That’s why we need to know how to make the best use of power that awaits inside our devices. We can do it by utilising concurrency through the usage of Grand Central Dispatc
h or Operations. Today we will talk about Operations.
Simply put, Operation is a wrapper around some work that we would like to execute. Speaking mo
re formally it is an abstract class that when subclassed will perform a given task for us. It’s built atop of the Grand Central Dispatch, which allows us to focus less on details of how concurrent execution will be done and more on the implementation of our business logic. Operations can help us with tasks that require more time to complete such as network calls or data processing.
A very simple example of Operation is Block Operation. Sometimes we may think that a task that we would like to do is just too small to create Operation object for it — that’s when we can use the Block Operation.
In this playground example we create a printerOperation as our BlockOperation object, then we add to it blocks of code that will be part of this operation. After adding all of the blocks we set completion block that will be executed after operation finishes. Now the only thing missing is starting our operation and to do so we create an Operation Queue object. We will go into details of what exactly the Operation Queue is but for now think about it as a queue that waits for operations to get added to it and then executes them on a separate thread. If we were to run this example a few times in a row we would get a different order of printed words because order isn’t guaranteed here. We will later see how to control order of task execution inside an Operation Queue.
Sometimes using the BlockOperation simply won’t be enough. For example when we want to keep our code decoupled in order to be able to swap operations depending on our needs or when we would like to make our operation more generic. That’s when we need to define a custom operation object. When we create our own subclass of the Operation the least we have to do is to override its main method by putting an implementation of our task inside of it and handle the possibility that it was cancelled. We will talk more about cancelation later down the road.
To make our operations nicely organised we can add properties, initialisers and helper methods. Here we have an example of the Operation that adds filter to an image.
Now let’s take a look on how we can use our brand new operation
An important thing to note here is that inside of the completion handler we are setting the imageView’s image property to the output of our operation on the main queue, because every UI related change has to happen there.
Now that we know how to create a custom Operation, it’s important to understand its lifecycle.
Every operation has states that change throughout the course of its execution. When we instantiate an operation we set its state to isReady. Later when we decide to run our operation, its state switches to isExecuting. Next it can go down two ways: we can decide to cancel our operation, or we can wait and let it finish — in both cases we end up with isFinished status. We’ll discuss cancelling in more detail after the next example.
Using synchronous operations we don’t have to worry about states because they are being handled for us by the system. It’s not the case with an asynchronous operation, that’s why it’s important to know how and when the operation states change.
Some long running tasks, such as network calls, require us to make our operation asynchronous. We need to do it because if we were just to call an asynchronous method from regular Operation’s main() method it would think that it’s completed right away, as asynchronous calls return immediately.
Because an operation queue that’s running our operations can’t tell when it’s done with an asynchronous call, we need to notify it about the operation’s state changes through KVO.
To make our operation asynchronous we need to override following method and properties:
This property explicitly says that our operation is going to be asynchronous.
This property has to be overridden by us because in case of asynchronous operations we must handle state changes on our own. It states whether an operation is currently executing.
Similar to isExecuting, we have to override isFinished in order to take care of changing operation state. This property demonstrates whether operation is already finished.
This method is the place where our operation begins. We’re responsible for checking if it wasn’t cancelled, then changing its state to isExecuting and generating KVO notifications. After that we have to call an asynchronous method right away or main method with an asynchronous method inside it which gives us nicer code organisation.
Here we can see an Operation subclass called AsyncOperation which will be a base for an asynchronous operation that we will create in the next example. We added a computed property state that will generate proper KVO notifications before and after the state changes so we won’t have to worry about doing it manually. These notifications are being used by the OperationQueue for handling operations.
Next we have an extension inside which we override necessary properties and start() method as described in the beginning of this section.
This operation subclass was taken from one of Ray Wenderlich’s tutorials.
Now let’s see how we can use this base class to create an actual asynchronous operation.
As discussed above inside main() method we have an asynchronous method, in this case some image downloading code. Inside the dataTask call we check if the operation wasn’t cancelled in the meantime — if it was then we return without setting the downloadedImage. Each state change sends KVO notifications through the computed state property inside the AsyncOperation class.
To actually use an object of this class we should add it to an operation queue, and we already know how to do it, but we didn’t yet talk in detail about what an operation queue is.
Operation Queue is a special queue to which we can add our operations to have them executed on a separate thread. It is a kind of FIFO queue but we don’t have any certainty about the exact order execution. If we would like to have one operation executed faster than the other, we can make use of a queue priority.
Queue priority has to be set on the operation before adding it to an operation queue.
We can pick from one of 5 priorities:
By default an operation has normal queuePriority.
On the chart above we have an example of adding multiple processes with different priorities to an operation queue. As we can see tasks with higher priority are getting executed faster. We can limit the number of operations being executed concurrently inside a queue by setting its maxConcurrentOperationCount property. For example making it equal one would make the queue serial. There is also an option to make queue wait for all of its operations to complete, but keep in mind that it will block the current thread.
If we would like to cancel all tasks that are currently inside an operation queue we can do so by calling its cancelAllOperations() method. This method is the reason why we check if the operation isn’t cancelled before starting it because there may be a case when it was waiting for its turn inside a queue but got cancelled before even starting. We also have an option to suspend a queue preventing it from starting any queued operations, but operations that are already executing will continue to do so. Even if our operation queue is suspended, we can keep adding new operations to it, however it will not start any of them until we will set its isSuspended property to false.
Quality of Service
Through the usage of qualityOfService we determine which object will get a faster access to resources such as network, CPU time, disk resources and so on. We can set quality of service to a certain operation object or to the whole operation queue. If operation has its qualityOfService property set then it will be used instead of queue’s qualityOfService.
We can pick one of five options:
Used for things that have to be provided to the user as fast as possible such as drawing to the screen or processing control events
Used for a user-requested work — for example loading a recipe after a user has selected it on the recipes list.
Used for work that the user is willing to wait longer for such as exporting a bulk file.
Used for work that is not visible to the user — syncing with remote database or pre-fetching content.
This is the one that will be assigned to the object that got no qualityOfService assigned by us — but it’s something that won’t happen in our case because both Operation and Operation Queue have a default background qualityOfService.
It’s important to choose the right qualityOfService for the job that our operation or operation queue should do because it can have both positive or negative impact. If we are doing some resource-heavy operation that is important for the user we need to pick the userInitiated, but doing so for something not important because we feel like it may help will only waste battery.
Concurrency programming isn’t easy but with the right tools, such as the Operations we can make a good use of it without worrying too much about all the details happening under the hood. Now that we know the basics of Operations we will be able to tackle more complicated (and fun) cases in the next post!
Stanisław Paśkowski is an iOS Developer at inFullMobile, an international digital product design and development studio based in Warsaw, Poland.
Fell free to contact us: firstname.lastname@example.org
Originally published at blog.infullmobile.com on March 9, 2018.