How To Use NSOperations and NSOperationQueues

Update 10/7/14: This tutorial has now been updated for iOS 8 and Swift; check it out!

Everyone has had the frustrating experience of tapping a button or entering some text in an iOS or Mac app, when all of a sudden – WHAM, the user interface stops being responsive.

Lucky you — you get to stare at the hourglass or the colorful wheel rotating for a while until you’re able to interact with the UI again! Annoying, isn’t it?

In a mobile iOS application, users expect your apps to respond immediately to their touches, and when it doesn’t the app feels clunky and slow, and usually results in bad reviews.

However this is easier said than done. Once your app needs to perform more than a handful of tasks, things get complicated quickly. There isn’t much time to perform heavy work in the main run loop and still provide a responsive UI.

What’s a poor developer to do? One solution is to move work off the main thread via concurrency. Concurrency means that your application executes multiple streams (or threads) of operations all at the same time – this way the user interface can stay responsive as you’re performing your work.

One way to perform operations concurrently in iOS is with the NSOperation and NSOperationQueue classes. In this tutorial, you’ll learn how to use them! You’ll first create an app that doesn’t use multithreading at all, so it will appear very sluggish and unresponsive. Then you will rework the application to add concurrent operations and — hopefully — provide a more responsive interface to the user!

Before reading this tutorial, it might be helpful to read our Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial first. However, this tutorial is self-sufficient so it is not required reading.

Background

Before you dive into the tutorial, there are a few technical concepts that need to be dealt with first.

It’s likely that you’ve heard of concurrent and parallel operations. From a technical standpoint,concurrency is a property of the program and parallel execution is a property of the machine. Parallelism and concurrency are two separate concepts. As a programmer, you can never guarantee that your code will run on a machine which is capable of processing your code in parallel operations. However, you can design your code so that it takes advantage of concurrent operations.

First, it’s imperative to define a few terms:

  • Task: a simple, single piece of work that needs to be done.
  • Thread: a mechanism provided by the operating system that allows multiple sets of instructions to operate at the same time within a single application.
  • Process: an executable chunk of code, which can be made up of multiple threads.

Note: in iPhone and Mac, the threading functionality is provided by the POSIX Threads API (or pthreads), and is part of the operating system. This is pretty low level stuff, and you will find that it is easy to make mistakes; perhaps the worst thing about threads is those mistakes can be incredibly hard to find!

The Foundation framework contains a class called NSThread, which is much easier to deal with, but managing multiple threads with NSThread is still a headache. NSOperation and NSOperationQueue are higher level classes that have greatly simplified the process of dealing with multiple threads.

In this diagram, you can see the relationship between a process, threads, and tasks:

How To Use NSOperations and NSOperationQueues_第1张图片

Process, Thread and Task

As you can see, a process can contain multiple threads of execution, and each thread can perform multiple tasks one at a time.

In this diagram, thread 2 performs the work of reading a file, while thread 1 performs user-interface related code. This is quite similar to how you should structure your code in iOS – the main thread should perform any work related to the user-interface, and secondary threads should perform slow or long-running operations (such as reading files, acccessing the network, etc.)

NSOperation vs. Grand Central Dispatch (GCD)

You may have heard of Grand Central Dispatch (GCD). In a nutshell, GCD consists of language features, runtime libraries, and system enhancements to provide systemic and comprehensive improvements to support concurrency on multi-core hardware in iOS and OS X. If you’d like to learn more about GCD, you can read our Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.

Before Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were different from GCD and used two completely different mechanisms. Starting with Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were built on top of GCD. As a very general rule, Apple recommends using highest-level abstraction, and then dropping down to lower-levels when measurements show they are needed.

Here’s a quick comparison of the two that will help you decide when and where to use GCD or NSOperation and NSOperationQueue:

  • GCD is a lightweight way to represent units of work that are going to be executed concurrently. You don’t schedule these units of work; the system takes care of scheduling for you. Adding dependency among blocks can be a headache. Canceling or suspending a block creates extra work for you as a developer! :]
  • NSOperation and NSOperationQueue add a little extra overhead compared to GCD, but you can add dependency among various operations. You can re-use operations, cancel or suspend them. NSOperation is compatible with Key-Value Observation (KVO).

Preliminary Project Model

In the preliminary model of the project, you have a table view with a dictionary as its data source. The keys of the dictionary are the images’ names, and the value of the each key is a URL where the image is located. The goal of this project is to read the contents of the dictionary, download images, apply an image filter, and finally display the images in a table view.

Here’s a schematic view of the model:

How To Use NSOperations and NSOperationQueues_第2张图片

Preliminary Model

Implementation – the way you might first think of doing it…

Note: if you don’t want to build the non-threaded first version of this project, and get right to the multithreading aspect, you can skip this section and download the first version of the project that we create in this section.

All images are from stock.xchng. Some images in the data source are intentionally mis-named, so that there are instances, where an image fails to download to exercise the failure case.

Start up Xcode and create a new project with the iOS\Application\Empty Application template, and clickNext. Name it ClassicPhotos. Choose Universal, check Use Automatic Reference Counting (but nothing else is checked) and click Next. Save it wherever you like.

Select the ClassicPhoto project from the Project Navigator. Select Targets\ ClassicPhotos\Build Phasesand expand Link Binary with Libraries. Use the + button and add Core Image framework (you’ll need Core Image for image filtering).

Switch to AppDelegate.h in the Project Navigator, and import ListViewController — this is going to be the root view controller, which you’ll declare later. ListViewController will be a subclass of UITableViewController.

#import "ListViewController.h"

Switch to AppDelegate.m, find application:didFinishLaunchingWithOptions:. Init and alloc an instance of ListViewController. Wrap it in an instance of UINavigationController and set it as the root view controller of UIWindow.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
 
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
 
    /*
     ListViewController is a subclass of UITableViewController.
     We will display images in ListViewController.
     Here, we wrap our ListViewController in a UINavigationController, and set it as the root view controller.
     */
 
    ListViewController *listViewController = [[ListViewController alloc] initWithStyle:UITableViewStylePlain];
    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:listViewController];
 
    self.window.rootViewController = navController;
 
 
    [self.window makeKeyAndVisible];
    return YES;
}

Note: If you haven’t created a user interface in this way before, this is how you can create a user interface programmatically without using Storyboards or Interface Builder. We’re doing it this way just for simplicity in this tutorial.

Next create a new subclass of UITableViewController and name it ListViewController. Switch toListViewController.h and modify it as follows:

// 1
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
 
// 2
#define kDatasourceURLString @"http://www.raywenderlich.com/downloads/ClassicPhotosDictionary.plist"
 
