iOS OC多线程

953 阅读16分钟
  • NSThread
  • GCD
  • NSOperation

一、多线程概念

1.1 同步/异步

  • 同步
    • 代码从上到下顺序执行
    • 一个人依次执行任务,一次执行一个任务
  • 异步
    • 多个人可以同时执行多个任务

1.2 进程/线程

  • 进程
    • 系统中独立运行的一个应用程序
    • 每个进程之间是独立的, 每个进程均运行在其专用的受保护的内存空间内
    • 通过活动监视器可以查看Mac系统中所开启的进程
  • 线程
    • 一个进程由多个线程组成, 至少一个
    • 线程是进程的基本执行单元, 一个进程中的所有任务都在线程中执行

1.3 多线程

  • 一个进程可以开启多个线程, 多个线程可以"同时"执行不同的任务

  • 多线程可以解决程序阻塞的问题

  • 多线程可以提高程序的执行效率

1.3.1 多线程执行原理

  • 单任务操作系统
    • 只有进程, 没有线程
    • 进程顺序执行
  • 多任务操作系统
    • 引入了线程
    • 同一时间可以执行多个程序
    • 单核CPU中同一时间CPU只能处理1个线程, 只有1个线程在执行
    • CPU在线程之间切换,切换之前保存当前线程状态,切换到下一个线程的状态,调度线程的时间足够快, 多线程"同时"执行
    • 如果线程数量非常多, CPU会在n个线程之间切换, 消耗大量的CPU资源
      • 线程本身也占用一定资源
      • 每个线程被调度的次数也会降低, 效率也就下去了

1.3.2 多线程优缺点

  • 优点
    • 能适当提高程序执行效率
    • 能提高资源利用率(CPU, 内存)
    • 线程上的任务执行完成后, 线程会自动销毁
  • 缺点
    • 开启线程占用一定的内存空间(默认情况下, 每个线程占用512KB)
    • 如果开启大量的线程, 就会占用大量的内存, 降低程序的性能
    • 线程越多, CPU在线程的开销就越大
    • 程序设计更加复杂, 线程间的通信、多线程的数据共享

1.3.3 主线程

  • 一个程序运行后, 默认会开启一个线程, 称为“主线程”或“UI主线程”
  • 主线程一般用来刷新UI界面、处理UI事件(点击、滚动、拖拽)
  • 主线程使用注意
    • 别将耗时的操作放到主线程
    • 耗时操作会阻塞主线程, 严重影响UI的流畅度, 给用户一种坏体验

1.3.4 何时使用

耗时操作

  • 网络请求
  • I/O

二、iOS中多线程的技术方案

  • pthread
    • POSIX 表示可移植操作系统接口(Protable Operating System Interface) pThread
  • NSThread
  • GCD
  • NSOperation

image.png

2.1 pthread

#import <pthread.h>

pthread_t pthread;
NSString *name = @"lisi";
int result =  pthread_create(&pthread, NULL, demo2, (__bridge void *)(name));

// 线程执行函数
void *demo2(void *param) {
    NSString *name = (__bridge NSString *)(param);
    NSLog(@"hello %@ %@", name, [NSThread currentThread]);
    return NULL;
}

2.2 NSThread

// 方式1
NSThread *thread = [[NSThread alloc] initWithTarget: self selector:@selector(printHello) object:nil];
[thread start];

// 方式2
[NSThread detachNewThreadSelector:@selector(printHello) toTarget:self withObject:nil];

// 方式3
[self performSelectorInBackground:@selector(printHello:) withObject: @"lisi"];

2.2.1 线程状态

  • 新建
  • 就绪 (start) 进入可调度线程池 和其他线程一起等待被执行
  • 运行 CPU调度 在不同线程之间切换执行
  • 阻塞 从可调度线程池移出
  • 死亡 自然执行完或 或 出错exit

2.2.2 线程属性

  • name
    • 设计线程名称可以在线程执行的方法内部出现异常的时候记录 异常和当前线程
  • threadPriority
    • 0到1 1表示优先级别最高

    • 内核调度算法在决定运行哪个线程的时候, 会把线程的优先级作为考量因素, 较高的优先级的线程会比较低优先级的线程具有更多的运行机会。较高优先级不保证线程的具体执行时间先后, 只是相比较低优先级的线程, 它更有可能被调度器选择执行。

