iOS开发多线程基础讲解四(NSOperation)

本文中所有代码演示均有GitHub源码,点击下载

NSOperation 是iOS中比较高级的并发编程的操作!

NSOperation : 封装了 GCD 中的"任务"!

NSOperationQueue : 封装了 GCD 中的"队列"!

  • NSOperation 是一个"抽象类",不能直接使用
  • 抽象类的用处是定义子类共有的属性和方法
  • 在苹果的头文件中,有些抽象类和子类的定义是在同一个头文件中的
  • 子类:
    • NSInvocationOperation (使用和按钮/ target)
    • NSBlockOperation (利用block 封装任务)
  • NSOperationQueue 队列

其他基本的抽象类

  • UIGestureRecognizer
  • CAAnimation
  • CAPropertyAnimation

一. 基本演练

NSInvocationOperation

start

  • start 方法 会在当前线程执行 @selector 方法
- (void)opDemo1 {
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"Invocation"];

    // start方法:直接开始,会在当前线程执行 @selector 方法
    [op start];
}

- (void)downloadImage:(id)obj {

    NSLog(@"%@ %@", [NSThread currentThread], obj);
}

添加到队列

  • 将操作添加到队列,会"异步"执行 selector 方法
- (void)opDemo2 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"queue"];

    [q addOperation:op];
}

添加多个操作

- (void)opDemo3 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    for (int i = 0; i < 10; ++i) {
        NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@(i)];

        [q addOperation:op];
    }
}

执行效果:会开启多条线程,而且不是顺序执行。与GCD中并发队列&异步执行效果一样!

结论,在 NSOperation 中:

  • 操作 -> 异步执行的任务
  • 队列 -> 全局队列

NSBlockOperation

- (void)opDemo4 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];
    #warning 打印结果是在主线程中,说明直接在主线程执行,没有开辟新的线程
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
    }];

    [q addOperation:op];
}

使用 block 来定义操作,所有的代码写在一起,更简单,便于维护!

更简单的,直接添加 Block

- (void)opDemo5 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    for (int i = 0; i < 10; ++i) {
        [q addOperationWithBlock:^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }];
    }
}

向队列中添加不同的操作

- (void)opDemo5 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    for (int i = 0; i < 10; ++i) {
        [q addOperationWithBlock:^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }];
    }

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block %@", [NSThread currentThread]);
    }];
    [q addOperation:op1];

    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"invocation"];
    [q addOperation:op2];
}
  • 可以向 NSOperationQueue 中添加任意 NSOperation 的子类

线程间通讯

- (void)opDemo6 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    [q addOperationWithBlock:^{
        NSLog(@"耗时操作 %@", [NSThread currentThread]);

        // 主线程更新 UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"更新 UI %@", [NSThread currentThread]);
        }];
    }];
}

二. 高级演练

全局队列

/// 全局操作队列,统一管理所有的异步操作
@property (nonatomic, strong) NSOperationQueue *queue;

- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
    }
    return _queue;
}

最大并发操作数

/// MARK: - 最大并发操作数
- (void)opDemo1 {

    // 设置同时并发操作数
    self.queue.maxConcurrentOperationCount = 2;

    NSLog(@"start");

    for (int i = 0; i < 10; ++i) {
        NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }];

        [self.queue addOperation:op];
    }
}

暂停 & 继续

/// MARK: - 暂停 & 继续
- (IBAction)pauseAndResume {

    if (self.queue.operationCount == 0) {
        NSLog(@"没有操作");
        return;
    }

    // 暂停或者继续
    self.queue.suspended = !self.queue.isSuspended;

    if (self.queue.isSuspended) {
        NSLog(@"暂停 %tu", self.queue.operationCount);
    } else {
        NSLog(@"继续 %tu", self.queue.operationCount);
    }
}
  • 队列挂起,当前"没有完成的操作",是包含在队列的操作数中的
  • 队列挂起,不会影响已经执行操作的执行状态
  • 对列一旦被挂起,再添加的操作不会被调度

取消全部操作

/// MARK: - 取消所有操作
- (IBAction)cancelAll {
    if (self.queue.operationCount == 0) {
        NSLog(@"没有操作");
        return;
    }

    // 取消对列中的所有操作,同样不会影响到正在执行中的操作!
    [self.queue cancelAllOperations];

    NSLog(@"取消全部操作 %tu", self.queue.operationCount);
}
  • 取消队列中所有的操作
  • 不会取消正在执行中的操作
  • 不会影响队列的挂起状态

依赖关系

#warning 不要添加循环操作依赖,一定要在添加进操作队列之前设置操作依赖
- (void)dependency {

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"登录 %@", [NSThread currentThread]);
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"付费 %@", [NSThread currentThread]);
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载 %@", [NSThread currentThread]);
    }];
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"通知用户 %@", [NSThread currentThread]);
    }];

    [op2 addDependency:op1];
    [op3 addDependency:op2];
    [op4 addDependency:op3];
    // 注意不要循环依赖