// 3
@interface ListViewController : UITableViewController
 
// 4
@property (nonatomic, strong) NSDictionary *photos; // main data source of controller
@end

Let’s go through the above code section by section:

  1. Import UIKit and Core Image.
  2. For convenience, define kDatasourceURLString as the string URL of where the datasource file is located.
  3. Make ListViewController a subclass of UITableViewController, by substituting NSObject to UITableViewController.
  4. Declare an instance of NSDictionary. This will be the data source.

Now, switch to ListViewController.m, and add the following:

@implementation ListViewController
// 1
@synthesize photos = _photos;
 
#pragma mark -
#pragma mark - Lazy instantiation
 
// 2
- (NSDictionary *)photos {
 
    if (!_photos) {
        NSURL *dataSourceURL = [NSURL URLWithString:kDatasourceURLString];
        _photos = [[NSDictionary alloc] initWithContentsOfURL:dataSourceURL];
    }
    return _photos;
}
 
 
#pragma mark -
#pragma mark - Life cycle
 
- (void)viewDidLoad {
    // 3
    self.title = @"Classic Photos";
 
    // 4
    self.tableView.rowHeight = 80.0;
    [super viewDidLoad];
}
 
 
- (void)viewDidUnload {
    // 5
    [self setPhotos:nil];
    [super viewDidUnload];
}
 
 
#pragma mark -
#pragma mark - UITableView data source and delegate methods
 
// 6
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {    
    NSInteger count = self.photos.count;
    return count;
}
 
// 7
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 80.0;
}
 
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *kCellIdentifier = @"Cell Identifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
 
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
    }
 
    // 8
    NSString *rowKey = [[self.photos allKeys] objectAtIndex:indexPath.row];
    NSURL *imageURL = [NSURL URLWithString:[self.photos objectForKey:rowKey]];
    NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = nil;
 
    // 9
    if (imageData) {
        UIImage *unfiltered_image = [UIImage imageWithData:imageData];
        image = [self applySepiaFilterToImage:unfiltered_image];
    }
 
    cell.textLabel.text = rowKey;
    cell.imageView.image = image;
 
    return cell;
}
 
 
#pragma mark -
#pragma mark - Image filtration
 
// 10
- (UIImage *)applySepiaFilterToImage:(UIImage *)image {
 
    CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)];
    UIImage *sepiaImage = nil;
    CIContext *context = [CIContext contextWithOptions:nil];
    CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil];
    CIImage *outputImage = [filter outputImage];
    CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]];
    sepiaImage = [UIImage imageWithCGImage:outputImageRef];
    CGImageRelease(outputImageRef);
    return sepiaImage;
}
 
@end

Okay! There’s a lot going on here. Don’t panic – the code is explained below:

  1. Synthesize photos.
  2. Use lazy instantiation to load data source, i.e. photos dictionary.
  3. Give your view a title.
  4. Set the height of rows in table view to 80.0 points.
  5. Set photos to nil when ListViewController is unloaded.
  6. Return the number of rows to be displayed.
  7. This is an optional UITableViewDelegate method. For better viewing, change the height of each row to 80.0. The default value is 44.0.
  8. Get the key from the dictionary, create the NSURL from value of the key, and download the image as NSData.
  9. If you have successfully downloaded the data, create the image, and apply sepia filter.
  10. This method applies sepia filter to the image. If you would like to know more about Core Image filters, you can read Beginning Core Image in iOS 5 Tutorial.

That’s it! Give it a try! Build and run. Beautiful, sepia images – but…they…appear…so…slowly! It’s nice to look at, but only if you go make a snack while you wait for it to load. :]

ClassicPhotos (stalled version)

It’s time to think about how can you improve that user experience!

Threads

Every application has at least one thread known as the main thread. A thread’s job is to execute a sequence of instructions. In Cocoa Touch, the main thread contains the application’s main run loop. Nearly all application code that you write gets executed on the main thread, unless you specifically create a separate thread and execute some code in the new thread.

Threads have two specific characteristics:

  1. Each thread has equal access to all of your application’s resources; this includes access to any object except local variables. Therefore, any object can potentially be modified, used, and changed by any thread.
  2. There is no way to predict how long a thread will run — or which thread will finish first!

Therefore, it is important to be aware of techniques to overcome these issues, and prevent unexpected errors! :] This is a brief list of the challenges that multi-threaded applications face — and some tips on how to deal with them effectively.

  • Race Condition: the fact that every thread can access the same memory may cause what is known as a race condition.

    When multiple concurrent threads access shared data, the thread that gets to the memory first will change the shared data — and there’s no guarantee which thread will get there first. You might assume a variable has the value your thread last wrote to this shared memory, but another thread may have changed the shared memory in the meantime, and your variable is out of date!

     

    If you know that this condition might exist in your code (i.e. you know that you are going to read / write data concurrently from multiple threads) you should use mutex lock. Mutex stands for “mutual exclusion”. You create a mutex lock for instance variables by wrapping it around a “@synchronized block”. This way you make sure that the code within it is accessed only by one thread at a time:

    @synchronized (self) {
        myClass.object = value;
    }

    “Self” in the above code is called a “semaphore”. When a thread reaches this piece of code, it checks to see if any other thread is accessing “self”. If nobody else is accessing “self”, it executes the block; otherwise execution of the thread is blocked until the mutex lock becomes available.

  • Atomicity: you have likely seen “nonatomic” in property declarations numerous times. When you declare a property as atomic, it is usually wrapped in a @synchronized block to make it thread safe. Of course, this approach does add some extra overhead. To give you an idea, here is a rough implementation of an atomic property:
    // If you declare a property as atomic ...
    @property (atomic, retain) NSString *myString;
     
    // ... a rough implementation that the system generates automatically,
    // looks like this:
    - (NSString *)myString {
        @synchronized (self) {

            return [[myString retain] autorelease];

        }
    
}

    In this code, “retain” and “autorelease” calls are used as the returned value is being accessed from multiple threads, and you do not want the object to get deallocated between calls.

    Therefore, you retain the value first, and then put it in an autorelease pool. You can read more in Apple’s documentation about Thread Safety. This is worth knowing, if only for the reason that most iOS programmers never bother to find this out. Protip: this makes a great job interview question! :]

    Most of the UIKit properties are not thread-safe. To find out whether a class is thread-safe or not, take a look at the API documentation. If the API documentation does not say anything about thread-safety, then you should assume that the class is not thread-safe.

    As a general rule, if you are executing on a secondary thread and you must do something to a UIKit object, use performSelectorOnMainThread.

  • Deadlock: a situation where a thread is blocked waiting for a condition that can never be met. For example, if two threads that are executing with synchronized code call each other, then each thread will be waiting for the other one to finish and open the lock. But this will never happen, and both threads will be deadlocked.
  • Sleepy Time: this occurs when there are too many threads executing simultaneously and the system gets bogged down. NSOperationQueue has a property that you can set to tell it how many concurrent threads you want executing at the same time.