2.2.3 多线程访问共享资源的问题

  • 共享资源
    • 一个资源可能被多个线程共享, 即多个线程可能会访问同一块资源
    • 多个线程访问同一个对象、同一个变量、同一个文件
  • 当多个线程访问同一块资源的时候, 很容易引发数据错乱和数据安全问题
  • 加锁后会影响程序的执行效率
  • 线程安全
    • 线程同时操作是不安全的, 多个线程同时操作一个全局变量
    • 线程安全: 在多个线程进行读写操作的时候, 仍然能够保证数据的正确
  • 主线程(UI线程)
    • 几乎所有UIKit提供的类都是线程不安全的, 但所有更新UI的操作都放在主线程上进行, 这样就安全了
    • 所有包含Mutable的类都是线程不安全的
      • NSMutableString
      • NSMutableArray
      • NSMutableDictionary

2.2.4 互斥锁 @synchronized

  • 能有效防止多线程抢夺资源造成的数据安全问题, 数据的正确性
  • 线程同步, 顺序访问临界区域
  • 锁的部分越少越好, 严格限制临界区域的语句
  • 互斥锁原理:
    • 每个对象内部都有一个锁(变量), 当有线程要进入sychronized到代码块中会先检查对象的锁是打开还是关闭状态, 默认锁是打开的, 如果线程执行到代码块内部, 会先上锁, 如果锁被关闭, 再有线程要执行代码块就先等待, 直到锁打开才可以进入。
    • 加锁后程序的执行效率比不加锁的时候要低, 因为线程要等待锁, 但是锁保证了多个线程同时操作全局变量的安全性
@synchronized (对象) {
    //读写操作
}

2.2.5 原子属性

  • 属性中的修饰符

    • nonatomic 非原子属性
    • atomic 原子属性(线程安全), 针对多线程设计, 默认值, 保证同一时间只有一个线程能够写入(但同一时间多个线程都能取值)
      • atomic 本身就有一把锁(自旋锁)
      • 单写多读: 单个线程写入, 多个线程可以读取
  • nonatomic 和 atomic对比

    • atomic: 线程安全, 需要消耗大量的资源
    • nonatomic: 非线程安全, 适合内存小的移动设备
  • iOS开发的建议

    • 所有属性都声明为nonatomic
    • 尽量避免多线程抢夺同一块资源
    • 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理, 减小移动客户端的压力

2.2.6 互斥锁和自旋锁

  • 互斥锁
    • 如果发现其他线程正在执行锁定代码, 线程会进入休眠, 等其他线程时间片到打开锁后, 线程会被唤醒
  • 自旋锁
    • 如果发现有其他线程正在锁定代码, 线程会用死循环的方式, 一直等待锁定的代码执行完成, 自旋锁更适合执行不耗时的代码

2.2.7 weak和strong

  • OC对象用strong
  • 连线的UI对象用weak
    • self.view.subviews.新增对象 用strong也不影响

2.2.8 自动释放池

  • iOS开发中的内存管理

    • iOS开发中, 并没有JAVA或者C#中的垃圾回收机制(OC有垃圾回收)
    • MRC中对象谁申请, 谁释放
    • 使用ARC开发, 只是在编译的时候, 编译器会根据代码结构自动提添加retain、release和autorelease
  • 自动释放池

    • 标记为autorelease的对象, 会被添加到最近一次创建的自动释放池中
    • 当自动释放池被销毁或耗尽的时候, 会向自动释放池中的所有对象发送release消息
    • 每一次主线程消息循环的时候会创建自动释放池
      • 事件机制是基于消息循环的
      • 程序一直执行等待用户的输入是因为消息循环
      • 消息循环在一个循环内部不断接收用户的输入
      • 消息循环作用
        • 程序不退出
        • 处理用户的事件
    • 运行循环结束前, 会释放自动释放池

