NSOperations and NSOperationQueues学习笔记

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

文摘来源:

1. How To Use NSOperations and NSOperationQueues

时间: 2024-11-11 09:52:22

NSOperations and NSOperationQueues学习笔记的相关文章

vector 学习笔记

vector 使用练习: /**************************************** * File Name: vector.cpp * Author: sky0917 * Created Time: 2014年04月27日 11:07:33 ****************************************/ #include <iostream> #include <vector> using namespace std; int main

Caliburn.Micro学习笔记(一)----引导类和命名匹配规则

Caliburn.Micro学习笔记(一)----引导类和命名匹配规则 用了几天时间看了一下开源框架Caliburn.Micro 这是他源码的地址http://caliburnmicro.codeplex.com/ 文档也写的很详细,自己在看它的文档和代码时写了一些demo和笔记,还有它实现的原理记录一下 学习Caliburn.Micro要有MEF和MVVM的基础 先说一下他的命名规则和引导类 以后我会把Caliburn.Micro的 Actions IResult,IHandle ICondu

jQuery学习笔记(一):入门

jQuery学习笔记(一):入门 一.JQuery是什么 JQuery是什么?始终是萦绕在我心中的一个问题: 借鉴网上同学们的总结,可以从以下几个方面观察. 不使用JQuery时获取DOM文本的操作如下: 1 document.getElementById('info').value = 'Hello World!'; 使用JQuery时获取DOM文本操作如下: 1 $('#info').val('Hello World!'); 嗯,可以看出,使用JQuery的优势之一是可以使代码更加简练,使开

[原创]java WEB学习笔记93:Hibernate学习之路---Hibernate 缓存介绍,缓存级别,使用二级缓存的情况,二级缓存的架构集合缓存,二级缓存的并发策略,实现步骤,集合缓存,查询缓存,时间戳缓存

本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱好者,互联网技术发烧友 微博:伊直都在0221 QQ:951226918 -----------------------------------------------------------------------------------------------------------------

Activiti 学习笔记记录(三)

上一篇:Activiti 学习笔记记录(二) 导读:上一篇学习了bpmn 画图的常用图形标记.那如何用它们组成一个可用文件呢? 我们知道 bpmn 其实是一个xml 文件

HTML&CSS基础学习笔记8-预格式文本

<pre>标签的主要作用是预格式化文本.被包围在 pre 标签中的文本通常会保留空格和换行符.而文本也会呈现为等宽字体. <pre>标签的一个常见应用就是用来表示计算机的源代码.当然你也可以在你需要在网页中预显示格式时使用它. 会使你的文本换行的标签(例如<h>.<p>)绝不能包含在 <pre> 所定义的块里.尽管有些浏览器会把段落结束标签解释为简单地换行,但是这种行为在所有浏览器上并不都是一样的. 更多学习内容,就在码芽网http://www.

java/android 设计模式学习笔记(14)---外观模式

这篇博客来介绍外观模式(Facade Pattern),外观模式也称为门面模式,它在开发过程中运用频率非常高,尤其是第三方 SDK 基本很大概率都会使用外观模式.通过一个外观类使得整个子系统只有一个统一的高层的接口,这样能够降低用户的使用成本,也对用户屏蔽了很多实现细节.当然,在我们的开发过程中,外观模式也是我们封装 API 的常用手段,例如网络模块.ImageLoader 模块等.其实我们在开发过程中可能已经使用过很多次外观模式,只是没有从理论层面去了解它. 转载请注明出处:http://bl

[原创]java WEB学习笔记48:其他的Servlet 监听器:域对象中属性的变更的事件监听器 (3 个),感知 Session 绑定的事件监听器(2个)

本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱好者,互联网技术发烧友 微博:伊直都在0221 QQ:951226918 ---------------------------------

java/android 设计模式学习笔记(10)---建造者模式

这篇博客我们来介绍一下建造者模式(Builder Pattern),建造者模式又被称为生成器模式,是创造性模式之一,与工厂方法模式和抽象工厂模式不同,后两者的目的是为了实现多态性,而 Builder 模式的目的则是为了将对象的构建与展示分离.Builder 模式是一步一步创建一个复杂对象的创建型模式,它允许用户在不知道内部构建细节的情况下,可以更精细地控制对象的构造流程.一个复杂的对象有大量的组成部分,比如汽车它有车轮.方向盘.发动机.以及各种各样的小零件,要将这些部件装配成一辆汽车,这个装配过