NSOperation API

The NSOperation class has a fairly easy and short declaration. To create a customized operation, follow these steps:

  1. Subclass NSOperation
  2. Override “main”
  3. Create an “autoreleasepool” in “main”
  4. Put your code within the “autoreleasepool”

The reason you should create your own autorelease pool is that you do not have access to the autorelease pool of the main thread, so you should create your own. Here is an example:

#import <Foundation/Foundation.h>
 
@interface MyLengthyOperation: NSOperation
@end
@implementation MyLengthyOperation
 
- (void)main {
    // a lengthy operation
    @autoreleasepool {
        for (int i = 0 ; i < 10000 ; i++) {
        NSLog(@"%f", sqrt(i));
        }
    }
}
 
@end

The code above shows the ARC syntax for autorelease pool usage. You should definitely be using ARC by now! :]

In threaded operations, you never know exactly when the operation is going to start, and how long it will take to finish. Most of the time you don’t want to perform an operation in the background if the user has scrolled away or has left a page — there’s no reason to perform the operation. The key is to check forisCancelled property of NSOperation class frequently. For example, in the imaginary sample code above, you would do this:

@interface MyLengthyOperation: NSOperation
@end
 
@implementation MyLengthyOperation
- (void)main {
    // a lengthy operation
    @autoreleasepool {
        for (int i = 0 ; i < 10000 ; i++) {
 
        // is this operation cancelled?
        if (self.isCancelled)
            break;
 
        NSLog(@"%f", sqrt(i));
        }
    }
}
@end

To cancel an operation, you call the NSOperation’s cancel method, as shown:

// In your controller class, you create the NSOperation
// Create the operation
MyLengthyOperation *my_lengthy_operation = [[MyLengthyOperation alloc] init];
.
.
.
// Cancel it
[my_lengthy_operation cancel];

NSOperation class has a few other methods and properties:

  • Start: Normally, you will not override this method. Overriding “start” requires a more complex implementation, and you have to take care of properties such as isExecuting, isFinished, isConcurrent, and isReady. When you add an operation to a queue (an instance of NSOperationQueue, which will be discussed later), the queue will call “start” on the operation and that will result in some preparation and the subsequent execution of “main”.

    If you call “start” on an instance of NSOperation, without adding it to a queue, the operation will run in the main loop.

  • Dependency: you can make an operation dependent on other operations. Any operation can be dependent on any number of operations. When you make operation A dependent on operation B, even though you call “start” on operation A, it will not start unless operation B isFinished is true. For example:
    MyDownloadOperation *downloadOp = [[MyDownloadOperation alloc] init]; // MyDownloadOperation is a subclass of NSOperation
    MyFilterOperation *filterOp = [[MyFilterOperation alloc] init]; // MyFilterOperation  is a subclass of NSOperation
     
    [filterOp addDependency:downloadOp];

    To remove dependencies:

    [filterOp removeDependency:downloadOp];
  • Priority: sometimes the operation you wish to run in the background is not crucial and can be performed at a lower priority. You set the priority of an operation by using “setQueuePriority:”.
    [filterOp setQueuePriority:NSOperationQueuePriorityVeryLow];

    Other options for thread priority are: NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityHigh, and NSOperationQueuePriorityVeryHigh.

    When you add operations to a queue, the NSOperationQueue looks through all of the operations, before calling “start” on them. Those that have higher priorities will be executed first. Operations with the same priority will be executed in order of submission to the queue (FIFO).

    (Historical note: In 1997, an embedded system in the Mars Rover suffered from priority inversion, perhaps the most expensive illustration of why it is important to get priority and mutex locks right. Seehttp://research.microsoft.com/en-us/um/people/mbj/Mars_Pathfinder/Mars_Pathfinder.html for further background information on this event.)

  • Completion block: another useful method in NSOperation class is setCompletionBlock:. If there is something that you want to do once the operation has been completed, you can put it in a block and pass it into this method. Note that there is no guarantee the block will be executed on the main thread.
    [filterOp setCompletionBlock: ^{
        NSLog(@"Finished filtering an image.");
    }];

Some additional notes on working with operations:

  • If you need to pass in some values and pointers to an operation, it is a good practice to create your own designated initializer:
    #import <Foundation/Foundation.h>
     
    @interface MyOperation : NSOperation
     
    -(id)initWithNumber:(NSNumber *)start string:(NSString *)string;
     
    @end
  • If your operation is going to have a return value or object, it’s a good practice to declare delegate methods. Usually you want to call back to the delegate method on the main thread. You make the compiler happy, you need to cast the delegate to NSObject:
    [(NSObject *)self.delegate performSelectorOnMainThread:@selector(delegateMethod:) withObject:object waitUntilDone:NO];
  • You cannot enqueue an operation again. Once it is added to a queue, you should give up ownership. If you want to use the same operation class again, you need to create a new instance.
  • A finished operation cannot be restarted.
  • If you cancel an operation, it will not happen instantly. It will happen at some point in the future when someone explicitly checks for isCancelled == YES; otherwise, the operation will run until it is done.
  • Whether an operation finishes successfully, unsuccessfully, or is cancelled, the value of isFinished will always be set to YES. Therefore never assume that isFinished == YES means everything went well — particularly, if there are dependencies in your code!

NSOperationQueue API

NSOperationQueue also has a fairly simple interface. It is even simpler than NSOperation, because you don’t need to subclass it, or override any method — you simply create one. It is a good practice to give your queue a name; this way you can identify your operation queues at run time and make its debugging easier:

NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
myQueue.name = @"Download Queue";
  • Concurrent operations: a queue is not the same thing as thread. A queue can have multiple threads. Each operation within a queue is running on its own thread. Take the example where you create one queue, and add three operations to it. The queue will launch three separate threads, and run all operations concurrently on their own threads.

     

    By default, NSOperationQueue class will do some magic behind the scenes, decide what is best for the particular platform the code is running on, and will launch the maximum possible number of threads.

    Consider the following example. Assume the system is idle, and there are lots of resources available, so NSOperationQueue could launch something like eight simultaneous threads. Next time you run the program, the system could be busy with other unrelated operations which are consuming resources, and NSOperationQueue will only launch two simultaneous threads.

  • Maximum number of concurrent operations: you can set the maximum number of operations that NSOperationQueue can run concurrently. NSOperationQueue may choose to run any number of concurrent operations, but it won’t be more than the maximum.
    myQueue.MaxConcurrentOperationCount = 3;

    If you change your mind, and want to set MaxConcurrentOperationCount back to its default, you would perform the following changes:

    myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
  • Add operation: as soon as an operation is added to a queue, you should relinquish ownership by sending a release message to the operation object (if using manual reference counting, no ARC), and the queue will then assume responsibility to start the operation. At this point, it is up to the queue as to when it will call “start”.
    [myQueue addOperation:downloadOp];
    [downloadOp release]; // manual reference counting
  • Pending operations: at any time you can ask a queue which operations are in the queue, and how many operations there are in total. Remember that only those operations that are waiting to be executed, and those that are running, are kept in the queue. As soon as an operation is done, it is gone from the queue.
    NSArray *active_and_pending_operations = myQueue.operations;
    NSInteger count_of_operations = myQueue.operationCount;
  • Pause (suspend) queue: you can pause a queue by setting setSuspended:YES. This will suspend all operations in a queue — you can’t suspend operations individually. To resume the queue, simply setSuspended:NO.
    // Suspend a queue
    [myQueue setSuspended:YES];
    .
    .
    .
    // Resume a queue
    [myQueue setSuspended: NO];
  • Cancel operations: to cancel all operations in a queue, you simply call “cancelAllOperations”. Do you remember earlier where it was noted that your code should frequently check for isCancelled property in NSOperation?

    The reason is that “cancelAllOperations” calls “cancel” on every operation in the queue — it doesn’t do anything magical! :] If an operation has not yet started, and you call “cancel” on it, the operation will be cancelled and removed from the queue. However, if an operation is already executing, it is up to that individual operation to recognize the cancellation (by checking the isCancelled property) and stop what it is doing.

    [myQueue cancelAllOperations];
  • addOperationWithBlock: if you have a simple operation that does not need to be subclassed, you can create an operation using the block API. If you want to reference any object from outside in the block, remember that you should pass in a weak reference. Also, if you want to do something that is related to the UI in the block, you must do it on the main thread:
     
    // Create a weak reference
    __weak MyViewController *weakSelf = self;
     
    // Add an operation as a block to a queue
    [myQueue addOperationWithBlock: ^ {
     
        NSURL *aURL = [NSURL URLWithString:@"http://www.somewhere.com/image.png"];
        NSError *error = nil;
        NSData *data = [NSData dataWithContentsOfURL:aURL options:nil error:&error];
        UIImage *image = nil;
        If (data)
            image = [UIImage imageWithData:data];
     
        // Update UI on the main thread.
        [[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
            weakSelf.imageView.image = image;
        }];
     
    }];

