iOS多线程编程--NSOperation

Posted 乌戈勒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS多线程编程--NSOperation相关的知识,希望对你有一定的参考价值。

前言:

ios支持多个层次的多线程编程,层次越高的抽象程度越高,使用也越方便。

根据抽象层次从低到高依次列出iOS所支持的多线程编程方法:

1.Thread :是三种方法里面相对轻量级的,但需要管理线程的生命周期、同步、加锁问题,
这会导致一定的性能开销。

2.Cocoa Operations:是基于OC实现的,NSOperation以面向对象的方式封装了需要执行的操作,不必关心线程管理、同步等问题。
NSOperation是一个抽象基类,我们需要实例化NSOperation的具体子类来进行多线程开发。

3.Grand Central Dispatch:简称GCD,iOS4才开始支持.
提供了一些新特性、运行库来支持多核并行编程,它的关注点更高:如何在多个cpu上提升效率

了解GCD点这里


下面来分析一下NSOPeration的用法

一、NSOPeration

1、简介

NSOperation对象封装了需要执行的操作,以及操作所需的数据,并且能够以并发或者非并发的方式执行这个操作。

NSOperation本身是一个抽象基类,相当于一个独立的计算单元,它为子类提供了有用且线程安全的建立状态,优先级,依赖和取消等操作。

既然是一个抽象基类,所以我们只能使用它的子类进行多线程的开发,它的子类有两种方式:

1、Foundation框架提供的2个具体子类:NSInvocationOperation和NSBlockOperation。
2、自定义子类继承NSOperation,需要我们自己实现内部相应的方法。

使用Operation的目的就是为了让开发人员不再关心线程。


2、执行操作

  • 执行一个operation有两种方法:

    1、手动调用start这个方法:
    这种方法调用NSOperation对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行,
    所以在主线程里面一定要小心的调用,不然就会把主线程给卡死。
    
    2、将operation添加到operationQueue中:
    operation会在我们添加进NSOperationQueue的时候,被系统尽快取出执行,按异步方式进行。
    
  • NSOperation和NSOperationQueue实现多线程的具体步骤:

    1、先将需要执行的操作封装到一个NSOperation对象中;
    2、然后将NSOperation对象添加到NSOperationQueue中;
    3、系统会⾃动将NSOperationQueue中的NSOperation取出来;
    4、将取出的NSOperation封装的操作放到⼀条新线程中执⾏。
    
  • 注意:

    1、操作对象默认在主线程中执行,只有添加到队列中才会开启新的线程。
    默认情况下,如果操作没有放到队列中,都是同步执行。只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作 。
    
    2、NSOperation对象的isConcurrent方法会告诉我们这个操作相对于调用start方法的线程,是同步还是异步执行。
    isConcurrent方法默认返回NO,表示操作与调用线程同步执行。
    

3、队列是怎么调用我们执行的操作?

调用方式有两种:

1、同步调用:
如果你只是想弄一个同步的方法,那很简单,你只要重写main这个函数,在里面添加你要的操作。

2、异步调用:
如果想定义异步的方法的话就重写start方法,在你添加进operationQueue中的时候系统将自动调用你这个start方法,
这时将不再调用main里面的方法。

4、取消操作

operation开始执行之后,默认会一直执行操作直到完成,我们也可以调用cancel方法中途取消一个操作的执行,当然,这时候操作并不是我们所想象的被取消了。

[operation cancel]; 
  • 这个取消主要发生在两种不同的阶段:

    1、这个操作在队列中还没有开始执行:
    这个时候取消队列中的任务,并将状态finished设置为YES,这样的取消就是直接取消了。
    
    2、这个操作在队列中处于执行中:
    那么我们只能等其操作完成。
    所以,当我们调用cancel方法时,它只是将NSOperation对象中的状态isCancelled设置为YES。
    因此,在我们的操作任务中,我们应该在每个操作开始前,或每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为YES。
    如果是YES,则后面操作都可以不用在执行了。
    

    注意:当我们调用cancel方法的时候,他只是将isCancelled设置为YES。

  • 操作被取消的正确理解:

    1、NSOperation任务开始执行之后,会一直执行直到完成为止,或者显式地取消操作。
    取消可能发生在任何时候,甚至在operation执行之前。
    
    2、尽管NSOperation提供了一个方法cancel,让应用可以取消一个操作,
    但是识别出取消事件则是我们自己的事情,因为取消可能发生在任务已经开始执行的过程中。
    
    3、如果NSOperation直接终止,可能无法回收所有已分配的内存或资源。
    因此NSOperation对象需要检测取消事件,并优雅地退出执行(回收所有已分配的内存或资源)。
    
  • 如何正确执行一个操作的取消?

    1、当我们想要取消一个已经在执行的操作时,调用cancel方法,它只是将NSOperation对象的属性isCancelled设置为YES。
    所以,NSOperation对象需要定期地调用isCancelled方法,检测操作是否已经被取消,如果返回YES,则立即退出执行。
    
    2、不管是自定义NSOperation子类,还是使用系统提供的两个具体子类,都需要支持取消。
    isCancelled方法本身非常轻量,可以频繁地调用而不产生大的性能损失。
    
  • 需要在哪些地方调用isCancelled?

    1、在执行任何实际的工作之前。
    2、在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次。
    3、代码中相对比较容易中止操作的任何地方。
    
  • 示例代码

