iOS 自定义NSOperation

537 阅读11分钟

任务执行状态的控制是相对于自定义的NSOperation子类来说的。对于自定义NSOperation子类有两种类型:

  1. 重写main方法
    只重写operation的main方法,main方法里面写要执行的任务,系统底层控制变更任务执行完成状态,以及任务的退出。

    看个例子:

#import "TestOperation.h"

@interface TestOperation ()
@property (nonatomic, copy) id obj;

@end

@implementation TestOperation

- (instancetype)initWithObject:(id)obj{
    if(self = [super init]){
        self.obj = obj;
    }
    return  self;
}

- (void)main{
    NSLog(@"开始执行任务%@ thread===%@",self.obj,[NSThread currentThread]);
}

调用:

  TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务4"]];
    [operation4 setCompletionBlock:^{
        NSLog(@"执行完成 thread===%@",[NSThread currentThread]);
    }];
    [operation4 start];
// 打印
开始执行任务我是任务4 thread===<NSThread: 0x6000008d8880>{number = 1, name = main}
执行完成 thread===<NSThread: 0x60000089fa40>{number = 7, name = (null)}

可以看到任务operation的main方法执行是在主线程中的,只是最后完成后的回调setCompletionBlock是异步的,好像没什么用,别着急,我们把他放入队列中执行看下,还是上面的例子,加入队列执行

 NSOperationQueue *queue4 = [[NSOperationQueue alloc] init];
 TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务4"]];
 TestOperation *operation5 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务5"]];
 TestOperation *operation6 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务6"]];

 [queue4 addOperation:operation4];
 [queue4 addOperation:operation5];
 [queue4 addOperation:operation6];
//打印:
开始执行任务我是任务6 thread===<NSThread: 0x600001fc8200>{number = 5, name = (null)}
开始执行任务我是任务4 thread===<NSThread: 0x600001fcc040>{number = 6, name = (null)}
开始执行任务我是任务5 thread===<NSThread: 0x600001fd7c80>{number = 7, name = (null)}

这时候可以看到任务的并发执行了,operation的main方法执行结束后就会调用各自的dealloc方法进行释放,任务的生命周期结束。如果我们想让任务4、5、6 倒序执行,可以添加任务依赖

 [operation4 addDependency:operation5];
 [operation5 addDependency:operation6];
// 打印
开始执行任务我是任务6 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
开始执行任务我是任务5 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
开始执行任务我是任务4 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}

这样做貌似是可以的,但是如果我们的operation 中又存在异步任务(如网络请求),我们想让网络任务6请求完后调用任务5,任务5调用成功后调任务4,那该怎么办呢,我们先卖个关子,我们在第二节多个请求完成后继续进行下一个请求的方法总结中介绍。

  1. 重写start方法
    通过重写main方法可以实现任务的串行执行,如果要让任务并发执行,就需要重写start方法。

两者还是有很大区别的:
如果只是重写main方法,方法执行完毕,那么整个operation就会从队列中被移除。如果你是一个自定义的operation并且它是某些类的代理,这些类恰好有异步方法,这时就会找不到代理导致程序出错了。然而start方法就算执行完毕,它的finish属性也不会变,因此你可以控制这个operation的生命周期了。然后在任务完成之后手动cancel掉这个operation即可。

@interface TestStartOperation : NSOperation
- (instancetype)initWithObject:(id)obj;
@property (nonatomic, copy) id obj;
@property (nonatomic, assign, getter=isExecuting) BOOL executing;
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@end
@implementation TestStartOperation
@synthesize executing = _executing;
@synthesize finished = _finished;