Redefined model

It is time to redefine the preliminary non-threaded model! If you take a closer look at the preliminary model, you see that there are three thread-bogging areas that can be improved. By separating these three areas and placing them in a separate thread, the main thread will be relieved and it can stay responsive to user interactions.

Note: if you can’t immediately see why your app is running so slow — and sometimes it isn’t obvious — you should use Instruments. However, that’s another whole tutorial unto itself! :]

Improved model

To get rid of your application bottlenecks, you’ll need a thread specifically to respond to user interactions, a thread dedicated to downloading data source and images, and a thread for performing image filtering. In the new model, the app starts on the main thread and loads an empty table view. At the same time, the app launches a second thread to download the data source.

Once the data source has been downloaded, you’ll tell the table view to reload itself. This is done on the main thread. At this point, table view knows how many rows it has, and it knows the URL of the images it needs to display, but it doesn’t have the actual images yet! If you immediately started to download all images at this point, it would be terribly inefficient, as you don’t need all the images at once!

What can be done to make this better?

A better model is just to start downloading the images whose respective rows are visible on the screen. So your code will first ask the table view which rows are visible, and only then will it start the download process. As well, the image filtering process can’t be started before the image is completely downloaded. Therefore, the code should not start the image filtering process until there is an unfiltered image waiting to be processed.

To make the app appear more responsive, the code will display the image once it is downloaded without waiting for the filtering process. Once the image filtering is complete, update the UI to display the filtered image. The diagram below shows the schematic control flow for this process:

How To Use NSOperations and NSOperationQueues_第3张图片

Control Flow

To achieve these objectives, you will need to track whether the image is currently being downloaded, is finished being downloaded, or if the image filtering has been applied. You will also need to track the status of each operation, and whether it is a downloading or filtering operation, so that you can cancel, pause or resume each as the user scrolls.

Okay! Now you’re ready to get coding! :]

Open the project where you left it off, and add a new NSObject subclass to your project namedPhotoRecord. Open PhotoRecord.h, and add the followings to the header file:

#import <UIKit/UIKit.h> // because we need UIImage
 
@interface PhotoRecord : NSObject
 
@property (nonatomic, strong) NSString *name;  // To store the name of image
@property (nonatomic, strong) UIImage *image; // To store the actual image
@property (nonatomic, strong) NSURL *URL; // To store the URL of the image
@property (nonatomic, readonly) BOOL hasImage; // Return YES if image is downloaded.
@property (nonatomic, getter = isFiltered) BOOL filtered; // Return YES if image is sepia-filtered 
@property (nonatomic, getter = isFailed) BOOL failed; // Return Yes if image failed to be downloaded
 
@end

Does the syntax above look familiar? Each property has a getter and setter. Specifying the getter like this in the property specification merely makes the naming of the getter method explicit.

Switch to PhotoRecord.m, and add the following:

@implementation PhotoRecord
 
@synthesize name = _name;
@synthesize image = _image;
@synthesize URL = _URL;
@synthesize hasImage = _hasImage;
@synthesize filtered = _filtered;
@synthesize failed = _failed;
 
 
- (BOOL)hasImage {
    return _image != nil;
}
 
 
- (BOOL)isFailed {
    return _failed;
}
 
 
- (BOOL)isFiltered {
    return _filtered;
}
 
@end

To track status of each operation, you’ll need a separate class. Create another new subclass of NSObject named PendingOperations. Switch to PendingOperations.h and add the following:

#import <Foundation/Foundation.h>
 
@interface PendingOperations : NSObject
 
@property (nonatomic, strong) NSMutableDictionary *downloadsInProgress;
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
 
@property (nonatomic, strong) NSMutableDictionary *filtrationsInProgress;
@property (nonatomic, strong) NSOperationQueue *filtrationQueue;
 
@end

This one is also simple. You declare two dictionaries to keep track of active and pending downloads and filtering. The dictionary keys reference the indexPath of table view rows, and the dictionary values are going to be the separate instances of ImageDownloader and ImageFiltration.