//    [op1 addDependency:op4];

    [self.queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
    [[NSOperationQueue mainQueue] addOperation:op4];

    NSLog(@"come here");
}

追加操作

  • 当 NSBlockOperation 中的任务书 > 1 之后,无论是将操作添加到主线程,还是在主线程直接执行start, NSBlockOperation中的任务执行顺序都不确定,执行线程也不确定!

  • 一般在开发中,要避免向NSBlockOperation 中追加任务

  • 如果任务都是在主线程中执行,并且不需要保证执行顺序,可以直接追加任务

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 1. 实例化操作对象
    NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"11111--->%@", [NSThread currentThread]);
    }];

    // 2. 往当前操作中追加操作一
    [blockOperation1 addExecutionBlock:^{
        NSLog(@"追加任务1--->%@", [NSThread currentThread]);
    }];

    // 2. 往当前操作中追加操作二
    [blockOperation1 addExecutionBlock:^{
        NSLog(@"追加任务2--->%@", [NSThread currentThread]);
    }];

    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"22222--->%@", [NSThread currentThread]);
    }];


    NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"33333--->%@", [NSThread currentThread]);
    }];

    // 将操作添加到主队列中
    //    [[NSOperationQueue mainQueue] addOperation:blockOperation1];
    //    [[NSOperationQueue mainQueue] addOperation:blockOperation2];
    //    [[NSOperationQueue mainQueue] addOperation:blockOperation3];
    /*
     输出结果:

     11111---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     追加任务2---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     追加任务1---><NSThread: 0x7fed62f07ba0>{number = 4, name = (null)}
     22222---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     33333---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     */


    // 将操作添加到非主队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    [queue addOperation:blockOperation1];
    [queue addOperation:blockOperation2];
    [queue addOperation:blockOperation3];
    /*
     输出结果:

     22222---><NSThread: 0x7fba18f08a20>{number = 9, name = (null)}
     11111---><NSThread: 0x7fba18f03780>{number = 7, name = (null)}
     33333---><NSThread: 0x7fba18d99c20>{number = 10, name = (null)}
     追加任务1---><NSThread: 0x7fba18f08770>{number = 8, name = (null)}
     追加任务2---><NSThread: 0x7fba18c06220>{number = 11, name = (null)}
     */

}

三. 与 GCD 的对比

  • GCD

    • 任务(block)添加到队列(串行/并发/主队列),并且指定任务执行的函数(同步/异步)
    • GCD是底层的C语言构成的API
    • iOS 4.0 推出的,针对多核处理器的并发技术
    • 在队列中执行的是由 block 构成的任务,这是一个轻量级的数据结构
    • 要停止已经加入 queueblock 需要写复杂的代码
    • 需要通过 Barrier 或者同步任务设置任务之间的依赖关系
    • 只能设置队列的优先级
    • 高级功能:
      • 一次性 once
      • 延迟操作 after
      • 调度组
  • NSOperation

    • 核心概念:把操作(异步)添加到队列(全局的并发队列)
    • OC 框架,更加面向对象,是对 GCD 的封装
    • iOS 2.0 推出的,苹果推出 GCD 之后,对 NSOperation 的底层全部重写
    • Operation作为一个对象,为我们提供了更多的选择
    • 可以随时取消已经设定要准备执行的任务,已经执行的除外
    • 可以跨队列设置操作的依赖关系
    • 可以设置队列中每一个操作的优先级
    • 高级功能:
      • 最大操作并发数(GCD不好做)
      • 继续/暂停/全部取消
      • 跨队列设置操作的依赖关系

四. 自定义操作

准备工作

  • 自定义 DownloadImageOperation 继承自 NSOperation
  • 代码调用
// 实例化自定义操作
DownloadImageOperation *op = [[DownloadImageOperation alloc] init];
// 将自定义操作添加到下载队列
[self.downloadQueue addOperation:op];

需求驱动开发

目标一:设置自定义操作的执行入口

对于自定义操作,只要重写了 main 方法,当队列调度操作执行时,会自动运行 main 方法

注意:为了能够及时释放内存,main方法内部一般建立一个自动释放池,但是苹果官方文档不要求写!

- (void)main {
    @autoreleasepool {

        NSLog(@"main--->%@", [NSThread currentThread]);

        NSString *filePath = @"http://ww1.sinaimg.cn/bmiddle/c260f7abjw1exxbbyckd6j20gn0m8wgh.jpg";

        UIImage *image = [self downLoadImageWithStr:filePath];

        // 回到主线程设置UI
        [[NSOperationQueue mainQueue] addOperation:[NSBlockOperation blockOperationWithBlock:^{

            NSLog(@"setupUI--->%@", [NSThread currentThread]);

            self.imgView.image = image;
        }]];
    }
}