image.png

  • 主线程消息循环

    • 程序启动开启主线程的消息循环
    • 等待用户输入事件
    • 创建Event
    • 创建自动释放池
    • 处理事件, 一些对象放入自动释放池中
    • 一次循环结束前, 销毁自动释放池, 发送release消息
  • 什么时候使用自动释放池

    • 循环中创建大量临时变量, 循环内部创建
      • 大循环中创建的临时变量无法及时回收
    • 开启子线程需要创建子线程的自动释放池, 开始执行的时候
      • 自线程无法访问主线程的自动释放池

2.2.9 属性修饰符

以下的为对属性的修饰符的讨论

  • NSString 用copy
    • 如果使用strong 原字符串和当前字符串指向一致, 一起变化
    • copy对原来的字符串进行了拷贝操作
  • block使用copy
    • block MRC下捕获变量为栈block
    • 如果block作为属性使用assign, 栈block赋值之后被清空, 访问野指针
    • copy属性表示block在赋值的时候做了一次copy操作, 从栈block变为了堆block
  • delegate使用weak
    • 当前类 控制器的属性strong 当前类的delegate又设置为了self 循环引用
    • 为防止这种情况, delegate属性会被设置为weak
  • assign和weak
    • assign 基本数据类型
      • 如果assign修饰对象, 在栈上开辟一片空间存储新建对象在堆上的地址, 之后堆上的对象没有被强引用, 被释放 assign的内存地址不变 但指向的空间已经没有对象了 这个时候就会出现野指针错误
    • weak 对象
      • 如果新建对象赋值给weak, 在栈上开辟一片空间存储新建对象在堆上的地址, 之后堆上的对象没有被强引用, 被释放, weak会变为nil , 对这个weak发送消息不会发生任何事情
        • objc_msgSend(消息接受者, 消息主体); 消息接收者为nil, 无法找到方法进行响应, 根据方法的返回值类型返回, 如果是对象返回nil, 如果返回值类型为指针, 指针大小为基本数据类型, 返回0, 返回值类型为结构体, 结构体字段用0填充
  • MRC和ARC下都能使用assign, 只有ARC下才可以使用weak
  • MRC下使用retain, ARC中使用strong
  • MRC和ARC下都能用copy

2.2.10 消息循环

  • 什么是消息循环

    • Runloop就是消息循环, 每个线程内部都有一个消息循环
    • 只有主线程的消息循环默认开启, 子线程的消息循环默认不开启
  • 消息循环的目的

    • 保证程序不退出
    • 负责处理输入事件
    • 如果没有事件发生, 会让程序进入休眠状态
  • 输入事件

    • 输入源 input source 触摸 键盘 自定义输入源 performSelector: onThread

    • 定时源 定时器 定时器执行的方法不易执行太耗时的操作, 否则会降低用户体验, 用户拖拽的时候会感到卡顿

  • 消息循环模式

    • NSDefaultRunLoopMode
      • 使用最广的
      • 处理 除了NSConnection 外的输入源
    • NSRunLoopCommonModes
      • 包括以下等等
        • NSDefaultRunLoopMode
        • UITrackingRunloopMode scrollview拖拽时消息模式发生变化
  • 其他

    • 消息循环模式必须和输入事件的模式匹配才会执行响应的事件
    • 消息循环运行在某一种消息循环模式上, 没有指定就是默认
// 定时源加到当前runloop
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2.2.11 子线程的消息循环

  • 主线程消息循环默认开启, 子线程消息循环不会开启
  • 启动子线程的消息循环
    • [[NSRunLoop currentRunLoop] run]]
  • 线程池, 在开启线程后永不销毁, 当需要让子线程执行新的方法, 使用performSelector让指定的方法在指定的子线程上运行
//在子线程添加输入源 开启runloop 
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
[thread start];

// 添加输入源
[self performSelector:@selector(demo1) onThread:thread withObject:nil waitUntilDone:NO];