- (instancetype)initWithObject:(id)obj{
    if(self = [super init]){
        self.obj = obj;
    }
    return  self;
}
- (void)start{
    
    //在任务开始前设置executing为YES,在此之前可能会进行一些初始化操作
    self.executing = YES;
    NSLog(@"开始执行任务%@ thread===%@",self.obj,[NSThread currentThread]);
    /*
    需要在适当的位置判断外部是否调用了cancel方法
    如果被cancel了需要正确的结束任务
    */
    if (self.isCancelled)
    {
        //任务被取消正确结束前手动设置状态
        self.executing = NO;
        self.finished = YES;
        return;
    }
    
    NSString *str = @"https://www.360.cn";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    __weak typeof(self) weakSelf = self;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
       // NSLog(@"response==%@",response);
        NSLog(@"TASK完成:====%@ thread====%@",weakSelf.obj,[NSThread currentThread]);
        //任务执行完成后手动设置状态
        weakSelf.executing = NO;
        weakSelf.finished = YES;
    }];
    [task resume];
}
- (void)setExecuting:(BOOL)executing
{
    //手动调用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //调用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isExecuting
{
    return _executing;
}
- (void)setFinished:(BOOL)finished
{
    //手动调用KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    //调用KVO通知
    [self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isFinished
{
    return _finished;
}
- (BOOL)isAsynchronous
{
    return YES;
}
- (void)dealloc{
    NSLog(@"Dealloc %@",self.obj);
}

执行与结果:

NSOperationQueue *queue4 = [[NSOperationQueue alloc] init];
TestStartOperation *operation4 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务4"]];
TestStartOperation *operation5 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务5"]];
TestStartOperation *operation6 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任务6"]];
//设置任务依赖 
[operation4 addDependency:operation5];
[operation5 addDependency:operation6];
[queue4 addOperation:operation4];
[queue4 addOperation:operation5];
[queue4 addOperation:operation6];
/*打印
开始执行任务我是任务6 thread===<NSThread: 0x600002bb8480>{number = 6, name = (null)}
TASK完成:====我是任务6 thread====<NSThread: 0x600002bd4d80>{number = 8, name = (null)}
开始执行任务我是任务5 thread===<NSThread: 0x600002bb0300>{number = 5, name = (null)}
TASK完成:====我是任务5 thread====<NSThread: 0x600002bb0300>{number = 5, name = (null)}
开始执行任务我是任务4 thread===<NSThread: 0x600002bfb080>{number = 7, name = (null)}
TASK完成:====我是任务4 thread====<NSThread: 0x600002bfb080>{number = 7, name = (null)}
2021-06-22 17:57:56.436591+0800 Interview01-打印[15994:9172130] Dealloc 我是任务4
2021-06-22 17:57:56.436690+0800 Interview01-打印[15994:9172130] Dealloc 我是任务5
2021-06-22 17:57:56.436784+0800 Interview01-打印[15994:9172130] Dealloc 我是任务6
*/

在这个例子中我们在任务请求完成后,手动设置其self.executingself.finished状态,并且手动触发KVO,队列会监听任务的执行状态。由于我们设置了任务依赖,当任务6请求完成后才会执行任务5,任务5请求完成后 才会执行任务4。最后对各自任务进行移除队列并释放。其实这样也变相解决了上面重写main方法中无法解决的问题。

实际应用

多个请求完成后继续进行下一个请求的方法总结

在我们的工作中经常会遇到这样的请求:一个请求依赖另一个请求的结果,或者多个请求一起发出然后再获取所有的结果后继续后续操作。根据这几种情况总结常用的方法:

1. 使用GCDdispatch_group_t实现

需求:请求顺序执行,执行完成后回调结果

 NSString *str = @"https://www.360.cn";
 NSURL *url = [NSURL URLWithString:str];
 NSURLRequest *request = [NSURLRequest requestWithURL:url];
 NSURLSession *session = [NSURLSession sharedSession];
   
 dispatch_group_t downloadGroup = dispatch_group_create();
 for (int i=0; i<10; i++) {
       dispatch_group_enter(downloadGroup);
       NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
           
          NSLog(@"执行完请求=%d",i);
          dispatch_group_leave(downloadGroup);
       }];
       
       [task resume];
   }
   dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
       NSLog(@"end");
   });
/*
2021-06-22 18:37:56.786878+0800 Interview01-打印[17121:9352056] 请求结束:0
2021-06-22 18:37:56.787770+0800 Interview01-打印[17121:9352057] 请求结束:1
2021-06-22 18:37:56.788492+0800 Interview01-打印[17121:9352057] 请求结束:2
2021-06-22 18:37:56.789148+0800 Interview01-打印[17121:9352057] 请求结束:3
2021-06-22 18:37:56.789837+0800 Interview01-打印[17121:9352057] 请求结束:4
2021-06-22 18:37:56.790433+0800 Interview01-打印[17121:9352059] 请求结束:5
2021-06-22 18:37:56.791117+0800 Interview01-打印[17121:9352059] 请求结束:6
2021-06-22 18:37:56.791860+0800 Interview01-打印[17121:9352059] 请求结束:7
2021-06-22 18:37:56.792614+0800 Interview01-打印[17121:9352059] 请求结束:8
2021-06-22 18:37:56.793201+0800 Interview01-打印[17121:9352059] 请求结束:9
2021-06-22 18:37:56.804529+0800 Interview01-打印[17121:9351753] end*/

主要方法:

  • dispatch_group_t downloadGroup = dispatch_group_create();创建队列组
  • dispatch_group_enter(downloadGroup); 每次执行请求前调用
  • dispatch_group_leave(downloadGroup); 请求完成后调用离开方法
  • dispatch_group_notify() 所有请求完成后回调block

对于enterleave必须配合使用,有几次enter就要有几次leave

2. GCD信号量dispatch_semaphore_t

(1) 需求:顺序执行多个请求,都执行完成后回调给end

NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
       
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
   
for (int i=0; i<10; i++) {
      
     NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
           
           NSLog(@"请求结束:%d",i);
           dispatch_semaphore_signal(sem);
     }];
     [task resume];
     dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
   dispatch_async(dispatch_get_main_queue(), ^{
       NSLog(@"end");
   });

