一、GCD VS NSOperation and NSOperationQueue
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).
二、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:
- 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.
- 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:
- Subclass NSOperation
- Override “main”
- Create an “autoreleasepool” in “main”
- 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 for isCancelled 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. See http://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;
}];
}];
五、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:
- 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.
- 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.
- 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:
- Get a set of visible rows.
- Get a set of all pending operations (download and filtration).
- Rows (or indexPaths) that need an operation = visible rows – pendings.
- Rows (or indexPaths) that their operations should be cancelled = pendings – visible rows.
- Loop through those to be cancelled, cancel them, and remove their reference from PendingOperations.
- 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!