// 子线程执行方法
- (void)demo {
   NSLog(@"I'm running");
   // run方式开启 消息循环一直执行 如果消息循环中没有设置输入事件 消息循环会立即结束         
   [[NSRunLoop currentRunLoop] run];
   
   // 或者run同时设置自动关闭时间
   [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
   
   // 如果设置了输入事件并且输入事件匹配当前runloop的mode 会等待下个输入事件不执行这里
   NSLog(@"End");
}

// 执行在子线程的消息循环当中的输入源的方法
- (void)demo1 {
   NSLog(@"I'm running on subThread runloop");
}

2.3 GCD

  • Grand Central Dispatch
  • 纯C语言, 提供了非常多强大的函数
  • GCD的优点
    • 苹果公司为多核的并行运算提出的解决方案
    • GCD会自动利用更多的CPU内核
    • GCD会自动管理线程的生命周期(创建、调度、销毁)
    • 程序员只需要告诉GCD需要执行什么任务, 不需要写任何线程管理代码
    • GCD管理着一个线程池, 已经执行完任务的线程会在线程中存在一段时间, 当线程池中存在可用线程, 会进行重用

2.3.1 任务和队列

  • GCD核心
    • 任务 方法
    • 队列 用来存放任务
  • GCD使用步骤
    • 创建任务
    • 将任务添加到队列中
      • GCD会自动将队列中的任务取出, 放入对应的线程中执行
      • 任务的取出遵循队列的FIFO原则: 先进先出
  • 两个用来执行任务的函数
    • 同步的方式执行
    • 异步的方式执行
// 同步 全局队列 当前线程执行
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
   NSLog(@"hello %@", [NSThread currentThread]);
});

2.3.2 队列

  • 队列类型
    • 并发队列 Concurrent Dispatch Queue
      • 多个任务并发执行
      • 只有在异步函数下才有效
    • 串行队列 Serial Dispatch Queue
      • 任务顺序执行
      • 包括主队列, 也叫全局串行队列
  • 同步和异步决定了要不要开新的线程
    • 同步, 在当前线程中执行, 不具备开启新的线程的能力
    • 异步, 新的线程中执行, 能够开启新的线程
  • 并发和串行决定了任务的执行方式
    • 并发, 多个任务并发执行
    • 串行, 顺序执行

2.3.3 串行队列

  • 同步执行

    • 不开启新线程
    • 顺序执行队列内的任务
  • 异步执行

    • 开启一个新的线程
    • 线程顺序执行队列中的任务
  • 串行队列 同步 主队列进行同步会死锁

// 当前线程 顺序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i++) {
    // 同步
    dispatch_sync(queue, ^{
        NSLog(@"%d %@", i, [NSThread currentThread]);
    });
}
  • 串行队列 异步
// 只新开一个线程 顺序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i++) {
    // 同步
    dispatch_async(queue, ^{
        NSLog(@"%d %@", i, [NSThread currentThread]);
    });
}

2.3.4 并发队列

  • 同步执行
    • 不开新线程
    • 顺序执行队列内的任务
  • 异步执行
    • 能够开启多个线程

    • 无序执行

  • 并发队列 同步
// 当前线程 顺序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
    dispatch_sync(queue, ^{
        NSLog(@"%d %@", i, [NSThread currentThread]);
    });
}
  • 并发队列 异步
// 开启多个线程 无序输出
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
    dispatch_async(queue, ^{
        NSLog(@"%d %@", i, [NSThread currentThread]);
    });
}

2.3.5 主队列

  • 主队列先执行完主线程上的代码, 才会执行队列中的任务
  • 主队列 异步任务
    • 不开线程, 同步执行
    • 主队列特点: 如果主线程正在执行代码暂时不调度任务, 等主线程执行结束后再调度任务
    • 主队列也叫全局串行队列
// 在主线程上 顺序执行
- (void)demo1 {
    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"hello %d %@", i, [NSThread currentThread]);
        });
    }
}
  • 主队列 同步执行
    • 在主线程上 写这段代码 程序执行不出来(死锁)
    • 死锁的原因, 当程序执行到下面这段代码时
      • 主队列: 如果主线程正在执行代码, 就不调度任务
      • 同步执行: 如果第一个任务没有执行, 就继续等待第一个任务执行完成, 再执行下一个任务
      • 此时互相等待, 程序无法往下执行