主要方法

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_semaphore_signal(sem);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

dispatch_semaphore信号量为基于计数器的一种多线程同步机制,dispatch_semaphore_signal(sem);表示为计数+1操作,dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 信号量-1,遇到dispatch_semaphore_wait如果信号量的值小于0,就一直阻塞线程,不执行后面的所有程序,直到信号量大于等于0;当第一个for循环执行后dispatch_semaphore_wait堵塞线程,直到执行到dispatch_semaphore_signal后继续下一个for循环进行请求,以此类推完成顺序请求。

(2) 需求:多个请求同时进行,都执行完成后回调给end

NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
       
 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
 __block int count = 0;
 for (int i=0; i<10; i++) {
           
      NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
               
        NSLog(@"%d---%d",i,i);
        count++;
        if (count==10) {
            dispatch_semaphore_signal(sem);
            count = 0;
         }
       }];
           
       [task resume];
   }
   dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
       
   dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"end");
   });
/*
2021-06-23 09:47:49.723576+0800 Interview01-打印[21740:9823752] 请求完成:0
2021-06-23 09:47:49.741118+0800 Interview01-打印[21740:9823751] 请求完成:1
2021-06-23 09:47:49.756781+0800 Interview01-打印[21740:9823752] 请求完成:3
2021-06-23 09:47:49.765250+0800 Interview01-打印[21740:9823752] 请求完成:2
2021-06-23 09:47:49.773008+0800 Interview01-打印[21740:9823756] 请求完成:4
2021-06-23 09:47:49.797809+0800 Interview01-打印[21740:9823751] 请求完成:5
2021-06-23 09:47:49.801775+0800 Interview01-打印[21740:9823751] 请求完成:6
2021-06-23 09:47:49.805542+0800 Interview01-打印[21740:9823751] 请求完成:7
2021-06-23 09:47:49.814714+0800 Interview01-打印[21740:9823751] 请求完成:8
2021-06-23 09:47:49.850517+0800 Interview01-打印[21740:9823753] 请求完成:9
2021-06-23 09:47:49.864394+0800 Interview01-打印[21740:9823591] end
*/

这个也比较好理解,for循环运行后堵塞当前线程(当前是主线程,你也可以把这段代码放入子线程中去执行),当10个请求全部完成后发送信号,继续下面的流程。

3. 使用NSOperation与GCD结合使用

需求:两个网络请求,第一个依赖第二个的回调结果

通过自定义operation实现,我们重写其main方法

@interface CustomOperation : NSOperation
@property (nonatomic, copy) id obj;
- (instancetype)initWithObject:(id)obj;
@end
@implementation CustomOperation

- (instancetype)initWithObject:(id)obj{
    if(self = [super init]){
        self.obj = obj;
    }
    return  self;
}

- (void)main{
    
    //创建信号量并设置计数默认为0
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    NSLog(@"开始执行任务%@",self.obj);
    NSString *str = @"https://www.360.cn";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"TASK完成:====%@ thread====%@",self.obj,[NSThread currentThread]);
        //请求成功 计数+1操作
        dispatch_semaphore_signal(sema);
    }];

    [task resume];
    
    //若计数为0则一直等待
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    
}

调用与结果

 NSOperationQueue *queue3 = [[NSOperationQueue alloc] init];
    [queue3 setMaxConcurrentOperationCount:2];
    CustomOperation *operation0 = [[CustomOperation alloc] initWithObject:@"我是任务0"];
    CustomOperation *operation1 = [[CustomOperation alloc] initWithObject:@"我是任务1"];
    CustomOperation *operation2 = [[CustomOperation alloc] initWithObject:@"我是任务2"];
    CustomOperation *operation3 = [[CustomOperation alloc] initWithObject:@"我是任务3"];

    [operation0 addDependency:operation1];
    [operation1 addDependency:operation2];
    [operation2 addDependency:operation3];

    [queue3 addOperation:operation0];
    [queue3 addOperation:operation1];
    [queue3 addOperation:operation2];
    [queue3 addOperation:operation3];
/**打印结果
开始执行任务我是任务3
TASK完成:====我是任务3 thread====<NSThread: 0x6000039c3340>{number = 5, name = (null)}
开始执行任务我是任务2
TASK完成:====我是任务2 thread====<NSThread: 0x6000039ece80>{number = 7, name = (null)}
开始执行任务我是任务1
TASK完成:====我是任务1 thread====<NSThread: 0x6000039c3340>{number = 5, name = (null)}
开始执行任务我是任务0
TASK完成:====我是任务0 thread====<NSThread: 0x6000039c3d00>{number = 6, name = (null)}
*/
  • 设置任务依赖并且添加到队列后是可以满足我们的需求
  • 由于任务内部是异步回调,可以看到任务内部的执行还是依赖于dispatch_semaphore_t来实现的
  • 也可以通过重写start方法实现