OC之NSOperation

作者: 苏沫离 | 来源:发表于2018-08-16 14:29 被阅读153次

    NSOperationNSOperationQueue 是苹果提供的一套面向对象的基于GCD封装的多线程解决方案。

    1、NSOperation

    NSOperation是与单个任务关联的代码和数据的抽象类,任务捆绑在NSOperation实例里,可以简单地认为NSOperation是单个的工作单元。
    它在使用上,更加符合面向对象的思想,更加方便的为任务添加依赖关系,同时提供了四个支持KVO监听的代表当前任务执行状态的属性cancelled、executing、finished、readyNSOperation内部对这四个状态行为作了预处理,根据任务的不同状态这四个属性的值会自动改变。

    1.1、NSOperation属性与方法

    1.1.1、执行操作
    方法 方法描述
    -(void)start 开始执行操作。
    -(void)main 执行的非并发任务
    属性 值描述
    completionBlock 一个读写属性的闭包,当属性finished的值更改为YES时,将执行该代码块。该代码块通常是一个子线程执行;因此,不应该使用这个块来做刷新UI。一个完成的操作可能因为它被取消或者因为它成功地完成了它的任务而结束;在编写代码块时,应该考虑到这一点。

    注意: 调用-(void)start方法之前必须确认操作准备就绪(即ready的值为YES),否则操作调用-(void)start会出现异常Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSOperationInternal _start:]: receiver is not yet ready to execute' :如下图所示

    is not yet ready to execute.png
    1.1.2、取消操作
    方法 方法描述
    -(void)cancel 通知操作对象应该停止执行其任务。该方法不会将操作强制停止;它更新NSOperation的内部cancelled以反映状态的变化。如果操作已经完成执行,调用该方法无实际意义;可以调用该方法取消在操作队列中但尚未执行的操作。
    属性 值描述
    cancelled 一个只读属性的布尔值,指示操作是否已被取消,默认为NO。调用-(void)cancel方法将此属性的值设置为YES,一旦取消,操作必须移动到完成状态。
    1.1.3、获取操作状态
    属性 值描述
    ready 一个只读属性的布尔值,指示现在是否可以执行该操作。当为YES时,这个operation是处于执行的就绪状态的,当为NO时,表示它依赖的operation未完成。该属性的默认值为 YES,只有在这个操作执行之前使用- addDependency:方法给这个操作添加一个依赖,ready 值才会改为 NO;当添加的依赖操作 finished 属性为 YES 即依赖操作完成或者取消时,这个操作的 ready 改为YES。注意: readyNO 的操作还没准备就绪,此时不能调用 - (void)start ,否则异常终止NSInvalidArgumentException
    executing 一个只读属性的布尔值,指示操作是否正在执行。在实现并发操作对象时,必须重写此属性的实现,以便返回操作的执行状态;对于非并发操作,不需要重新实现此属性。
    finished 一个只读属性的布尔值,指示操作是否已完成任务或者被取消。一个操作对象直到finished属性值变为YES时,才会清除依赖。同样地,操作队列直到finished属性值为YES时才回将一个NSOperation移除队列。在实现并发操作对象时,必须重写此属性的实现,以便返回操作的完成状态;对于非并发操作,不需要重新实现此属性。
    cancelled 一个只读属性的布尔值,指示操作是否已被取消,默认为NO。调用-(void)cancel方法将此属性的值设置为YES,一旦取消,操作必须移动到完成状态。
    concurrent 一个只读属性的布尔值,指示操作是否异步执行其任务,默认为NO。已被asynchronous属性替代。
    asynchronous 一个只读属性的布尔值,指示操作是否异步执行其任务,默认为NO。在实现异步操作对象时,必须实现此属性并返回YES。
    name 一个读写属性的字符串,为操作对象分配一个名称,以方便在调试期间识别它。
    1.1.4、依赖关系管理
    方法 方法描述
    -(void)addDependency:(NSOperation *)op NSOperation之间可以设置依赖来保证执行顺序,如一定要让操作A执行完后,才能执行操作B;直到所有依赖的操作都完成执行,该操作才被认为准备好执行。如果该操作已经在执行其任务,那么添加依赖关系没有实际效果。这个方法可以改变此操作的isReadydependencies属性。不能创建循环依赖(不能A依赖于B,B又依赖于A),这样做可能导致操作之间的死锁。
    -(void)removeDependency:(NSOperation *)op 移除对指定操作的依赖操作。这个方法可以改变此操作的isReadydependencies属性。
    属性 值描述
    dependencies 一个只读属性的数组,表示在当前对象开始执行之前必须完成执行的操作对象集合。要向这个数组添加对象,需要使用- (void)addDependency:(NSOperation *)op方法。在完成执行操作时,不会从这个依赖项列表中删除操作。可以使用这个列表来跟踪所有依赖的操作,包括那些已经完成执行的操作。从列表中删除操作的唯一方法是使用-(void)removeDependency:(NSOperation *)op方法。
    1.1.5、配置执行优先级
    属性 值描述
    queuePriority 操作队列中操作的执行优先级,这个值用于影响操作退出队列和执行的顺序。如果没有显式设置优先级,该方法返回NSOperationQueuePriorityNormal
    qualityOfService NSOperation对象访问系统资源(如CPU时间、网络资源、磁盘资源等)的优先级,反映有效执行NSOperation所需的最低服务级别,默认值是NSQualityOfServiceBackground。服务质量较高的NSOperation优先于系统资源,因此可以更快地执行任务。想要了解更多可以阅读Energy Efficiency Guide for iOS Apps
    1.1.6、等待操作对象

    -(void)waitUntilFinished方法:阻塞当前线程的执行,直到操作对象完成其任务;常用于将操作提交到队列后,调用此方法,等待该操作完成。
    注意:操作对象永远不能在自身上调用此方法,并且应该避免在提交给与自身相同的操作队列的任何操作上调用它,这样做会导致操作死锁。相反,应用程序的其他部分可以根据需要调用此方法,以防止其他任务在目标操作对象完成之前完成。对于位于不同操作队列中的操作调用此方法通常是安全的,尽管如果每个操作都在另一个操作队列上等待,仍然可以创建死锁。

    NSOperation默认是非并发的(non-concurrent);执行它的任务一次,就不能再次执行;

    1.2、单独执行一个任务NSOperation

    因为NSOperation是抽象的,所以不能直接使用这个类,可以使用系统定义的子类NSInvocationOperation/NSBlockOperation或者我们自定义它的子类。
    虽然操作对象通常通过操作对列执行,但是我们也可以手动启动操作队象:

    1.2.1、使用NSInvocationOperation
    @interface NSInvocationOperation : NSOperation 
    - (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
    - (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;
    @property (readonly, retain) NSInvocation *invocation;
    @property (nullable, readonly, retain) id result;
    @end
    

    我们使用NSInvocationOperation创建一个任务:

    - (void)invocationOperationMethod
    {
        NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationHandleTask:) object:@{}];
        invocationOperation.completionBlock = ^{
            NSLog(@"监听回调:taskA ------- %@",NSThread.currentThread);
        };
        [invocationOperation start];
    }
    
    - (void)invocationOperationHandleTask:(id)object
    {
        NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
        [NSThread sleepForTimeInterval:3];//模拟耗时任务
        NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
    }
    

    运行程序,分析打印结果:

    10:09:51 开始处理 -------- <NSThread: 0x60000006a040>{number = 1, name = main}
    10:09:51 开始执行:taskA ======= <NSThread: 0x60000006a040>{number = 1, name = main}
    10:09:54 结束执行:taskA ------- <NSThread: 0x60000006a040>{number = 1, name = main}
    10:09:54 结束处理 -------- <NSThread: 0x60000006a040>{number = 1, name = main}
    10:09:54 监听回调:taskA ------- <NSThread: 0x60000047c780>{number = 3, name = (null)}
    

    通过打印结果可以看到:

    • NSInvocationOperation在当前线程执行任务,会堵塞当前线程直到任务执行完毕。
    • 如果想要开辟一条线程执行任务,可以在指定的处理方法里开辟一条线程;当然这样做显得多余,不符合NSInvocationOperation的适用场景。
    • NSInvocationOperation任务完成之后的回调completionBlock处理在分线程中。
    1.2.2、使用NSBlockOperation

    我们使用NSBlockOperation快速创建一个任务,然后使用addExecutionBlock额外添加了一个任务:

    - (void)blockOperationMethod
    {
        NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:3];//模拟耗时任务
            NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
        }];
        
        [blockOperation addExecutionBlock:^{
            NSLog(@"开始执行:taskB ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:6];//模拟耗时任务
            NSLog(@"结束执行:taskB ------- %@",NSThread.currentThread);
        }];
        
        blockOperation.completionBlock = ^{
            NSLog(@"监听回调: ------- %@",NSThread.currentThread);
        };
        
        [blockOperation start];
    }
    

    运行程序,分析打印结果:

    10:11:52 开始处理 -------- <NSThread: 0x604000074cc0>{number = 1, name = main}
    10:11:52 开始执行:taskA ======= <NSThread: 0x604000074cc0>{number = 1, name = main}
    10:11:52 开始执行:taskB ======= <NSThread: 0x60400047bbc0>{number = 3, name = (null)}
    10:11:55 结束执行:taskA ------- <NSThread: 0x604000074cc0>{number = 1, name = main}
    10:11:58 结束执行:taskB ------- <NSThread: 0x60400047bbc0>{number = 3, name = (null)}
    10:11:58 结束处理 -------- <NSThread: 0x604000074cc0>{number = 1, name = main}
    10:11:58 监听回调: ------- <NSThread: 0x60400047bbc0>{number = 3, name = (null)}
    

    通过打印结果可以看到:

    • 相对于NSInvocationOperationNSBlockOperation可以使用addExecutionBlock追加多个任务;
    • 对于taskANSBlockOperation在当前线程执行任务;对于taskBNSBlockOperation新开辟一条线程并发执行任务;
    • 也就是说NSBlockOperation封装的操作数 > 1,就会异步并发执行操作;如果 = 1,就会同步执行操作。
    • NSBlockOperation会堵塞当前线程,直到封装的所有任务处理完毕,当前线程代码才会接着向下执行;
    • NSBlockOperation任务完成之后的回调completionBlock处理在分线程中。
    1.2.3、自定义NSOperation同步执行

    我们已经使用NSOperation的系统定义子类NSInvocationOperationNSBlockOperation来单独处理耗时任务,知道这两个子类默认在当前线程处理,会堵塞当前线程直到所有任务全部处理完毕,不会开辟新的线程。我们不妨自定义一个NSOperation的子类,来了解内部的工作机制:

    1.2.3.1、非并发类OperationSync

    我们定义一个同步执行的OperationSync操作类,重写- (void)main方法;可以研究下非并发操作对象的内部实现:

    @interface OperationSync : NSOperation
    @end
    
    @implementation OperationSync
    @synthesize completionBlock = _completionBlock;
    
    - (void)main
    {
        //该自动释放池可以防止相关线程发生内存泄漏
        @autoreleasepool{
            //使用  try-catch 语句防止出现超出这个线程范围的异常情况
            @try{
                //检查操作是否被取消,在取消操作时尽可能快地退出
                if (self.isCancelled == NO) {
                    NSLog(@"耗时任务执行中 ------- %@",NSThread.currentThread);
                    [NSThread sleepForTimeInterval:3];//模拟耗时任务
                    if (_completionBlock){
                        self.completionBlock();
                    }
                }
    
            }@catch (NSException *exception){}
        }
    }
    
    @end
    
    1.2.3.2、使用OperationSync

    我们将耗时操作封装在OperationSyncmian方法中,

    - (void)customSyncOperationTaskAMethod
    {
        OperationSync *taskA = [[OperationSync alloc] init];
        taskA.completionBlock = ^{
            NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
        };
        NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
        [taskA start];
    }
    

    运行程序,分析打印结果:

    10:43:31 开始处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
    10:43:31 开始执行:taskA ======= <NSThread: 0x600000076b80>{number = 1, name = main}
    10:43:31 耗时任务执行中 ------- <NSThread: 0x600000076b80>{number = 1, name = main}
    10:43:34 结束执行:taskA ------- <NSThread: 0x600000076b80>{number = 1, name = main}
    10:43:34 结束处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
    

    运行程序,可以看到:

    • OperationSync同步执行任务,阻塞当前线程;
    • 由于completionBlock回调执行在当前线程,而上文NSInvocationOperationNSBlockOperationcompletionBlock回调都在分线程处理;我们可以推断:苹果新开辟了一条线程用来处理NSInvocationOperationNSBlockOperationcompletionBlock回调;
    1.2.4、自定义NSOperation异步执行

    在上文,我们自定义一个NSOperation的子类OperationSync来同步处理耗时任务,了解了内部的工作机制。现在我们不妨来实现一个并发执行任务的NSOperation的子类:

    1.2.4.1、并发类OperationAsync

    将操作编写为并发,异步执行它,必须添加许多额外功能:

    • 重写 start 方法:将该方法更新为以异步方式执行操作,通常通过在新线程调用操作对象的main方法来做到这一点;
    • 重写 main 方法(可选):该方法实现与操作关联的任务,也可以直接在 start 方法实现该任务;
    • 配置和管理操作的执行环境:并发操作必须设置本身的环境,并向客户端报告其状态。尤其是 isExecuting、 isFinished 和 isAsynchronous 方法必须返回与操作状态有关的值,而且这 3 个方法必须具备线程安全性,当这些值改变时,还必须生成适当的键值观察通知(KVO)。
    @interface OperationAsync : NSOperation
    @end
    
    @implementation OperationAsync
    @synthesize finished = _finished;
    @synthesize executing = _executing;
    @synthesize completionBlock = _completionBlock;
    
    - (instancetype)init
    {
        self = [super init];
        
        if (self)
        {
            _finished = NO;
            _executing = NO;
        }
        
        return self;
    }
    
    - (void)start
    {
        //检查操作是否被取消,在取消操作时尽可能快地退出
        if (self.isCancelled){
            //重写start方法,当operation执行完成或者被取消的时候,必须重写这个finished属性以及生成KVO通知
            [self willChangeValueForKey:@"isFinished"];
            _finished = YES;
            [self didChangeValueForKey:@"isFinished"];
            
            //重写start方法,当operation执行完成或者被取消的时候,根据需要调用completionBlock
            if (_completionBlock){
                self.completionBlock();
            }
            return;
        }
        
        //重写start方法,当operation改变了执行状态时,必须重写这个executing属性以及生成KVO通知。
        [self willChangeValueForKey:@"isExecuting"];
        [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
        _executing = YES;
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    - (void)main
    {
        //该自动释放池可以防止相关线程发生内存泄漏
        @autoreleasepool{
            //使用  try-catch 语句防止出现超出这个线程范围的异常情况
            @try{
                //检查操作是否被取消,在取消操作时尽可能快地退出
                if (self.isCancelled == NO) {
                    NSLog(@"耗时任务执行中 ------- %@",NSThread.currentThread);
                    [NSThread sleepForTimeInterval:3];//模拟耗时任务
                    [self willChangeValueForKey:@"isFinished"];
                    [self willChangeValueForKey:@"isExecuting"];
                    _finished = YES;
                    _executing = YES;
                    [self didChangeValueForKey:@"isExecuting"];
                    [self didChangeValueForKey:@"isFinished"];
                    
                    //重写main方法,当operation执行完成或者被取消的时候,根据需要调用completionBlock
                    if (_completionBlock){
                        self.completionBlock();
                    }
                }
                
            }@catch (NSException *exception){}
        }
    }
    
    - (BOOL)isAsynchronous{
        return YES;
    }
    
    - (BOOL)isExecuting{
        return _executing;
    }
    
    - (BOOL)isFinished{
        return _finished;
    }
    
    @end
    
    1.2.4.2、使用OperationAsync

    我们使用并发操作OperationAsync

    - (void)customAsyncOperationTaskAMethod
    {
        OperationAsync *taskA = [[OperationAsync alloc] init];
        taskA.completionBlock = ^{
            NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
        };
        NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
        [taskA start];
    }
    

    运行程序,分析打印结果:

    10:44:12 开始处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
    10:44:12 开始执行:taskA ======= <NSThread: 0x600000076b80>{number = 1, name = main}
    10:44:12 结束处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
    10:44:12 耗时任务执行中 ------- <NSThread: 0x60000027c700>{number = 3, name = (null)}
    10:44:15 结束执行:taskA ------- <NSThread: 0x60000027c700>{number = 3, name = (null)}
    

    运行程序,可以看到:

    • OperationAsync异步执行任务,开辟一条新线程,不会阻塞当前线程;
    • 由于completionBlock在新开辟的分线程被调用,所以回调处理也在分线程被执行;

    1.3、NSOperation生命周期

    我们知道NSOperation 有多个操作状态,那么这些操作状态之间有什么联系呢?NSOperation的生命周期是什么?
    我们通过下面程序来探究下:

    - (void)operationLifeCycleMethod
    {
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        NSBlockOperation *blockOperationA = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
        }];
        blockOperationA.name = @"com.demo.taskA";
        [blockOperationA addObserver:self forKeyPath:@"ready" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
        [blockOperationA addObserver:self forKeyPath:@"cancelled" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
        [blockOperationA addObserver:self forKeyPath:@"executing" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
        [blockOperationA addObserver:self forKeyPath:@"finished" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
        
        NSBlockOperation *blockOperationB = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskB ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"结束执行:taskB ------- %@",NSThread.currentThread);
        }];
        blockOperationB.name = @"com.demo.taskB";
        [blockOperationB addObserver:self forKeyPath:@"ready" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
        [blockOperationB addObserver:self forKeyPath:@"cancelled" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
        [blockOperationB addObserver:self forKeyPath:@"executing" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
        [blockOperationB addObserver:self forKeyPath:@"finished" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
        
        NSLog(@"blockOperationA.ready --- %d",blockOperationA.ready);
        NSLog(@"blockOperationB.ready --- %d",blockOperationB.ready);
        [blockOperationA addDependency:blockOperationB];
        NSLog(@"blockOperationA.ready === %d",blockOperationA.ready);
        NSLog(@"blockOperationB.ready === %d",blockOperationB.ready);
        
        [queue addOperation:blockOperationA];
        [queue addOperation:blockOperationB];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        id task = (__bridge id)(context);
        NSLog(@"%@ ----- %@ === %@",task,keyPath,change[NSKeyValueChangeNewKey]);
    }
    

    运行程序,分析打印数据:

    11:42:09 开始处理 -------- <NSThread: 0x604000076300>{number = 1, name = main}
    11:42:09 blockOperationA.ready --- 1
    11:42:09 blockOperationB.ready --- 1
    11:42:09 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- ready === 0
    11:42:09 blockOperationA.ready === 0
    11:42:09 blockOperationB.ready === 1
    11:42:09 结束处理 -------- <NSThread: 0x604000076300>{number = 1, name = main}
    11:42:09 <NSBlockOperation: 0x600000441800>{name = 'com.demo.taskB'} ----- executing === 1
    11:42:09 开始执行:taskB ======= <NSThread: 0x60000047cac0>{number = 3, name = (null)}
    11:42:11 结束执行:taskB ------- <NSThread: 0x60000047cac0>{number = 3, name = (null)}
    11:42:11 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- executing === 1
    11:42:11 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- ready === 1
    11:42:11 开始执行:taskA ======= <NSThread: 0x60400046d880>{number = 4, name = (null)}
    11:42:11 <NSBlockOperation: 0x600000441800>{name = 'com.demo.taskB'} ----- executing === 0
    11:42:11 <NSBlockOperation: 0x600000441800>{name = 'com.demo.taskB'} ----- finished === 1
    11:42:13 结束执行:taskA ------- <NSThread: 0x60400046d880>{number = 4, name = (null)}
    11:42:13 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- executing === 0
    11:42:13 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- finished === 1
    

    分析打印数据:

    • 通过blockOperation.ready可以看到,NSOperation的属性ready值默认为YES,处于准备就绪状态;当给该NSOperation添加一个依赖后,属性ready变为 NO,处于未准备状态。
    • taskB结束执行后该操作属性finished变为 YES;这时taskA操作的属性ready值改为YES,表示taskA的操作已经准备就绪,可以开始执行;
    • 操作taskA的属性executing变为 YES,表示正在执行;taskA执行完毕之后,executing变为 NO,finished变为 1。

    我们通过KVO监听操作taskA的各属性变化,可以得出NSOperation的生命周期:

    NSOperation的生命周期.png

    1.4、NSOperation只能start一次

    我们已经知道,NSOperation的从准备任务到完成任务生命周期历程;那么start调用一次后,再次调用还能执行任务嘛?答案是否定的!我们不妨来看一段程序:

    - (void)startMuchMethod
    {
        NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:2];//模拟耗时任务
            NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
        }];
        
        [blockOperation start];
        NSLog(@"再次执行:taskA ======= %@",NSThread.currentThread);
        [blockOperation start];
    }
    

    运行程序,分析打印结果:

    09:38:10 开始处理 -------- <NSThread: 0x604000062080>{number = 1, name = main}
    09:38:10 开始执行:taskA ======= <NSThread: 0x604000062080>{number = 1, name = main}
    09:38:12 结束执行:taskA ------- <NSThread: 0x604000062080>{number = 1, name = main}
    09:38:12 再次执行:taskA ======= <NSThread: 0x604000062080>{number = 1, name = main}
    09:38:12 结束处理 -------- <NSThread: 0x604000062080>{number = 1, name = main}
    

    从打印结果可以看到:NSOperation的任务只执行了一次,就好比生命只有一次:从出生到死亡的历程。这点不同于dispatch_block_t,我们dispatch_block_create()创建了一个dispatch_block_t,可以多次调用dispatch_block_perform方法来执行这个任务。

    2、操作队列NSOperationQueue

    NSOperationQueue 是一个管理操作执行的队列。

    NSOperation配合NSOperationQueue使用时,Queue会监听所有Operation的状态从而分配任务的启动时机。NSOperation隐藏了很多内部细节,让开发者无需关心任务的各种状态。

    使用 NSOperationQueue 时控制任务数量会并不总是有效,原因何在?利用 NSOperation 封装异步代码有什么需要注意的地方?是否有更好的方法来控制任务的并发数量?为此,我们需要深入了解 NSOperation 的运作机制,现在我们从实际应用场景出发探讨这些问题。

    2.1、NSOperationQueue的属性与方法

    2.1.1、访问特定操作队列
    属性 值描述
    mainQueue 一个只读属性的NSOperationQueue,返回绑定到主线程的默认操作队列;返回的队列每次在应用程序的主线程上执行一个操作。(class修饰,由类对象调用)
    currentQueue 一个只读属性的NSOperationQueue,在运行的操作对象中使用此方法来获取当前操作的操作队列。从运行操作的上下文外部调用此方法通常会返回nil。(class修饰,由类对象调用)
    2.1.2、管理队列中的操作

    我们不妨先运行几个程序,体验下操作队列的方法使用

    2.1.2.1、程序一

    我们使用NSBlockOperation创建了一个耗时操作;创建了两个操作队列使用NSOperationQueue的实例方法- addOperation:将操作添加到这两个操作操作队列:

    - (void)addOperationMethod
    {
        NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
        NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
        
        NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行 ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:5];//模拟耗时任务
            NSLog(@"结束执行 ------- %@",NSThread.currentThread);
        }];
    
        [queue1 addOperation:blockOperation];
        [queue2 addOperation:blockOperation];
    }
    

    运行这段代码,发现程序异常终止:-[NSOperationQueue addOperation:]: operation is already enqueued on a queue'操作已经加入到队列中:

     *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSOperationQueue addOperation:]: operation is already enqueued on a queue'
    *** First throw call stack:
    (
        0   CoreFoundation                      0x00000001052e21e6 __exceptionPreprocess + 294
        1   libobjc.A.dylib                     0x0000000104977031 objc_exception_throw + 48
        2   CoreFoundation                      0x0000000105357975 +[NSException raise:format:] + 197
        3   Foundation                          0x0000000104386f4c __addOperations + 1186
        4   OperationQueueDemo                  0x00000001040632c4 -[OperationQueueTableViewController addOperationMethod] + 212
        5   OperationQueueDemo                  0x00000001040618c7 -[OperationQueueTableViewController tableView:didSelectRowAtIndexPath:] + 583
        6   UIKit                               0x0000000106253e89 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1813
        7   UIKit                               0x00000001062540a4 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 344
        8   UIKit                               0x00000001061204b3 _runAfterCACommitDeferredBlocks + 318
        9   UIKit                               0x000000010610f71e _cleanUpAfterCAFlushAndRunDeferredBlocks + 388
        10  UIKit                               0x000000010613dea5 _afterCACommitHandler + 137
        11  CoreFoundation                      0x0000000105284607 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
        12  CoreFoundation                      0x000000010528455e __CFRunLoopDoObservers + 430
        13  CoreFoundation                      0x0000000105268b81 __CFRunLoopRun + 1537
        14  CoreFoundation                      0x000000010526830b CFRunLoopRunSpecific + 635
        15  GraphicsServices                    0x000000010b54ea73 GSEventRunModal + 62
        16  UIKit                               0x0000000106115057 UIApplicationMain + 159
        17  OperationQueueDemo                  0x000000010405dfaf main + 111
        18  libdyld.dylib                       0x00000001096f5955 start + 1
    )
    libc++abi.dylib: terminating with uncaught exception of type NSException
    (lldb) 
    

    - (void)addOperation:(NSOperation *)op 将指定的操作添加到操作队列。如果操作已经在另一个队列中,该方法将抛出一个NSInvalidArgumentException异常

    还记得上文提过的NSOperationready状态为NO时调用start方法导致程序抛出NSInvalidArgumentException异常嘛?类似地,如果NSOperation正在执行(executing为YES)或已经执行完毕(finished为YES),-addOperation:方法也会抛出NSInvalidArgumentException异常。

    异常终止:operation is executing and cannot be enqueued
    正在执行操作,无法加入队列.png
    异常终止:operation is finished and cannot be enqueued
    操作已经完成,无法加入队列.png
    2.1.2.2、程序二

    我们使用NSBlockOperation创建了操作A,操作B,操作C,操作D;使用NSOperationQueue的实例方法- addOperation:将操作A、操作C、操作D添加到操作队列,使用- addOperations: waitUntilFinished:方法将操作B添加至操作队列;在添加操作C与操作D之间使用- waitUntilAllOperationsAreFinished堵塞当前线程。

    - (void)waitUntilFinishedMethod
    {
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
        NSBlockOperation *blockOperationA = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:5];//模拟耗时任务
            NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
        }];
        blockOperationA.name = @"com.demo.taskA";
        
        NSBlockOperation *blockOperationB = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskB ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:3];//模拟耗时任务
            NSLog(@"结束执行:taskB ------- %@",NSThread.currentThread);
        }];
        blockOperationB.name = @"com.demo.taskB";
    
        NSBlockOperation *blockOperationC = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskC ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:3];//模拟耗时任务
            NSLog(@"结束执行:taskC ------- %@",NSThread.currentThread);
        }];
        blockOperationC.name = @"com.demo.taskC";
        
        NSBlockOperation *blockOperationD = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"开始执行:taskD ======= %@",NSThread.currentThread);
            [NSThread sleepForTimeInterval:3];//模拟耗时任务
            NSLog(@"结束执行:taskD ------- %@",NSThread.currentThread);
        }];
        blockOperationD.name = @"com.demo.taskD";
        
        [queue addOperation:blockOperationA];
        [queue addOperations:@[blockOperationB] waitUntilFinished:YES];
        NSLog(@"queue.operations === 1 === %@",queue.operations);
        [queue addOperation:blockOperationC];
        NSLog(@"queue.operations === 2 === %@",queue.operations);
        [queue waitUntilAllOperationsAreFinished];
        [queue addOperation:blockOperationD];
        NSLog(@"queue.operations === 3 === %@",queue.operations);
    }
    

    我们运行程序,分析打印数据:

    10:21:58 开始处理 -------- <NSThread: 0x60400006f6c0>{number = 1, name = main}
    10:21:58 开始执行:taskA ======= <NSThread: 0x60000046bac0>{number = 3, name = (null)}
    10:21:58 开始执行:taskB ======= <NSThread: 0x60400046bd80>{number = 4, name = (null)}
    10:22:01 结束执行:taskB ------- <NSThread: 0x60400046bd80>{number = 4, name = (null)}
    10:22:01 queue.operations === 1 === (
     "<NSBlockOperation: 0x60000044f900>{name = 'com.demo.taskA'}"
     )
    10:22:01 queue.operations === 2 === (
     "<NSBlockOperation: 0x60000044f900>{name = 'com.demo.taskA'}",
     "<NSBlockOperation: 0x6000002586c0>{name = 'com.demo.taskC'}"
     )
    10:22:01 开始执行:taskC ======= <NSThread: 0x60400046bd80>{number = 4, name = (null)}
    10:22:03 结束执行:taskA ------- <NSThread: 0x60000046bac0>{number = 3, name = (null)}
    10:22:04 结束执行:taskC ------- <NSThread: 0x60400046bd80>{number = 4, name = (null)}
    10:22:04 queue.operations === 3 === (
     "<NSBlockOperation: 0x6000004436c0>{name = 'com.demo.taskD'}"
     )
    10:22:04 开始执行:taskD ======= <NSThread: 0x60000046bac0>{number = 3, name = (null)}
    10:22:04 结束处理 -------- <NSThread: 0x60400006f6c0>{number = 1, name = main}
    10:22:07 结束执行:taskD ------- <NSThread: 0x60000046bac0>{number = 3, name = (null)}
    

    通过打印数据:

    • taskB执行完毕之后,才开始执行taskC:这是因为- addOperations: waitUntilFinished:方法的第二个参数wait如果YES,则阻塞当前线程,直到所有指定的操作执行完毕;如果为NO,则不会堵塞当前线程,立即返回。
    • taskC执行完毕之后,才调用结束处理这句代码:这是因为- waitUntilAllOperationsAreFinished方法会阻塞当前线程,直到队列中所有操作执行完成为止;在此期间,当前线程不能向队列添加操作,但其他线程可以。
    • 通过queue.operations === 1 ===可以看到此时的操作队列只有操作com.demo.taskA:这是因为- addOperations: waitUntilFinished:方法的第二个参数wait如果YES,则阻塞当前线程,直到指定的操作执行完毕,这时会将指定的操作移除操作队列;
    • 通过queue.operations === 2 ===queue.operations === 3 === 的结果对比,可以看到- waitUntilAllOperationsAreFinished之前的操作都被移除操作队列。

    思考一下: - waitUntilAllOperationsAreFinished方法已将操作从队列中移除,这时调用-addOperation:方法将该操作添加到另一个队列是否会抛出异常?为什么?

    方法 方法描述
    - (void)addOperation:(NSOperation *)op 将指定的操作添加到操作队列。添加后,指定的操作将保留在操作队列中,直到执行完毕。一个操作对象一次最多可以在一个操作队列中,如果操作已经在另一个操作队列中,该方法将抛出一个NSInvalidArgumentException异常。类似地,如果操作正在执行或已经执行完毕,该方法将抛出NSInvalidArgumentException异常。
    - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait 将指定的操作添加到操作队列中;添加后,指定的操作将保留在队列中,直到完成的方法返回YES为止;指定的操作完成后,系统会将这些操作集从operations数组中移除。参数ops要添加到操作队列中的操作集;参数wait如果YES,则阻塞当前线程,直到所有指定的操作执行完毕;如果为NO,则不会堵塞当前线程,立即返回。
    - (void)addOperationWithBlock:(void (^)(void))block 在操作中包装指定的块并将其添加到操作队列。参数block是从操作中执行的块,该块不接受任何参数,也没有返回值。
    - (void)waitUntilAllOperationsAreFinished 阻塞当前线程,直到队列中所有操作执行完成为止。在此期间,当前线程不能向队列添加操作,但其他线程可以;完成所有挂起操作后,该方法返回;如果队列中没有操作,此方法立即返回;完成返回后,系统会将operations的所有操作移除。
    - (void)cancelAllOperations 取消所有排队和执行的操作。此方法会对当前操作队列中的所有操作调用-(void)cancel方法。取消操作不会自动将它们从队列中删除或停止当前正在执行的操作。对于排队和等待执行的操作,队列必须仍然尝试执行操作,然后才会识别它被取消并将其移动到完成状态。对于已经执行的操作,操作对象本身必须检查是否取消,并停止正在执行的操作,以便能够移动到完成状态。在这两种情况下,已完成(或已取消)的操作在从队列中删除之前仍然有机会执行其完成块。
    属性 属性描述
    operations 一个只读属性的数组,是当前操作队列中所有操作NSOperation集合。该数组包含多个NSOperation对象,其顺序与它们被添加到队列的顺序相同;这个顺序不一定反映执行这些操作的顺序。数组可能包含正在执行或等待执行的操作。列表还可能包含在数组最初被检索时正在执行的操作,但随后已经完成。
    operationCount 一个只读属性的NSUInteger类型数据,表示操作队列中当前的并发数;
    2.1.3、管理与配置操作队列
    属性 属性描述
    qualityOfService 应用于添加到操作队列的操作对象的服务级别NSQualityOfService。服务级别影响操作对象访问系统资源(如CPU时间、网络资源、磁盘资源等)的优先级;服务质量较高的操作优先于系统资源,因此可以更快地执行任务。对于自己创建的队列,默认值是NSOperationQualityOfServiceBackground。对于主线程方法返回的队列,默认值是NSOperationQualityOfServiceUserInteractive,无法更改。
    maxConcurrentOperationCount 操作队列可以同时执行的最大数量;设置并发操作的数量不会影响当前正在执行的任何操作。可以使用苹果推荐值NSOperationQueueDefaultMaxConcurrentOperationCount,该值是根据当前系统条件动态确定的最大操作数。
    name 操作队列的名称,使用此名称方便调试或分析代码。此属性的默认值是一个包含操作队列内存地址的字符串。
    underlyingQueue 用于执行操作的分派队列dispatch_queue_t,默认值为nil。可以将此属性的值设置为现有的调度队列,以便将排队操作与提交到该调度队列的块穿插在一起;只有在队列中没有操作时才应该设置此属性的值,当operationCount不等于0时设置此属性的值会引发NSInvalidArgumentException。该分派队列不能是主队列dispatch_get_main_queue。该分派队列的服务质量级别集覆盖操作队列的服务质量;如果OS_OBJECT_IS_OBJC是YES,此属性将自动保留其分配的队列。
    2.1.4、操作队列暂停执行

    属性suspended:一个可读可写的布尔值,指示队列是否在主动调度要执行的操作。该属性的默认值为NO。当此属性的值为NO时,操作队列会启动队列中准备执行的操作;将此属性设置为YES可挂起操作队列中的任何排队操作,但已经执行的操作将继续执行。挂起的操作队列可以继续添加操作,但在将此属性更改为NO之前,这些操作不会计划执行。

    属性suspended在操作队列的作用类似于GCD中的dispatch_suspend()函数,并不会立即暂停分派队列dispatch_queue中正在执行的任务,而是在当前任务执行完成后,暂停后续的任务执行。

    3、操作队列NSOperationQueue与分派队列dispatch_queue_t

    我们在开篇时提出:NSOperationNSOperationQueue 是苹果提供的一套面向对象的基于GCD封装的多线程解决方案。那么苹果为什么要封装GCD?使用NSOperationQueue的优势是什么?

    3.1、NSOperationdispatch_block_t

    通过上文的学习,我们已经感受到NSOperation很像GCD中的dispatch_block_t。那么相比于dispatch_block_t,使用Operation的优势如下:

    • 可以给代码块添加completionBlock, 在任务完成以后自己调用. 相对于GCD代码更简洁;类似于GCD的dispatch_block_wait()或者dispatch_block_notify()
    • NSOperation之间可以添加依赖关系,- addDependency:;
    • 设置NSOperation的优先级;类似dispatch_block_t的qos
    • 方便的设置operation取消操作;类似dispatch_block_cancel()
    • 使用KVO观察对NSOperation状态的监听: isExcuting, isFinished, isCancelled

    3.2、NSOperationQueuedispatch_queue_t

    相比于分派队列dispatch_queue_t,使用操作队列NSOperationQueue的优势如下:

    • 对操作队列当前并发数的监控operationCount
    • 对操作队列最大并发数的设置maxConcurrentOperationCount
    • 可以调用- cancelAllOperations方法取消操作队列中尚未执行的操作;GCD中并没有提供取消分派队列dispatch_queue_t中任务的函数;
    • 有时候我们很希望知道当前执行的操作队列是谁,我们可以使用currentQueue获取到当前的操作队列。但是GCD中分派队列dispatch_queue_t是按照层级结构来组织的,无法单用某个队列对象来描述“当前队列”,dispatch_get_current_queue()函数可能返回与预期不一致的结果,而且误用dispatch_get_current_queue()可能导致死锁,所以GCD中dispatch_get_current_queue()在 iOS 6已被废弃。

    示例Demo
    参考文章:
    NSOperation概述
    NSOperation相关学习笔记

    相关文章

      网友评论

        本文标题:OC之NSOperation

        本文链接:https://www.haomeiwen.com/subject/kcszsftx.html