Note:You might wonder why you have to keep track of all active and pending operations. Isn’t it possible to simply access them by making an inquiry to [NSOperationQueue operations]? Well, yes, but in this project it won’t be very efficient to do so.

Every time that you need to compare the indexPath of visible rows with the indexPath of rows that have a pending operation, you would need to use several iterative loops, which is an expensive operation. By declaring an extra instance of NSDictionary, you can conveniently keep track of pending operations without the need to perform inefficient loop operations.

Switch to PendingOperations.m and add the following:

@implementation PendingOperations
@synthesize downloadsInProgress = _downloadsInProgress;
@synthesize downloadQueue = _downloadQueue;
 
@synthesize filtrationsInProgress = _filtrationsInProgress;
@synthesize filtrationQueue = _filtrationQueue;
 
 
- (NSMutableDictionary *)downloadsInProgress {
    if (!_downloadsInProgress) {
        _downloadsInProgress = [[NSMutableDictionary alloc] init];
    }
    return _downloadsInProgress;
}
 
- (NSOperationQueue *)downloadQueue {
    if (!_downloadQueue) {
        _downloadQueue = [[NSOperationQueue alloc] init];
        _downloadQueue.name = @"Download Queue";
        _downloadQueue.maxConcurrentOperationCount = 1;
    }
    return _downloadQueue;
}
 
- (NSMutableDictionary *)filtrationsInProgress {
    if (!_filtrationsInProgress) {
        _filtrationsInProgress = [[NSMutableDictionary alloc] init];
    }
    return _filtrationsInProgress;
}
 
- (NSOperationQueue *)filtrationQueue {
    if (!_filtrationQueue) {
        _filtrationQueue = [[NSOperationQueue alloc] init];
        _filtrationQueue.name = @"Image Filtration Queue";
        _filtrationQueue.maxConcurrentOperationCount = 1;
    }
    return _filtrationQueue;
}
 
@end

Here, you override some getters to take advantage of lazy instantiation, so that you don’t actually allocate instance variables until they are accessed. You also initialize and allocate two queues — one for downloading operations, one for filtering — and set their properties, so that when you access them in another class, you don’t have to worry about their initialization. The maxConcurrentOperationCount is set to 1 here for the sake of this tutorial.

Now, it’s time to take care of download and filtration operations. Create a new subclass of NSOperation named ImageDownloader. Switch to ImageDownloader.h, and add the following:

#import <Foundation/Foundation.h>
 
// 1
#import "PhotoRecord.h"
 
// 2
@protocol ImageDownloaderDelegate;
 
@interface ImageDownloader : NSOperation
 
@property (nonatomic, assign) id <ImageDownloaderDelegate> delegate;
 
// 3
@property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readonly, strong) PhotoRecord *photoRecord;
 
// 4
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>) theDelegate;
 
@end
 
@protocol ImageDownloaderDelegate <NSObject>
 
// 5
- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader;
@end

Here’s what’s happening at each of the numbered comments in the code above:

  1. Import PhotoRecord.h so that you can independently set the image property of a PhotoRecord once it is successfully downloaded. If downloading fails, set its failed value to YES.
  2. Declare a delegate so that you can notify the caller once the operation is finished.
  3. Declare indexPathInTableView for convenience so that once the operation is finished, the caller has a reference to where this operation belongs to.
  4. Declare a designated initializer.
  5. In your delegate method, pass the operation back as an object. The caller can access both indexPathInTableView and photoRecord.

Switch to ImageDownloader.m and make the following changes:

 
// 1
@interface ImageDownloader ()
@property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readwrite, strong) PhotoRecord *photoRecord;
@end
 
 
@implementation ImageDownloader
@synthesize delegate = _delegate;
@synthesize indexPathInTableView = _indexPathInTableView;
@synthesize photoRecord = _photoRecord;
 
#pragma mark -
#pragma mark - Life Cycle
 
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>)theDelegate {
 
    if (self = [super init]) {
        // 2
        self.delegate = theDelegate;
        self.indexPathInTableView = indexPath;
        self.photoRecord = record;
    }
    return self;
}
 
#pragma mark -
#pragma mark - Downloading image
 
// 3
- (void)main {
 
    // 4
    @autoreleasepool {
 
        if (self.isCancelled)
            return;
 
        NSData *imageData = [[NSData alloc] initWithContentsOfURL:self.photoRecord.URL];
 
        if (self.isCancelled) {
            imageData = nil;
            return;
        }
 
        if (imageData) {
            UIImage *downloadedImage = [UIImage imageWithData:imageData];
            self.photoRecord.image = downloadedImage;
        }
        else {
            self.photoRecord.failed = YES;
        }
 
        imageData = nil;
 
        if (self.isCancelled)
            return;
 
        // 5
        [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:) withObject:self waitUntilDone:NO];
 
    }
}
 
@end

Stepping through the numbered comments, you’ll see that the code does the following:

  1. Declare a private interface, so you can change the attributes of properties to read-write.
  2. Set the properties.
  3. Regularly check for isCancelled, to make sure the operation terminates as soon as possible.
  4. Apple recommends using @autoreleasepool block instead of alloc and init NSAutoreleasePool, because blocks are more efficient. You might use NSAuoreleasePool instead and that would be fine.
  5. Notify the caller on the main thread.

Now, go ahead and create a subclass of NSOperation to take care of image filtering!

Create another new subclass of NSOperation named ImageFiltration. Open ImageFiltration.h, and add the following:

 
// 1
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
#import "PhotoRecord.h"
 
// 2
@protocol ImageFiltrationDelegate;
 
@interface ImageFiltration : NSOperation
 
@property (nonatomic, weak) id <ImageFiltrationDelegate> delegate;
@property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readonly, strong) PhotoRecord *photoRecord;
 
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate;
 
@end
 
@protocol ImageFiltrationDelegate <NSObject>
- (void)imageFiltrationDidFinish:(ImageFiltration *)filtration;
@end

Once again, here’s what the above code is doing:

  1. Since you need to perform filtering on the UIImage instance, you need to import both UIKit and CoreImage frameworks. You also need to import PhotoRecord. Similar to ImageDownloader, you want the caller to alloc and init using the designated initializer.
  2. Declare a delegate to notify the caller once its operation is finished.

Switch to ImageFiltration.m and add the following code:

 
@interface ImageFiltration ()
@property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readwrite, strong) PhotoRecord *photoRecord;
@end
 
 
@implementation ImageFiltration
@synthesize indexPathInTableView = _indexPathInTableView;
@synthesize photoRecord = _photoRecord;
@synthesize delegate = _delegate;
 