继承NSOperation,重写main方法,执行主任务。

- (void)main   
    // 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池  
    @autoreleasepool   
        if (self.isCancelled) return;  

        // 获取图片数据  
        NSURL *url = [NSURL URLWithString:self.imageUrl];  
        NSData *imageData = [NSData dataWithContentsOfURL:url];  

        if (self.isCancelled)   
            url = nil;  
            imageData = nil;  
            return;  
          

        // 初始化图片  
        UIImage *image = [UIImage imageWithData:imageData];  

        if (self.isCancelled)   
            image = nil;  
            return;  
                    

        if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)])   
            // 把图片数据传回到主线程  
            [(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];  
          
      
 

5、状态

NSOperation提供了ready、cancelled、executing、finished这几个状态变化,我们的开发也是必须处理其中自己关心的状态。

  • 这里有3点需要注意的:

    1、这些状态都是基于keypath的KVO通知决定,所以在你手动改变自己关心的状态时,请别忘了手动发送通知。
    
    2、这里面每个属性都是相互独立的,同时只可能有一个状态是YES。
    
    3、finished这个状态在操作完成后请及时设置为YES,因为NSOperationQueue所管理的队列中,
    只有isFinished为YES时才将其移除队列,这点在内存管理和避免死锁很关键。
    
  • 如何处理这几个状态?

    1、我们将重写finished、executing两个属性,通过重写set方法,并手动发送keyPath的KVO通知。
    
    2、在start函数中,我们首先判断是否已经取消,如果取消的话,我们将直接return,并将_finished设置为YES。
    
    3、如果没有取消操作,我们将_executing设置为YES,表示当前operation正在执行,继续执行我们的逻辑代码。
    
    4、在执行完我们的代码后,别忘了设置operation的状态,将_executing设置为NO,并将finished设置为YES。
    
  • 示例代码

@interface TestOperation ()
@property (nonatomic, assign) BOOL finished;
@property (nonatomic, assign) BOOL executing;
@end

@implementation TestOperation
@synthesize finished = _finished;
@synthesize executing = _executing;

- (void)start

    if ([self isCancelled]) 
        _finished = YES;
        return;
     else 
        _executing = YES;

        //start your task;
        //end your task;

        _executing = NO;
        _finished = YES;
    

- (void)setFinished:(BOOL)finished 
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];

- (void)setExecuting:(BOOL)executing 
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];

6、依赖

NSOperation中我们可以为操作分解为若干个小的任务,通过添加他们之间的依赖关系进行操作。

[operation1 addDependency:operation2];
[operationQueue addOperation:operation1];
[operationQueue addOperation:operation2];

operationQueue是一个并发执行的队列,如果没有添加依赖关系,那么operation1和operation2会并发执行,但是添加了上面的依赖关系后,operation1必须等待operation2执行完之后,才会开始执行。

另外,我们还可以为不同队列queue中的不同操作operation添加依赖关系,即不一定是同一个队列中的两个操作才可以添加依赖关系。

  • 这里有4点需要注意的:

    1、不能添加相互依赖,像A依赖B,B依赖A,这样会导致死锁!
    
    2、在每个操作完成时,请将isFinished设置为YES,不然后续的操作是不会开始执行的。
    
    3、注意:一定要在添加之前,进行设置。
    
    4、并发队列中,任务添加的顺序并不能够决定执行顺序,执行的顺序取决于依赖。
    

7、优先级

NSOperationQueue可以设定队列中的最大并发数,当队列中的操作operation很多而你希望让后面添加进队列的操作优先执行的时候,你可以为你的operation设置高优先级。

- (NSOperationQueuePriority)queuePriority;
- (void)setQueuePriority:(NSOperationQueuePriority)p;

说明:优先级高的任务,调用的几率会更大。

NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8

8、监听操作的执行

如果我们想在一个NSOperation执行完毕后做一些事情,就调用NSOperation的setCompletionBlock方法来设置想做的事情。

[operation setCompletionBlock:^()   
    NSLog(@"执行完毕");  
]; 

9、总结