// 抽取出下载图片的方法
- (UIImage *)downLoadImageWithStr:(NSString *)filePath {

    // 1. 转化为地址
    NSURL *url = [NSURL URLWithString:filePath];

    // 2. 转化为NSData 二进制类型数据(下载方法,耗时方法)
    NSData *data = [NSData dataWithContentsOfURL:url];

    // 3. 转化为图片类型
    UIImage *image = [UIImage imageWithData:data];

    NSLog(@"downLoadImage ---> %@", [NSThread currentThread]);

    return image;
}

目标二:给自定义参数传递参数

  • 定义属性
/** 承载图片的容器 */
@property (nonatomic, strong) UIImageView *imgView;
  • 代码调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    NSLog(@"touchBegin--->%@", [NSThread currentThread]);

    // 1. 实例化自定义操作对象
    NSDownLoadOperation *operation = [[NSDownLoadOperation alloc] init];

    // 2. 告诉操作在哪里显示图片
    // 注意: 不能做以下改变 self.imageView = [[UIImageView alloc] init];
    operation.imgView = self.imgView;

    // 3.1 将自定义操作添加到下载队列,操作启动后会执行 main
     [self.queue addOperation:operation];

    // 3.2 直接开始
    // 这样的话,全部操作都在当前线程(主线程)中进行操
    //[operation start];

    NSLog(@"touchEnd--->%@", [NSThread currentThread]);
}

注意,main 方法被调用时,属性已经准备就绪

目标三:如何回调?

利用系统提供的 CompletionBlock 属性
// 设置完成回调
[operation setCompletionBlock:^{
    NSLog(@"完成 %@", [NSThread currentThread]);
}];
  • 只要设置了 CompletionBlock,当操作执行完毕后,就会被自动调用
  • CompletionBlock 既不在主线程也不在操作执行所在线程
  • CompletionBlock 无法传递参数

自己定义回调 Block,在操作结束后执行

  • block: 指向结构体的指针!块代码!闭包!!

  • 闭包: javascript/js最先使用 :可以从函数外部访问函数内部的变量! --- 灵活性!

    1. 定义 block 类型 (返回值、接受的参数类型...)
    2. 执行 block 中执行的内容(block 中封装的代码)
    3. 执行或调用 block (相当于调用函数)

    ----以上三个步骤,必须按顺序进行!
    可以在不同的对象中,分别设置三个步骤,只要保证顺序就OK

  • 定义属性

/// 完成回调 Block
@property (nonatomic, copy) void (^finishedBlock)(UIImage *image);
  • 设置自定义回调
// 设置自定义完成回调
[op setFinishedBlock:^(UIImage *image) {
    NSLog(@"finished %@ %@", [NSThread currentThread], image);
}];
  • 耗时操作后执行回调
// 判断自定义回调是否存在
if (self.finishedBlock != nil) {
    // 通常为了简化调用方的代码,异步操作结束后的回调,大多在主线程
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.finishedBlock(image对象);
    }];
}

目标四:简化操作创建

  • 定义方法
///  实例化下载图像操作
///
///  @param URLString 图像 URL 字符串
///  @param finished  完成回调 Block
///
///  @return 下载操作实例
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *image))finished;
  • 实现方法
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
    DownloadImageOperation *operation = [[DownloadImageOperation alloc] init];

    operation.URLString = URLString;
    operation.finishedBlock = finished;

    return operation;
}
  • 方法调用
// 使用类方法实例化下载操作
DownloadImageOperation *operation = [DownloadImageOperation downloadImageOperationWithURLString:@"http://www.baidu.com/img/bdlogo.png" finished:^(UIImage *image) {
    NSLog(@"%@", image);
}];

// 将自定义操作添加到下载队列,操作启动后会执行 main 方法
[self.downloadQueue addOperation:op];

目标五:取消操作

在关键节点添加 isCancelled 判断

  • 添加多个下载操作
for (int i = 0; i < 10; ++i) {
    NSString *urlString = [NSString stringWithFormat:@"http://www.xxx.com/%04d.png", i];

    DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:urlString finished:^(UIImage *image) {
        NSLog(@"===> %@", image);
    }];

    // 将自定义操作添加到下载队列,操作启动后会执行 main 方法
    [self.downloadQueue addOperation:op];
}
  • 设置队列最大并发操作数
_downloadQueue.maxConcurrentOperationCount = 2;
  • 内存警告时取消所有操作
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

    [self.downloadQueue cancelAllOperations];
}

cancelAllOperations 会向队列中的所有操作发送 Cancel 消息

  • 调整 main 方法,在关键节点判断
- (void)main {
    NSLog(@"%s", __FUNCTION__);

    @autoreleasepool {

        NSLog(@"下载图像 %@", self.URLString);
        // 模拟延时
        [NSThread sleepForTimeInterval:1.0];

        if (self.isCancelled) {
            NSLog(@"1.--- 返回");
            return;
        }

        // 判断自定义回调是否存在
        if (self.finishedBlock != nil) {
            // 通常为了简化调用方的代码,异步操作结束后的回调,大多在主线程
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                self.finishedBlock(self.URLString);
            }];
        }
    }
}

注意:如果操作状态已经是 Cancel,则不会执行 main 函数

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

推荐阅读更多精彩内容