#pragma mark -
#pragma mark - Life cycle
 
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate {
 
    if (self = [super init]) {
        self.photoRecord = record;
        self.indexPathInTableView = indexPath;
        self.delegate = theDelegate;
    }
    return self;
}
 
 
#pragma mark -
#pragma mark - Main operation
 
 
- (void)main {
    @autoreleasepool {
 
        if (self.isCancelled)
            return;
 
        if (!self.photoRecord.hasImage)
            return;
 
        UIImage *rawImage = self.photoRecord.image;
        UIImage *processedImage = [self applySepiaFilterToImage:rawImage];
 
        if (self.isCancelled)
            return;
 
        if (processedImage) {
            self.photoRecord.image = processedImage;
            self.photoRecord.filtered = YES;
            [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageFiltrationDidFinish:) withObject:self waitUntilDone:NO];
        }
    }
 
}
 
#pragma mark -
#pragma mark - Filtering image
 
 
- (UIImage *)applySepiaFilterToImage:(UIImage *)image {
 
    // This is expensive + time consuming
    CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)];
 
    if (self.isCancelled)
        return nil;
 
    UIImage *sepiaImage = nil;
    CIContext *context = [CIContext contextWithOptions:nil];
    CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil];
    CIImage *outputImage = [filter outputImage];
 
    if (self.isCancelled)
        return nil;
 
    // Create a CGImageRef from the context
    // This is an expensive + time consuming
    CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]];
 
    if (self.isCancelled) {
        CGImageRelease(outputImageRef);
        return nil;
    }
 
    sepiaImage = [UIImage imageWithCGImage:outputImageRef];
    CGImageRelease(outputImageRef);
    return sepiaImage;
}
 
@end

The implementation above is very similar to ImageDownloader. The image filtering is the same method implementation you used previously in ListViewController.m. It’s been moved here so that it can be done as a separate operation in the background. You should check for isCancelled very frequently; a good practice is to call it before and after any expensive method call. Once the filtering is done, the values of PhotoRecord instance are set appropriately, and then the delegate on the main thread is notified.

Great! Now you have all the tools and foundation you need in order to process operations in a background thread. It’s time to go back to the view controller and modify it appropriately, so that it can take advantage of all these new benefits.

Note: Before moving on, you’ll want to download the AFNetworking library from GitHub.

The AFNetworking library is built upon NSOperation and NSOperationQueue. It provides you with lots of convenient methods so that you don’t have to create your own operations for common tasks like downloading a file in the background.

When it comes to downloading a file from the internet, it’s good practice to have some code in place to check for errors.You can never assume that there is going to be a reliable constant internet connection.

Apple provides the NSURLConnection class for this purpose. Using that can be extra work. AFNetworking is an open source library that provides a very convenient way to do such tasks. You pass in two blocks, one for when the operation finishes successfully, and one for the time the operation fails. You will see it in action a little later on.

To add the library to your project, select File > Add Files To …, then browse and select the folder where you downloaded AFNetworking, and finally click “Add”. Make sure to tick the box “Copy items into destination group’s folder”!

Switch to ListViewController.h and update the header file as follows:

 
// 1
#import <UIKit/UIKit.h>
// #import <CoreImage/CoreImage.h> ... you don't need CoreImage here anymore.
#import "PhotoRecord.h"
#import "PendingOperations.h"
#import "ImageDownloader.h"
#import "ImageFiltration.h"
// 2
#import "AFNetworking/AFNetworking.h"
 
#define kDatasourceURLString @"https://sites.google.com/site/soheilsstudio/tutorials/nsoperationsampleproject/ClassicPhotosDictionary.plist"
 
// 3
@interface ListViewController : UITableViewController <ImageDownloaderDelegate, ImageFiltrationDelegate>
 
// 4
@property (nonatomic, strong) NSMutableArray *photos; // main data source of controller
 
// 5
@property (nonatomic, strong) PendingOperations *pendingOperations;
@end

What’s going on here? The following points explain the code above:

  1. You can delete CoreImage from ListViewController header, since you don’t need it anymore. However, you do need to import PhotoRecord.h, PendingOperations.h, ImageDownloader.h and ImageFiltration.h.
  2. Here’s the reference to the AFNetworking library.
  3. Make sure to make ListViewController compliant to ImageDownloader and ImageFiltration delegate methods.
  4. You don’t need the data source as-is. You are going to create instances of PhotoRecord using the property list. So, change the class of “photos” from NSDictionary to NSMutableArray, so that you can update the array of photos.
  5. This property is used to track pending operations.

Switch to ListViewController.m, and update it as follows:

// Add this to the beginning of ListViewController.m
@synthesize pendingOperations = _pendingOperations;
.
.
.
// Add this to viewDidUnload
[self setPendingOperations:nil];

Right before the lazy instantiation of “photos”, add a lazy instantiation of “pendingOperations”:

- (PendingOperations *)pendingOperations {
    if (!_pendingOperations) {
        _pendingOperations = [[PendingOperations alloc] init];
    }
    return _pendingOperations;
}

Go to the lazy instantiation of “photos” and modify it as follows:

- (NSMutableArray *)photos {
 
    if (!_photos) {
 
        // 1
        NSURL *datasourceURL = [NSURL URLWithString:kDatasourceURLString];
        NSURLRequest *request = [NSURLRequest requestWithURL:datasourceURL];
 
       // 2
        AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
 
       // 3
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
 
       // 4
        [datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
 
            // 5
            NSData *datasource_data = (NSData *)responseObject;
            CFPropertyListRef plist =  CFPropertyListCreateFromXMLData(kCFAllocatorDefault, (__bridge CFDataRef)datasource_data, kCFPropertyListImmutable, NULL);
 
            NSDictionary *datasource_dictionary = (__bridge NSDictionary *)plist;
 
            // 6
            NSMutableArray *records = [NSMutableArray array];
 
            for (NSString *key in datasource_dictionary) {
                PhotoRecord *record = [[PhotoRecord alloc] init];
                record.URL = [NSURL URLWithString:[datasource_dictionary objectForKey:key]];
                record.name = key;
                [records addObject:record];
                record = nil;
            }
 
            // 7
            self.photos = records;
 
            CFRelease(plist);
 
            [self.tableView reloadData];
            [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
 
        } failure:^(AFHTTPRequestOperation *operation, NSError *error){
 
            // 8
            // Connection error message
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Oops!"
                                                            message:error.localizedDescription
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
            [alert show];
            alert = nil;
            [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        }];
 
        // 9
        [self.pendingOperations.downloadQueue addOperation:datasource_download_operation];
    }
    return _photos;
}

There’s a fair bit going on in the code above. Stepping through the commented sections one by one, here’s what the code above has accomplished:

  1. Create a NSURL and a NSURLRequest to point to the location of the data source.
  2. Use AFHTTPRequestOperation class, alloc and init it with the request.
  3. Give the user feedback, while downloading the data source by enabling network activity indicator.
  4. By using setCompletionBlockWithSuccess:failure:, you can add two blocks: one for the case where the operation finishes successfully, and one for the case where it fails.
  5. In the success block, download the property list as NSData, and then by using toll-free bridging for data into CFDataRef and CFPropertyList, convert it into NSDictionary.
  6. Create a NSMutableArray and iterate through all objects and keys in the dictionary, create a PhotoRecord instance, and store it in the array.
  7. Once you are done, point the _photo to the array of records, reload the table view and stop the network activity indicator. You also release the “plist” instance variable.
  8. In case you are not successful, you display a message to notify the user.
  9. Finally, add “datasource_download_operation” to “downloadQueue” of PendingOperations.

Go to tableView:cellForRowAtIndexPath: and modify it as follows:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *kCellIdentifier = @"Cell Identifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
 
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
 
        // 1
        UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        cell.accessoryView = activityIndicatorView;
 
    }
 
    // 2
    PhotoRecord *aRecord = [self.photos objectAtIndex:indexPath.row];
 
    // 3
    if (aRecord.hasImage) {
 
        [((UIActivityIndicatorView *)cell.accessoryView) stopAnimating];
        cell.imageView.image = aRecord.image;
        cell.textLabel.text = aRecord.name;
 
    }
    // 4
    else if (aRecord.isFailed) {
        [((UIActivityIndicatorView *)cell.accessoryView) stopAnimating];
        cell.imageView.image = [UIImage imageNamed:@"Failed.png"];
        cell.textLabel.text = @"Failed to load";
 
    }
    // 5
    else {
 
        [((UIActivityIndicatorView *)cell.accessoryView) startAnimating];
        cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];
        cell.textLabel.text = @"";
        [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];
    }
 
    return cell;
}

Again, take some time to read through the explanation of the commented sections below:

  1. To provide feedback to the user, create a UIActivityIndicatorView and set it as the cell’s accessory view.
  2. The data source contains instances of PhotoRecord. Get a hold of each of them based on the indexPath of the row.
  3. Inspect the PhotoRecord. If its image is downloaded, display the image, the image name, and stop the activity indicator.
  4. If downloading the image has failed, display a placeholder to display the failure, and stop the activity indicator.
  5. Otherwise, the image has not been downloaded yet. Start the download and filtering operations (they’re not yet implemented), and display a placeholder that indicates you are working on it. Start the activity indicator to show user something is going on.

Now it’s time to implement the method that will take care of starting operations. You can delete the old implementation of “applySepiaFilterToImage:” in ListViewController.m, if you haven’t done so already.

Go all the way to the end of your code, and implement the following method:

 
// 1
- (void)startOperationsForPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {
 
    // 2
    if (!record.hasImage) {
        [self startImageDownloadingForRecord:record atIndexPath:indexPath];
 
    }
 
    if (!record.isFiltered) {
        [self startImageFiltrationForRecord:record atIndexPath:indexPath];
    }
}

The above code is pretty straightforward, but here’s some explanation as to what it’s doing:

  1. To keep it simple, you pass in an instance of PhotoRecord that requires operations, along with its indexPath.
  2. You inspect it to see whether it has an image. If it does not have an image, start downloading the image by calling startImageDownloadingForRecord:atIndexPath: (which will be implemented shortly). You’ll do the same for filtering operations: if the image has not yet been filtered, call startImageFiltrationForRecord:atIndexPath: (which will also be implemented shortly).

Note: the methods for downloading and filtering images are implemented separately, as there is a possibility that while an image is being downloaded the user can scroll away, and you won’t yet have applied the image filter. So next time the user comes to the same row, you don’t need to re-download the image; you only need to apply the image filter! Efficiency rocks! :]

Now you need to implement startImageDownloadingForRecord:atIndexPath: that you called in the code snippets above. Remember that you created a custom class, PendingOperations, to keep track of operations. Here, you actually get to use it:

- (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {
    // 1
    if (![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPath]) {
 
        // 2 
        // Start downloading
        ImageDownloader *imageDownloader = [[ImageDownloader alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];
        [self.pendingOperations.downloadsInProgress setObject:imageDownloader forKey:indexPath];
        [self.pendingOperations.downloadQueue addOperation:imageDownloader];
    }
}
 
- (void)startImageFiltrationForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {
    // 3
    if (![self.pendingOperations.filtrationsInProgress.allKeys containsObject:indexPath]) {
 
        // 4
        // Start filtration
        ImageFiltration *imageFiltration = [[ImageFiltration alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];
 
        // 5
        ImageDownloader *dependency = [self.pendingOperations.downloadsInProgress objectForKey:indexPath];
        if (dependency)
            [imageFiltration addDependency:dependency];
 
        [self.pendingOperations.filtrationsInProgress setObject:imageFiltration forKey:indexPath];
        [self.pendingOperations.filtrationQueue addOperation:imageFiltration];
    }
}

Okay! Here’s a quick list to make sure you understand what’s going on in the code above.

  1. First, check for the particular indexPath to see if there is already an operation in downloadsInProgress for it. If so, ignore it.
  2. If not, create an instance of ImageDownloader by using the designated initializer, and set ListViewController as the delegate. Pass in the appropriate indexPath and a pointer to the instance of PhotoRecord, and then add it to the download queue. You also add it to downloadsInProgress to help keep track of things.
  3. Similarly, check to see if there is any filtering operations going on for the particular indexPath.
  4. If not, start one by using the designated initializer.
  5. This one is a little tricky. You first must check to see if this particular indexPath has a pending download; if so, you make this filtering operation dependent on that. Otherwise, you don’t need dependency.

Great! You now need to implement the delegate methods of ImageDownloader and ImageFiltration. Add the following at the end of ListViewController.m:

- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader {
 
    // 1
    NSIndexPath *indexPath = downloader.indexPathInTableView;
    // 2
    [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
    // 3
    [self.pendingOperations.downloadsInProgress removeObjectForKey:indexPath];
}
 
- (void)imageFiltrationDidFinish:(ImageFiltration *)filtration {
    NSIndexPath *indexPath = filtration.indexPathInTableView;    
    [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
    [self.pendingOperations.filtrationsInProgress removeObjectForKey:indexPath];
}

Both delegate methods have very similar implementations, so there’s only a need to go over one of them:

  1. Check for the indexPath of the operation, whether it is a download, or filtration.
  2. Update UI.
  3. Remove the operation from downloadsInProgress (or filtrationsInProgress).

Update: “xlledo” from the forums made a good point in regard to handling instances of PhotoRecord. Because you are passing a pointer to PhotoRecord to NSOperation subclasses (ImageDownloader and ImageFiltration), you modify them directly. Therefore, replaceObjectAtIndex:withObject: is redundant and not needed.

Kudos!

Wow! You made it! Your project is complete. Build and run to see your improvements in action! As you scroll through the table view, the app doesn’t stall anymore, and starts downloading images and filtering them as they become visible.

Isn’t that cool? You can see how a little effort can go a long way towards making your applications a lot more responsive — and a lot more fun for the user!

Fine tuning

You’ve come a long way in this tutorial! Your little project is responsive and shows lots of improvement over the original version. However, there are still some small details that are left to take care of. You want to be a great programmer, not just a good one!

You may have noticed that as you scroll away in table view, those offscreen cells are still in the process of being downloaded and filtered. Didn’t you put cancellation provisions in your code? Yes, you did — you should probably make use of them! :]

Go back to Xcode, and switch to ListViewController.m. Go to the implementation of tableView:cellForRowAtIndexPath:, and wrap [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath]; in an if-clause as follows:

 
// in implementation of tableView:cellForRowAtIndexPath:
if (!tableView.dragging && !tableView.decelerating) {
    [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];
}

You tell the table view to start operations only if the table view is not scrolling. These are actually properties of UIScrollView, and because UITableView is a subclass of UIScrollView, you automatically inherit these properties.

Now, go to the end of ListViewController.m and implement the following UIScrollView delegate methods:

#pragma mark -
#pragma mark - UIScrollView delegate
 
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // 1
    [self suspendAllOperations];
}
 
 
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    // 2
    if (!decelerate) {
        [self loadImagesForOnscreenCells];
        [self resumeAllOperations];
    }
}
 
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // 3
    [self loadImagesForOnscreenCells];
    [self resumeAllOperations];
}

A quick walkthrough of the code above shows the following:

  1. As soon as the user starts scrolling, you will want to suspend all operations and take a look at what the user wants to see. You will implement suspendAllOperations in just a moment.
  2. If the value of decelerate is NO, that means the user stopped dragging the table view. Therefore you want to resume suspended operations, cancel operations for offscreen cells, and start operations for onscreen cells. You will implement loadImagesForOnscreenCells and resumeAllOperations in a little while as well.
  3. This delegate method tells you that table view stopped scrolling, so you will do the same as in #2.

Add the implementation of suspendAllOperations, resumeAllOperations, loadImagesForOnscreenCells to the very end of ListViewController.m:

 
#pragma mark - Cancelling, suspending, resuming queues / operations
 
- (void)suspendAllOperations {
    [self.pendingOperations.downloadQueue setSuspended:YES];
    [self.pendingOperations.filtrationQueue setSuspended:YES];
}
 
- (void)resumeAllOperations {
    [self.pendingOperations.downloadQueue setSuspended:NO];
    [self.pendingOperations.filtrationQueue setSuspended:NO];
}
 
- (void)cancelAllOperations {
    [self.pendingOperations.downloadQueue cancelAllOperations];
    [self.pendingOperations.filtrationQueue cancelAllOperations];
}
 
- (void)loadImagesForOnscreenCells {
 
    // 1
    NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]];
 
    // 2
    NSMutableSet *pendingOperations = [NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgress allKeys]];
    [pendingOperations addObjectsFromArray:[self.pendingOperations.filtrationsInProgress allKeys]];
 
    NSMutableSet *toBeCancelled = [pendingOperations mutableCopy];
    NSMutableSet *toBeStarted = [visibleRows mutableCopy];
 
    // 3
    [toBeStarted minusSet:pendingOperations];
    // 4
    [toBeCancelled minusSet:visibleRows];
 
    // 5
    for (NSIndexPath *anIndexPath in toBeCancelled) {
 
        ImageDownloader *pendingDownload = [self.pendingOperations.downloadsInProgress objectForKey:anIndexPath];
        [pendingDownload cancel];
        [self.pendingOperations.downloadsInProgress removeObjectForKey:anIndexPath];
 
        ImageFiltration *pendingFiltration = [self.pendingOperations.filtrationsInProgress objectForKey:anIndexPath];
        [pendingFiltration cancel];
        [self.pendingOperations.filtrationsInProgress removeObjectForKey:anIndexPath];
    }
    toBeCancelled = nil;
 
    // 6
    for (NSIndexPath *anIndexPath in toBeStarted) {
 
        PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row];
        [self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath];
    }
    toBeStarted = nil;
 
}

suspendAllOperations, resumeAllOperations and cancelAllOperations have a straightforward implementation. You basically use factory methods to suspend, resume or cancel operations and queues. For convenience, you put them together in separate methods.

LoadImagesForOnscreenCells is little complex. Here’s what’s going on:

  1. Get a set of visible rows.
  2. Get a set of all pending operations (download and filtration).
  3. Rows (or indexPaths) that need an operation = visible rows – pendings.
  4. Rows (or indexPaths) that their operations should be cancelled = pendings – visible rows.
  5. Loop through those to be cancelled, cancel them, and remove their reference from PendingOperations.
  6. Loop through those to be started, and call startOperationsForPhotoRecord:atIndexPath: for each.

And finally, the last piece of this puzzle is solved by didReceiveMemoryWarning of ListViewController.m.

 
// If app receive memory warning, cancel all operations
- (void)didReceiveMemoryWarning {
    [self cancelAllOperations];
    [super didReceiveMemoryWarning];
}

Build and run and you should have a more responsive, and better resource-managed application! Give yourself a round of applause!

How To Use NSOperations and NSOperationQueues_第4张图片

ClassicPhotos (improved version)

Where To Go From Here?

Here is the complete improved project.

If you completed this project and took the time to really understand it, congratulations! You can consider yourself a much more valuable iOS developer than you were at the beginning of this tutorial! Most development shops are lucky to have one or two people that really know this stuff.

But beware — like deeply-nested blocks, gratuitous use of threads can make a project incomprehensible to people who have to maintain your code. Threads can introduce subtle bugs that may never appear until your network is slow, or the code is run on a faster (or slower) device, or one with a different number of cores. Test very carefully, and always use Instruments (or your own observations) to verify that introducing threads really has made an improvement.

If you have any comments or questions about this tutorial or NSOperations in general, please join the forum discussion below!

你可能感兴趣的:(Opera)