NSOperations and NSOperationQueues学习笔记

一、GCD VS NSOperation and NSOperationQueue

一、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:

  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 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:

  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!

ClassicPhotos (improved version).png

文摘来源:

  1. How To Use NSOperations and NSOperationQueues
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容