// 出现错误 _dispatch_sync_f_slow
// 主队列在等待主线程将代码执行完(第一个任务执行完), 之后再执行调度任务
// 主线程遇到同步, 等待这一段代码执行完成
- (void)demo2 {
    for (int i = 0; i < 10; i++) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"hello %d %@", i, [NSThread currentThread]);
        });
    }
}
  • 主队列和串行队列区别
    • 串行队列: 等待一个任务执行完成, 才会调度下一个任务
    • 主队列: 先进先出调度队列, 如果主线程上有代码执行, 主队列不会调度任务
// 解决主队列串行死锁
// 将主队列同步代码 用异步新开线程包裹起来
// 同步执行放在了子线程 任务执行放到了主线程
- (void)demo3 {
    // 全局队列异步执行
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"全局队列 异步执行%@", [NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"主队列 同步执行%@", [NSThread currentThread]);
        });
    });
    NSLog(@"==");
}

2.3.5 全局队列

  • 全局队列的本质就是并发队列
    • dispatch_get_global_queue(0,0);
    • 参数1 服务质量(优先级) QOS_CLASS_DEFAULT
    • 参数2 flags 保留参数供未来使用, 传0避免返回值为NULL
  • 执行效果和并行队列相同
  • 全局队列和并发队列区别
    • 并发队列有名称, 可以跟踪错误, 全局队列没有
    • 在ARC中不需要考虑释放内存, dispatch_release(q); 不允许调用。在MRC中需要手动释放内存
      • 并发队列create后需要release
      • 全局队列不需要release, 一直存在
    • 一般使用全局队列

2.3.6 同步任务

  • 同步任务作用
    • 按先后顺序执行
    • 异步执行间的依赖关系
  • 同步任务特点:
    • 队列调度多个异步任务前, 指定一个同步任务, 让所有的异步任务都等待同步任务执行完, 这就是所谓的依赖关系

app下载, 验证密码, 扣费, 下载, 将验证密码的同步任务放到一个异步执行中去, 验证密码的同步任务会在子线程中执行, 不会阻塞UI

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"输入密码 %@", [NSThread currentThread]);
    });

    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"扣费%@", [NSThread currentThread]);
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"下载应用%@", [NSThread currentThread]);
    });
});
  • 各种队列执行效果 | |串行队列|主队列 | 并行队列| |:---: | :---: | :---: | :----: | |同步sync| | | |

| 一 |串行队列|并行队列|主队列| | --- | --- | --- | --- | | 同步执行 | | | |

串行队列主队列并行队列
同步sync不开新线程顺序执行主线程内死锁不开新线程顺序执行
异步async开新线程顺序执行就在主线程上顺序执行开新线程并发执行

2.3.7 Barrier 栅栏函数

  • 主要用于在多个异步操作完成之后, 统一对非线程安全的对象进行更新
  • 适合大规模的I/O操作
  • 当访问数据库或文件的时候, 更新数据的时候不能和其他更新或者读取的操作在统一时间执行, 可以使用调度组不过有点复杂, 可以使用dispatch_barrier_async解决
  • 队列创建的时候必须显示指定为是并行队列, 否则无法起到阻塞的作用, 效果和dispatch_async一样
// 模拟下载多张图片 多线程中访问线程不安全对象
dispatch_queue_t queue = dispatch_queue_create("yyh", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSString *fileName = [NSString stringWithFormat:@"image/0%d.jpg", i % 10];
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    NSLog(@"下载图片%d %@",i , [NSThread currentThread]);
    dispatch_barrier_async(queue, ^{
        [self.imageList addObject:image];
        NSLog(@"保存图片%d %@",i , [NSThread currentThread]);
    });
});

2.3.8 延迟执行和一次性执行

  • 延迟执行
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
		dispatch_block_t block);
  • 一次性执行 线程安全的
// 在当前线程执行 通过静态全局onceToken判断 初始化为0 执行完为-1
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"hello ,%@",[NSThread currentThread]);
});

2.3.9 单例

  • 工具类 不用每次都创建对象 程序中只存在一个实例
  • 通过互斥锁保证线程安全和dispatch_once相比, 更加耗时, 前者是加锁释放锁, 后者是做简单的判断
// 单例不应该存在多个入口 以下仅仅用于比较
+ (instancetype)sharedNetworkTools{
    static id instance = nil;
    // 线程同步, 保证线程安全的
    @synchronized (self) {
        if (instance == nil) {
            instance = [[self alloc] init];
        }
    }
    return instance;;
}