1、添加操作到NSOperationQueue中,自动执行操作,自动开启线程。

2、在并发操作中,队列的取出是有顺序的,与队列中任务执行结束的顺序无关。    
这就好比,选手A、B、C虽然起跑的顺序是先A、后B、再C,但是到达终点的顺序却不一定是A、B在前,C在后。

3、如果没有设置最大并发数,那么并发的个数是由系统内存和CPU决定的。
可能内存多久开多一点,内存少就开少一点。
最大并发数不要乱写(5以内),不要开太多,一般以2~3为宜。
因为虽然任务是在子线程进行处理的,但是cpu处理这些过多的子线程可能会让UI变卡。

二、NSInvocationOperation

基于一个对象和selector来创建操作,如果你已经有现有的方法来执行需要的任务,就可以使用这个类。

// 这个操作是:调用self的run方法  
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];  
// 开始执行任务(同步执行)  
[operation start];  

三、NSBlockOperation

能够并发地执行一个或多个block对象,所有相关的block都执行完之后,操作才算完成。

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^()  
    NSLog(@"执行了一个新的操作,线程:%@", [NSThread currentThread]);  
];  
[operation addExecutionBlock:^()   
    NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);  
];    
[operation addExecutionBlock:^()   
    NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);  
];  
 // 开始执行任务(这里还是同步执行)  
[operation start];  

这几个Block是并行执行的,在不同线程中执行,其中包括了调用start方法所在的当前线程。


四、自定义NSOperation

如果NSInvocationOperation和NSBlockOperation对象不能满足需求,你可以直接继承NSOperation,并添加任何你想要的行为。

继承所需的工作量主要取决于你要实现非并发还是并发的NSOperation。

1、定义非并发的NSOperation要简单许多,只需要重载main这个方法,在这个方法里面执行主任务,并正确地响应取消事件;

2、对于并发NSOperation,你必须重写NSOperation的多个基本方法进行实现,包括重写start方法,在内部开启新的线程执行操作。

五、NSOperationQueue

一个NSOperation对象可以通过调用start方法来执行任务,默认是同步执行的,也可以将NSOperation添加到一个NSOperationQueue(操作队列)中去执行,而这是异步执行的。

1、创建一个操作队列

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

2、添加NSOperation到NSOperationQueue中

1、添加一个operation

[queue addOperation:operation]; 

2、添加一组operation

[queue addOperations:operations waitUntilFinished:NO];

3、添加一个block形式的operation

[queue addOperationWithBlock:^()   
    NSLog(@"执行一个新的操作,线程:%@", [NSThread currentThread]);  
];  
  • 有几点需要注意的:

    1、NSOperation添加到queue之后,通常短时间内就会得到运行。
    但是如果存在依赖,或者整个queue被暂停等原因,也可能需要等待。
    
    2、NSOperation添加到queue之后,绝对不要再修改NSOperation对象的状态。
    因为NSOperation对象可能会在任何时候运行,因此改变NSOperation对象的依赖或数据会产生不利的影响。
    
    3、你只能查看NSOperation对象的状态,比如是否正在运行、等待运行、已经完成等。
    

3、修改Operations的执行顺序

对于添加到queue中的operations,它们的执行顺序取决于2点:

1、首先看看NSOperation是否已经准备好:是否准备好由对象的依赖关系确定。

2、然后再根据所有NSOperation的相对优先级来确定。优先级等级则是operation对象本身的一个属性。

注意:
默认所有operation都拥有“普通”优先级,不过可以通过setQueuePriority:方法来提升或降低operation对象的优先级。 
优先级只能应用于相同queue中的operations。如果应用有多个operation的queue,每个queue的优先级等级是互相独立的。 
因此不同queue中的低优先级操作仍然可能比高优先级操作更早执行。

注意:
优先级不能替代依赖关系,优先级只是对已经准备好的 operations确定执行顺序。
先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。


4、暂停和继续queue

如果你想临时暂停Operations的执行,可以使用queue的setSuspended:方法暂停queue。

不过暂停一个queue不会导致正在执行的operation在任务中途暂停,只是简单地阻止调度新Operation执行。

你可以在响应用户请求时,暂停一个queue来暂停等待中的任务。稍后根据用户的请求,可以再次调用setSuspended:方法继续queue中operation的执行。

// 暂停queue  
[queue setSuspended:YES];    
// 继续queue  
[queue setSuspended:NO]; 

以上是关于iOS多线程编程--NSOperation的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发多线程编程2 - NSOperation

iOS多线程编程--NSOperation

ios多线程 -- NSOperation 简介

iOS多线程---NSOperation介绍和使用

Ios 多线程之NSOperation与NSOprationQueue

iOS开发多线程篇 09 —NSOperation简单介绍