+ (instancetype)sharedNetworkToolsWithOnce{
    static id instance = nil;
    
    // dispatch_once本身就是线程安全的
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (instance == nil) {
            instance = [[self alloc] init];
        }
    });
    
    return instance;
}

2.3.10 调度组

有时候需要在多个异步任务都执行完成之后继续做某些事情, 比如下载歌曲, 等所有歌曲下载完毕后, 主线程提示用户

// 演示调度组 监测任务数 采用group计数 加入任务时+1 执行完任务-1 任务数减为0的时候notify
- (void)demo1 {
    // 创建组
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"正在下载第一个歌曲");
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"正在下载第2个歌曲");
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"正在下载第3歌曲");
    });
    
    // 运行的线程 取决于队列
    // 三个异步任务执行完毕, 才执行
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"over, %@", [NSThread currentThread]);
    });
}

调度组原理

- (void)demo2 {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任务1");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任务3");
        dispatch_group_leave(group);
    });
    
    //等待组中的任务执行完毕
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"over");
    });
    
    //等待组中任务执行完毕才会执行后续代码
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"hello");
}
  • 终端输入man dispatch_group_async

2.4 NSOperation

  • OC语言中基于GCD面向对象的封装
  • 使用比GCD更简单
  • 提供了GCD不好实现的功能
  • 苹果推荐使用, 不用关心线程和线程的生命周期
  • NSOperation头文件
    • NSOperation是一个抽象类
      • 不能直接使用(方法没有实现)
      • 约束子类都有共同的属性和方法
    • NSOperation的子类
      • NSInvocationOperation
      • NSBlockOperation
      • 自定义operation
  • NSOperationQueue

使用NSOperation和NSOperationQueue实现多线程的具体步骤

  • 执行的操作封装到NSOperation对象中
  • NSOperation对象添加到NSOperationQueue中
  • 系统自动将NSOperationQueue中的NSOperation取出来
  • 将取出的NSOpreation封装的操作放到一条新线程当中执行

2.4.1 NSInvocationOperation

  • 创建NSInvocationOperation对象
  • 调用start方法开始执行操作
    • 更新操作的状态
      • cancelled
      • executing
      • finished
    • 调用操作的main方法
  • 或者添加到队列 开启子线程
NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo1) object:nil];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];

默认情况下调用start并不会开一条新线程去执行操作, 而是在当前线程同步执行操作, 只有将NSOperation放到NSOperationQueue中, 才会执行异步操作

2.4.2 NSBlockOperation

  • 创建NSBlockOperation对象
  • 通过addExecutionBlock:方法添加更多操作
  • 只要NSBlockOperation封装的操作数>1, 就会异步执行操作
// 直接start
- (void)demo1 {
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"第1个 %@", [NSThread currentThread]);
    }];
    
    for (int i = 0; i < 10; i++) {
        [bo addExecutionBlock:^{
            NSLog(@"第%d个 %@", i, [NSThread currentThread]);
        }];
    }
    
    [bo start];
}


// 操作添加到队列
- (void)demo2 {
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"第1个 %@", [NSThread currentThread]);
    }];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    for (int i = 0; i < 10; i++) {
        [bo addExecutionBlock:^{
            NSLog(@"第%d个 %@", i, [NSThread currentThread]);
        }];
    }
    
    [queue addOperation:bo];
}


// block直接添加到queue
- (void)demo3 {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        NSLog(@"第1个 %@", [NSThread currentThread]);
    }];
}


// 操作的 completionBlock
- (void)demo5 {
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"hello %@", [NSThread currentThread]);
    }];
    
    // 这个block依然在子线程当中执行 不能设置UI相关
    [op setCompletionBlock:^{
        NSLog(@"操作完成 %@", [NSThread currentThread]);
    }];
    
    [self.myQueue addOperation:op];
}

2.4.3 NSOperationQueue

  • NSOperationQueue作用
    • NSOperation可以调用start方法执行任务, 但默认是同步执行的。如果将NSOperation添加到NSOperationQueue中, 系统会自动异步执行NSOperation中的操作
  • 添加操作到NSOperationQueue中
    • addOperation:
    • addOperationWithBlock:

2.4.5 线程间通信

  • 主队列
    • 添加到主线程的操作, 最终都执行在主线程上
    • [NSOperationQueue mainQueue]
  • 当前队列
    • 获取当前操作所在的队列
    • [NSOperationQueue currentQueue]
  • 模拟当图片下载完成后回归到主线程上更新UI
// 异步下载图片
[self.myQueue addOperationWithBlock:^{
    //回到主线程更新UI
    NSLog(@"异步下载 %@", [NSThread currentThread]);
    [[NSOperationQueue  mainQueue] addOperationWithBlock:^{
        NSLog(@"更新UI %@", [NSThread currentThread]);
    }];
}];

2.4.6 NSOperation和GCD比较

  • GCD
    • iOS4.0推出, 主要对多核CPU做了优化, 是C语言的技术
    • GCD是将任务(block)添加到队列(串行/并行/全局/主队列), 并且以同步/异步的方式执行任务的函数
    • GCD提供了一些NSOperation不具备的功能
      • 一次性执行 dispatch_once
      • 延迟执行 dispatch_after
      • 调度组 dispatch_group
  • NSOperation
    • iOS2.0推出, iOS4之后重写了NSOperation, 面向对象
    • NSOperation将操作(异步的任务)添加到队列(并发队列), 就会执行指定操作的函数
    • NSOperation里提供了方便的操作
      • 最大并发数
      • 队列的暂停/继续
      • 取消所有的操作
      • 指定操作之间的依赖关系(GCD可以同步实现)

2.4.7 最大并发数

  • 什么是并发数 同时执行的任务数, 比如同时开3个线程执行3个任务, 并发数是3, 但是线程数量不一定等于同时执行的任务数, 一般来说会比这个任务数大, 约等于任务数

  • 最大并发数相关方法

    • maxConcurrentOperationCount
    • setMaxConcurrentOperationCount:
  • 执行的过程

    • 把操作添加到队列
    • 去线程池取空闲的线程, 如果没有就创建线程
    • 把操作交给从线程池中取出的线程执行
    • 执行完成后, 线程放回线程池
    • 重复2,3,4直到所有的操作都做完
// 懒加载
- (NSOperationQueue *)queue{
    if (_queue == nil) {
        _queue = [NSOperationQueue new];
        _queue.maxConcurrentOperationCount = 2;
    }
    return _queue;
}

for (int i = 0; i < 20; i++) {
    [self.queue addOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"i %@", [NSThread currentThread]);
    }];
}

2.4.8 队列的暂停、取消、恢复

  • 取消队列所有操作 cancelAllOperations

    • 正在执行的操作会执行完毕, 后续操作会取消
    • 取消后队列移除剩余没有执行操作
  • 暂停和恢复队列

    • setSupended: YES表示暂停, NO代表恢复队列
      • 当前正在执行的操作会执行完毕, 后续操作会暂停
      • 没有执行的操作不会从队列中移除
    • isSuspended

2.4.9 操作在队列中你的优先级

  • 设置NSOperation在queue中的优先级, 可以改变操作的执行优先级
    • queuePriority
    • setQueuePriority
  • iOS8以后推荐使用服务质量 qualityOfService
  • 无法确保执行顺序严格依照优先级
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
}

2.4.10 completionBlock

  • 可以监听一个操作的执行完毕
  • setCompletionBlock:
  • 执行在子线程上

2.4.11 操作依赖

  • NSOperation之间可以设置依赖保证执行顺序
  • 比如一定要让A执行完后, 再去执行B
    • [operationB addDependency:operationA] //操作B依赖于操作A
    • 可以在不同的queue的NSOperation之间创建依赖关系
    • 不能互相依赖 A依赖B B依赖A
NSBlockOperation *op0 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下载 %@",[NSThread currentThread]);
}];

NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    [NSThread sleepForTimeInterval:2.0];
    NSLog(@"解压 %@",[NSThread currentThread]);
}];

NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"安装 %@",[NSThread currentThread]);
}];

[op2 addDependency:op1];
[op1 addDependency:op0];

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