记录寄己走过的路

iOS 模块详解—「多线程面试、工作」看我就 🐒 了 ^_^.

引导


谈到iOS多线程,想必大家第一反应就是多线程4种实现方案 1.pthread、2.NSThread、3.GCD、4.NSOperation;它们每个的用法、特点、应用场景及注意点,文章会一一讲到。

本篇文章主要从【iOS多线程模块】学习总结,
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。

目录:

  1. 多线程相关概念
    1.进程和线程概念
    2.多线程概念
    3.主线程
    4.GCD相关概念
  2. pthread & NSThread
    1.pthread
    2.NSThread
    1> NSThread创建线程有3个方法
    2> NSThread其它方法
    3> NSThread线程安全
    4> NSThread线程间通信
    5> NSThread线程状态转换
  3. GCD中枢调度器
    1.什么是GCD
    2.GCD基本概念
    3.任务&队列组合使用
    4.GCD的优势
    5.GCD基本使用
    6.GCD常见用法和应用场景
    7.内存和安全
    8.单例模式
    9.总结
  4. NSOperation操作队列
    1.什么是NSOperation
    2.NSOperation相关概念
    3.NSInvocationOperation & NSBlockOperation
    4.NSOperation优势
    5.NSOperation基本使用
    6.NSOperation结合NSOperationQueue使用
    7.非主队列控制串行和并行执行的关键
    8.添加操作依赖和操作监听
    9.NSOperation线程间通信
    10.管理操作:是操作队列的方法

多线程相关概念篇


1.进程

进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

2.线程

基本概念:1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程),线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。
线程的串行:1个线程中的任务的执行是串行(按顺序地执行),如果要在1个线程中执行多个任务,那么只能一个一个按顺序执行这些任务(也就是说,在同一时间内,1个线程只能执行一个任务)

3.进程和线程的比较

1、进程是CPU分配资源和调度的单位
2、线程是CPU调用(执行任务)的最小单位
3、一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个线程
4、同一个进程内的线程共享进程的资源

多线程概念

多线程概念:即1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务
多线程并发执行:在同一时间里,CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象

多线程优缺点:
优点
1、能适当提高程序的执行效率
2、能适当提高资源利用率(CPU、内存利用率)
缺点
1、开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
2、线程越多,CPU在调度线程上的开销就越大
3、程序设计更加复杂:比如线程之间的通信、多线程的数据共享

主线程

主线程:程序运行后,默认会开启1条线程
作用:刷新显示UI,处理UI事件
使用注意:
1、不要将耗时操作放到主线程中去处理,会卡住线程,严重影响UI界面的流畅度,给用户界面卡顿的现象(放到子线程中执行);
2、和UI相关的刷新操作必须放到主线程中进行处理

多线程实现方案 特点 语言 频率 线程生命周期
pthread 1、一套通用的多线程API 2、适用于Unix\Linux\Windows等系统 3、跨平台\可移植 4、使用难度大 c语言 几乎不用 由程序员进行管理
NSThread 1、使用更加面向对象 2、简单易用,可直接操作线程对象 OC语言 偶尔使用 由程序员进行管理
GCD 1、旨在替代NSThread等线程技术 2、充分利用设备的多核(自动) C语言 经常使用 自动管理
NSOperation 1、基于GCD(底层是GCD) 2、比GCD多了一些更简单实用的功能 3、使用更加面向对象 OC语言 经常使用 自动管理

GCD相关概念

初学GCD的时候,肯定会纠结一些看似很关键但却毫无意义的问题(不要纠结,概念理解的基础+实战,就会解决你的疑惑),
对于GCD需要关注的只有两个概念:任务、队列

1.任务

linux内核中的任务的定义是描述进程的一种结构体,而GCD中的任务只是一个代码块,它可以指一个block或者函数指针。根据这个代码块添加进入队列的方式,将任务分为异步任务和同步任务:
异步任务
使用dispatch_async将任务加入队列。将异步任务加入并发队列,会开启多条线程且任务是并发执行(这也是我们最常用的一种方式);将异步任务加入串行队列,会开启一条线程且任务是串行执行(按顺序执行);将异步任务加入主队列,不会开启线程且任务都在主线程中执行。
同步任务
使用dispatch_sync将任务加入队列。将同步任务加入并发队列,不会开启线程且任务是串行执行;将同步任务加入串行队列,不会开启线程且任务是串行执行(也没什么意义是吧);将同步任务加入主队列,不会开启线程且任务都在主线程中执行(注意:方法在主线程调用会造成死锁,在子线程中调用不会造成死锁)。

2.队列

调度队列是一个对象,它会以first-in、first-out的方式管理您提交的任务。GCD有三种队列类型
并行队列
并发队列虽然是能同时执行多个任务,但这些任务仍然是按照先到先执行(FIFO)的顺序来执行的。并发队列会基于系统负载来合适地选择并发执行这些任务。并发队列一般指的就是全局队列(Global queue),进程中存在四个全局队列:高、中(默认)、低、后台四个优先级队列,可以调用dispatch_get_global_queue函数传入优先级来访问队列。当然我们也可以用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_CONCURRENT,来自己创建一个并发队列。

串行队列
串行队列将任务以先进先出(FIFO)的顺序来执行,所以串行队列经常用来做访问某些特定资源的同步处理。你可以也根据需要创建多个队列,而这些队列相对其他队列都是并发执行的。换句话说,如果你创建了4个串行队列,每一个队列在同一时间都只执行一个任务,对这四个任务来说,他们是相互独立且并发执行的。如果需要创建串行队列,一般用dispatch_queue_create这个方法来实现,并指定队列类型DISPATCH_QUEUE_SERIAL。

主队列
主队列,与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。

注:队列间的执行是并行的,但是也存在一些限制。比如,并行执行的队列数量受到内核数的限制,无法真正做到大量队列并行执行;比如,对于并行队列中的全局队列而言,其存在优先级关系,执行的时候也会遵循其优先顺序,而不是并行。

以上概念文言文你也许感到有点什么,下面总结简单小表格方便你查看

3.GCD总结小表格
GCD 特点
核心概念 任务:执行什么操作
队列:用来存放任务
函数 异步:可以在新的线程中执行任务,具备开启新线程的能力
同步:只能在当前线程中执行任务,不具备开启新线程的能力
队列 并发:允许多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步函数下才有效
串行:一个任务执行完毕后,再执行下一个任务(按顺序执行)
全局并发队列 特点:存在优先级关系(DEFAULT默认的、HIGH高的、LOW低的、BACKGROUND最低的)
主队列 特点:添加到主队列上的任务,必须在主线程执行。如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务,直到主线程空闲为止

对于 任务与队列 之间的关系,下面总结简单小表格方便你查看

任务 & 队列 并发队列(concurrent) 串行队列(serial) 主队列(get_main)
异步函数(async) 会开启多条线程,队列中的任务是并发执行 会开启一条线程,队列中的任务是串行执行 不会开启线程,所有任务都在主线程中执行
同步函数(sync) 不会开启线程,队列中的任务是串行执行 不会开启线程,队列中的任务是串行执行 不会开启线程,所有任务都在主线程中执行(注意:在主线程调用会造成死锁,在子线程中调用不会造成死锁)

pthread & NSThread篇


1.pthread

其实这个方案开发几乎不用,只是拿来充个数,为让大家了解一下就好了
简单介绍下,pthread是一套通用的多线程的API,可以在Unix / Linux / Windows 等系统跨平台使用,使用C语言编写,需要程序员自己管理线程的生命周期,使用难度较大,所以仅了解,看一遍有个印象。

pthread使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark - pthread创建子线程
- (void)pthread {
    //1.创建线程对象
    pthread_t thread;
    //2.创建线程
    //参数:线程对象(传递地址),线程的属性(NULL),指向函数的指针,函数需要接受的参数
    pthread_create(&thread, NULL, task, NULL);
}
void *task(void *param) {
    NSLog(@"%@",[NSThread currentThread]);
    return NULL;
}

打印输出:

1
2016-02-10 19:10:36.902 多线程2.4[9565:222926] <NSThread: 0x600000275540>{number = 3, name = (null)}

应用场景:我们在iOS开发中几乎不使用pthread

2.NSThread

这个方案是经过苹果封装后的,使用更加面向对象,简单易用可直接操作线程对象,但是,它的生命周期还是需要我们手动管理,所以这个方案也是偶尔用用,比如 [NSThread currentThread]用来获得当前线程类,你就可以知道当前线程的各种属性,用于调试十分方便。下面来看看它的一些用法

1.NSThread创建线程有3个方法

首先要包含头文件#import <pthread.h>

方法一:创建线程且手动启动

1
2
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(task:) object:nil];
[thread start];

方法二:分离子线程并自动启动

1
[NSThread detachNewThreadSelector:@selector(thread:) toTarget:self withObject:nil];

方法三:后台线程并自动启动

1
[self performSelectorInBackground:@selector(thread:) withObject:@"开启后台线程"];

2.NSThread其他方法

除了创建启动外,NSThread 还以很多方法,下面我列举一些常见的方法,当然我列举的并不完整,更多方法大家可以去类的定义里去看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 取消线程
- (void)cancel;
// 启动线程
- (void)start;
// 强制停止线程
+ (void)exit;
// 判断某个线程的状态的属性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
// 设置和获取线程名字
-(void)setName:(NSString *)n;
-(NSString *)name;
// 设置优先级(取值范围 0.0 ~ 1.0 之间 最高是1.0 默认优先级是0.5)
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
// 获取当前线程信息
+ (NSThread *)currentThread;
// 获取主线程信息
+ (NSThread *)mainThread;
// 判断是否为主线程(对象方法)
- (BOOL)isMainThread;
// 判断是否为主线程(类方法)
+ (BOOL)isMainThread;
// 阻塞线程(延迟执行)
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;

2.NSThread线程安全

线程安全隐患.png

线程安全隐患解决.png

线程安全,解决方法采用线程加锁,需了解互斥锁

互斥锁使用格式:
@synchronized (self) {// 需要锁定的代码 }
注意:锁定一份代码只用一把锁,用多把锁是无效的

互斥锁的优缺点:
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源

互斥锁注意点:
锁:必须是全局唯一的(通常用self)
1.注意加锁的位置
2.注意加锁的前提条件,多线程共享同一块资源
3.注意加锁是需要代价的,需要耗费性能的
4.加锁的结果:线程同步(按顺序执行)

补充:
我们知道, 属性中有atomic和nonatomic属性

atomic : setter方法线程安全, 需要消耗大量的资源

nonatomic : setter方法非线程安全, 适合内存小的移动设备

3.NSThread线程间通信

线程间通信:任务从子线程回到主线程

1
2
3
4
5
6
7
/**
 线程间通信(回到主线程刷新UI)
 参数:回到主线程要调用那个方法、前面方法需要传递的参数、是否等待(YES执行完再执行下面代码,NO可先执行下面代码)
*/
[self performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
[self performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];

4.NSThread线程状态转换

线程状态转换.png

GCD中枢调度器


GCD.png

1.什么是GCD

GCD全称 Grand Central Dispath,可译为”强大的中枢调度器”,基于libdispatch 纯C语言,里面包含了许多多线程相关非常强大的函数. 程序员可以既不写一句线程管理的代码又能很好地使用多线程执行任务。

2.GCD基本概念

初学GCD的时候,肯定会纠结一些看似很关键但却毫无意义的问题(不要纠结,概念理解的基础+实战,就会解决你的疑惑),
对于GCD需要关注的只有两个概念:任务 & 队列
而对于GCD相关的概念解读,如果你对这些都已有了解 可以忽略,如果还有疑虑可以参考一下 「iOS多线程—夯实基础「多线程基本概念」

在这里就提供 GCD主要概念简单总结小表格,方便你查看。

GCD 特点
核心概念 任务:执行什么操作
队列:用来存放任务
函数 异步:可以在新的线程中执行任务,具备开启新线程的能力
同步:只能在当前线程中执行任务,不具备开启新线程的能力
队列 并发:允许多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步函数下才有效
串行:一个任务执行完毕后,再执行下一个任务(按顺序执行)
全局并发队列 特点:存在优先级关系(DEFAULT默认的、HIGH高的、LOW低的、BACKGROUND最低的)
主队列 特点:添加到主队列上的任务,必须在主线程执行。如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务,直到主线程空闲为止

3.任务&队列组合使用

下面总结简单小表格方便你查看

任务 & 队列 并发队列(concurrent) 串行队列(serial) 主队列(get_main)
异步函数(async) 会开启多条线程,队列中的任务是并发执行 会开启一条线程,队列中的任务是串行执行 不会开启线程,所有任务都在主线程中执行
同步函数(sync) 不会开启线程,队列中的任务是串行执行 不会开启线程,队列中的任务是串行执行 不会开启线程,所有任务都在主线程中执行(注意:在主线程调用会造成死锁,在子线程中调用不会造成死锁)

4.GCD的优势

易用: GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱,而是因为基于block,它能极为简单得在不同代码作用域之间传递上下文。
灵活: GCD 具有在常见模式(比如互斥锁、单例模式)上的特点,且会自动管理线程的生命周期创建线程、调度任务、销毁线程),用更高性能的方法优化代码,而且GCD(C API)能提供更多的控制权力以及大量的底层函数。
性能: GCD 会自动利用更多的CPU内核(比如双核、四核),且自动根据系统负载来增减线程数量,这就减少了线程间切换以及增加了计算效率。

怎么样? 心动不, 迫不及待想要知道怎么使用GCD了吧, 那我们正式投入GCD的怀抱了~
我会通过代码展示。

5.GCD基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
1.创建队列
queue1:全局并行队列(默认优先级,可为0)、queue2:主队列、queue3:未指定type则为串行队列、queue4:指定串行队列、queue5:指定并发队列
*/
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue2 = dispatch_get_main_queue();
dispatch_queue_t queue3 = dispatch_queue_create("queue3", NULL);
dispatch_queue_t queue4 = dispatch_queue_create("queue4", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue5 = dispatch_queue_create("queue5", DISPATCH_QUEUE_CONCURRENT);
// 2.封装异步任务添加到队列
dispatch_async(queue1, ^{
// 任务
});
// 封装同步任务添加到队列
dispatch_sync(queue1, ^{
// 任务
});

6.GCD常见用法和应用场景

1.dispatch_async 异步函数

使用方法:(线程间通信)

1
2
3
4
5
6
7
8
// 1.创建子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 执行任务
...
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程刷新UI
...
});});

应用场景:
这种用法非常常见,比如开启一个异步的网络请求,待数据返回后返回主队列刷新UI;又比如请求图片,待图片返回刷新UI或是耗时文件操作等等。

2.dispatch_after 延迟执行

使用方法:(多个方法,好对比)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法一:调用NSObject的方法
//[self performSelector:@selector(task) withObject:nil afterDelay:2.0];
// 方法二:使用NSTimer
//[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
// 方法三:使用GCD(优点:可以控制任务在那个线程执行)
//dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
/*
第一个参数:DISPATCH_TIME_NOW 从现在开始计算时间
第二个参数:延迟的时间 2.0 GCD时间单位:纳秒
第三个参数:队列
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
NSLog(@"GCD----%@",[NSThread currentThread]);
});

应用场景:
这为我们提供了一个简单的延迟执行的方式,比如在view加载结束延迟执行一个动画等等

3.dispatch_time 延迟时间

使用方法:

1
2
3
4
5
6
7
8
9
// dispatch_time_t一般在dispatch_after和dispatch_group_wait等方法里作为参数使用。这里最需要注意的是一些宏的含义。
// NSEC_PER_SEC,每秒有多少纳秒。
// USEC_PER_SEC,每秒有多少毫秒。
// NSEC_PER_USEC,每毫秒有多少纳秒。
// DISPATCH_TIME_NOW 从现在开始
// DISPATCH_TIME_FOREVE 永久
// time为2s的写法
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);

4.dispatch_once_t 一次性代码

使用方法:(保证某段代码在程序运行过程中只被执行1次)

1
2
3
4
5
6
// onceToken用来记录该部分的代码是否被执行过
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
NSLog(@"---once----");
});

应用场景:
可以使用其创建一个单例,也可以做一些其他只执行一次的代码,注意:看到一次性代码你可能会想到懒加载,提醒
dispatch_once_t 不能放在懒加载中的

5.dispatch_barrier_async 栅栏函数

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// dispatch_barrier_async 作用可一个词概括一一承上启下
- (void)barrier {
// 注意:栅栏函数(不能使用全局并发队列)
//dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"download1---%@",[NSThread currentThread]);
});
[NSThread sleepForTimeInterval:5];// 睡眠5s
dispatch_async(queue, ^{
NSLog(@"download2---%@",[NSThread currentThread]);
});
[NSThread sleepForTimeInterval:5];
// 栅栏函数
dispatch_barrier_async(queue, ^{
NSLog(@"++++++++++++++++++++++");
});
dispatch_async(queue, ^{
NSLog(@"download3---%@",[NSThread currentThread]);
});
}

栅栏函数使用全局并发队列,打印输出

1
2
3
4
2016-02-11 14:45:43.228 多线程2.4[17420:450903] download1---<NSThread: 0x60800007dd00>{number = 3, name = (null)}
2016-02-11 14:45:43.228 多线程2.4[17420:450906] ++++++++++++++++++++++
2016-02-11 14:45:43.228 多线程2.4[17420:450943] download3---<NSThread: 0x600000264440>{number = 5, name = (null)}
2016-02-11 14:45:43.228 多线程2.4[17420:450904] download2---<NSThread: 0x600000260b40>{number = 4, name = (null)}

栅栏函数使用手动创建并发队列,打印输出

1
2
3
4
2016-02-11 14:51:27.502 多线程2.4[17537:454708] download2---<NSThread: 0x60000007e6c0>{number = 4, name = (null)}
2016-02-11 14:51:27.502 多线程2.4[17537:454709] download1---<NSThread: 0x60800007f5c0>{number = 3, name = (null)}
2016-02-11 14:51:27.502 多线程2.4[17537:454709] ++++++++++++++++++++++
2016-02-11 14:51:27.503 多线程2.4[17537:454709] download3---<NSThread: 0x60800007f5c0>{number = 3, name = (null)}

应用场景:
和dispatch_group类似,dispatch_barrier也是异步任务间的一种同步方式,可以在比如文件的读写操作时使用,保证读操作的准确性。注意:dispatch_barrier_async只在自己创建的并发队列上有效。

6.dispatch_apply 快速迭代

使用方法:

1
2
3
4
5
6
7
8
9
10
11
// 注意:会开子线程和主线程一起完成遍历任务,任务的执行是并发的
- (void)apply {
/**
iterations 遍历的次数
queue 队列(只能是并发队列,传主队列会造成死锁,传串行队列无效果)
index 索引
*/
dispatch_apply(4, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"download--%zd--%@",index,[NSThread currentThread]);
});
}

打印输出:

1
2
3
4
2016-02-11 15:19:53.490 多线程2.4[18411:477679] download--3--<NSThread: 0x608000073940>{number = 5, name = (null)}
2016-02-11 15:19:53.490 多线程2.4[18411:477641] download--0--<NSThread: 0x608000066c00>{number = 1, name = main}
2016-02-11 15:19:53.490 多线程2.4[18411:477682] download--1--<NSThread: 0x600000076f00>{number = 3, name = (null)}
2016-02-11 15:19:53.490 多线程2.4[18411:477680] download--2--<NSThread: 0x600000077100>{number = 4, name = (null)}

应用场景:
dispatch_apply并行的运行机制,效率一般快于for循环的类串行机制(在for一次循环中的处理任务很多时差距比较大)。比如这可以用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性,如果用for循环,耗时较多,并且每个表单的数据没有依赖关系,所以用dispatch_apply比较好。

7.dispatch_group_t 队列组

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 队列组:会监听任务的执行情况,通知group
- (void)group1 {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
[NSThread sleepForTimeInterval:2];
dispatch_group_async(group, queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
[NSThread sleepForTimeInterval:2];
dispatch_group_async(group, queue, ^{
NSLog(@"3-----%@", [NSThread currentThread]);
});
[NSThread sleepForTimeInterval:2];
// 方法一:组通知
// 拦截通知,当队列组中所有的任务都执行完毕的时候回进入到这个方法
//dispatch_group_notify(group, queue, ^{
// NSLog(@"----group_notify---");
//});
// 方法二:组等待
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"----end----");
}

group_notify 打印输出:

1
2
3
4
5
2016-02-11 15:40:30.083 多线程2.4[19029:495158] 1-----<NSThread: 0x608000073ac0>{number = 3, name = (null)}
2016-02-11 15:40:32.084 多线程2.4[19029:495158] 2-----<NSThread: 0x608000073ac0>{number = 3, name = (null)}
2016-02-11 15:40:34.084 多线程2.4[19029:495158] 3-----<NSThread: 0x608000073ac0>{number = 3, name = (null)}
2016-02-11 15:40:36.085 多线程2.4[19029:494872] ----end----
2016-02-11 15:40:36.085 多线程2.4[19029:495158] ----group_notify---

group_wait 打印输出:

1
2
3
4
2016-02-11 15:38:30.374 多线程2.4[18909:492794] 1-----<NSThread: 0x608000266900>{number = 3, name = (null)}
2016-02-11 15:38:35.374 多线程2.4[18909:492794] 2-----<NSThread: 0x608000266900>{number = 3, name = (null)}
2016-02-11 15:38:40.376 多线程2.4[18909:492794] 3-----<NSThread: 0x608000266900>{number = 3, name = (null)}
2016-02-11 15:38:45.377 多线程2.4[18909:492734] ----end----

注意:
group_notify 拦截通知,当队列组中所有的任务都执行完毕的时候回进入到这个方法,问题? 该方法是阻塞的吗? –> 内部本身是异步的。
group_wait 等待.死等. 直到队列组中所有的任务都执行完毕之后才能执行,(DISPATCH_TIME_NOW : 现在,DISPATCH_TIME_FOREVER : 永远) 作用:阻塞。

8.dispatch_sync 死锁

怎样会造成死锁 & 如何避开死锁
使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 直接调用,主线程调用 会造成死锁
//[self syncMain];
// 正确调用,子线程中调用 不会造成死锁
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
}
- (void)syncMain {
// 1.获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 2.同步函数
dispatch_sync(queue, ^{
NSLog(@"download1---%@",[NSThread currentThread]);
});

打印输出:

1
2
3
4
1.在主线程调用 会造成死锁
2.在子线程中调用 不会造成死锁
2016-02-11 16:29:59.410 多线程2.4[19986:525068] download1---<NSThread: 0x60800007a640>{number = 1, name = main}

下面这种情况,也会造成死锁

1
2
3
4
5
6
7
8
9
10
11
12
// 死锁
- (void)syncMain2 {
// 因为dispatch_apply会卡住当前线程,内部的dispatch_apply会等待外部,外部的等待内部,所以死锁。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_apply(4, queue, ^(size_t index) {
NSLog(@"download1--%zd--%@",index,[NSThread currentThread]);
NSLog(@"+++++++++++++++");
dispatch_apply(4, queue, ^(size_t index) {
NSLog(@"download2--%zd--%@",index,[NSThread currentThread]);
});
});
}

9.dispatch_suspend&dispatch_resume 挂起队列和恢复队列

使用方法:

1
2
3
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_suspend(queue); // 挂起(暂停)队列
dispatch_resume(queue); // 恢复队列

应用场景:
有时候,我们不想让队列中的某些任务马上执行,这时我们可以通过挂起操作来阻止一个队列中将要执行的任务。
注意:执行挂起操作不会对已经开始执行的任务起作用,它仅仅只会阻止将要进行但是还未开始的任务。

7.内存和安全

内存
MRC:用dispatch_retaindispatch_release管理dispatch_object_t内存。
ARC:ARC在编译时刻自动管理dispatch_object_t内存,使用retainrelease会报错。

安全
dispatch_queue是线程安全的,你可以随意往里面添加任务。

补充
1.注意ARC不是垃圾回收机制,是编译器特性
配置MRC环境:build setting ->搜索automatic ref->修改为NO

2.在MRC环境下,如果用户retain了一次,那么直接返回instance变量,不对引用计数器+1
如果用户release了一次,那么什么都不做,因为单例模式在整个程序运行过程中都拥有且只有一份,程序退出之后被释放,所以不需要对引用计数器操作

8.单例模式

单例也就是在程序的整个生命周期中, 该类有且仅有一个实例对象, 此时为了保证只有一个实例对象, 我们这里用到了dispatch_once函数

在这里我就整理好吧,就不直接粘上代码了,可能会很多地方用到,到时会很麻烦。下面整理了单例模式通用的宏,如果你需要可以直接拷走,是吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 定义带参数的宏
// MRC单例模式 & ARC单例模式
#define SingleH(name) +(instancetype)share##name;
#if __has_feature(objc_arc)
//条件满足 ARC
#define SingleM(name) static id _instance;\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
+(instancetype)share##name\
{\
return [[self alloc]init];\
}\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}
#else
//MRC
#define SingleM(name) static id _instance;\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
+(instancetype)share##name\
{\
return [[self alloc]init];\
}\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(oneway void)release{}\
-(instancetype)retain\
{\
return _instance;\
}\
-(NSUInteger)retainCount\
{\
return MAXFLOAT;\
}
#endif

9.总结

GCD可进行线程间通信
GCD可以办到线程安全
GCD可用于延迟执行
GCD需要注意死锁问题(不要在当前队列调用同步函数)

NSOperation操作队列篇


1.什么是NSOperation

NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD 的封装,完全面向对象,但是比GCD更简单易用、代码可读性也更高,使用也更好理解。使用起来也和GCD差不多,其中 NSOperation相当于GCD中的任务,而NSOperationQueue则相当于GCD中的队列。
注意:
NSOperation需要配合NSOperationQueue来实现多线程(1.将要操作任务封装到一个NSOperation对象中,2.将此任务添加到一个NSOperationQueue对象中,然后系统就会自动在执行任务)。因为默认情况下,NSOperation单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue才能实现异步执行。

2.NSOperation相关概念

  1. 并行(Concurrent) & 串行(Serial)
    并行和串行描述的是任务和任务之间的执行方式,并行是任务A和任务B可以同时执行,串行是任务A执行完了任务B才能执行(按顺序执行)。
  2. 异步(Asynchronous) & 同步(Synchronous)
    异步和同步描述的其实就是函数什么时候返回. 比如用来下载图片的函数: 同步函数只有在image下载结束之后才返回, 下载的这段时间函数只能等待,而异步函数,不会去等它完成(异步函数不会堵塞当前线程去执行下一个函数)。
  3. 并发(Concurrency) & 并行(Parallelism)
    这个更容易混淆了, 并发和并行都是用来让不同的任务可以”同时执行”。 只是并行是真同时,而并发是假同时(是CPU地在各个进程之间快速切换, 给人一种能同时处理多任务的错觉)。

3.NSInvocationOperation& NSBlockOperation

NSOperation是一个抽象类,它不能直接使用,所以你必须使用NSOperation子类,系统已经给我们封装了NSBlockOperationNSInvocationOperation这两个实体类,不过我们更多的使用是自己继承并定制自己的操作。

1.NSInvocationOperation:使用这个类来初始化一个操作,它包括指定对象的调用selector

2.NSBlockOperation:使用这个类来用一个或多个block初始化操作,操作本身可以包含多个块。当所有block被执行操作将被视为完成。

4.NSOperation优势

  1. NSOperation是基于GCD的封装, 拥有更多的API(suspended, cancelled).
  2. NSOperationQueue中, 可以指定各个NSOperation之间的依赖关系(注意:不能相互依赖).
  3. 用KVO可以方便的监测NSOperation的状态(isExecuted, isFinished, isCancelled).
  4. 更高的可定制能力, 你可以继承NSOperation实现可复用的逻辑模块.

5.NSOperation基本使用

在不使用NSOperationQueue,需要调用 start 方法来启动任务,不开启新线程,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。

1.NSInvocationOperation

1
2
3
4
5
6
7
8
9
10
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
[op1 start];// 启动
```
2.NSBlockOperation
```objc
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
[op1 start];

这里,NSBlockOperation 还有一个方法:addExecutionBlock:(追加任务),通过这个方法可以给 NSBlockOperation 添加多个执行 Block。额外操作中的任务 会开子线程并发执行任务(这里可能是子线程也可能是主线程执行) 注意下面的打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1.创建操作,封装任务
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3----%@",[NSThread currentThread]);
}];
// 2.追加任务
[op3 addExecutionBlock:^{
NSLog(@"4----%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"5----%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"6----%@",[NSThread currentThread]);
}];
// 3.启动
[op1 start];
[op2 start];
[op3 start];

打印输出:

1
2
3
4
5
6
2016-02-13 16:13:15.690 多线程2.4[6890:173737] 1----<NSThread: 0x60000006be40>{number = 1, name = main}
2016-02-13 16:13:15.691 多线程2.4[6890:173737] 2----<NSThread: 0x60000006be40>{number = 1, name = main}
2016-02-13 16:13:15.692 多线程2.4[6890:173737] 3----<NSThread: 0x60000006be40>{number = 1, name = main}
2016-02-13 16:13:15.692 多线程2.4[6890:174061] 4----<NSThread: 0x600000079d00>{number = 3, name = (null)}
2016-02-13 16:13:15.692 多线程2.4[6890:173737] 6----<NSThread: 0x60000006be40>{number = 1, name = main}
2016-02-13 16:13:15.692 多线程2.4[6890:174072] 5----<NSThread: 0x600000079080>{number = 4, name = (null)}

3.自定义子类继承NSOperation,实现内部相应的方法(重写main)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// LNOperation.m
#import "LNOperation.h"
@implementation LNOperation
- (void)main {
NSLog(@"自定义NSOperation----%@",[NSThread currentThread]);
}
// **********************我是分割线***************************
//
// 1.封装操作
LNOperation *op1 = [[LNOperation alloc] init];
LNOperation *op2 = [[LNOperation alloc] init];
// 2.创建队列
[op1 start];
[op2 start];

打印输出:

1
2
2016-02-13 16:18:28.687 多线程2.4[7015:177012] 自定义NSOperation----<NSThread: 0x60800007f780>{number = 1, name = main}
2016-02-13 16:18:28.688 多线程2.4[7015:177012] 自定义NSOperation----<NSThread: 0x60800007f780>{number = 1, name = main}

6.NSOperation结合NSOperationQueue使用

NSOperationQueue 按类型来说共有两种类型:主队列、其他队列。只要添加到队列,会自动调用任务的 start 方法。
1.主队列
凡是添加到主队列中的任务(NSOperation),都会放到主线程中 串行执行。
NSOperationQueue *queue1 = [NSOperationQueue mainQueue];
2.其他队列(非主队列)
添加到这种队列中的任务(NSOperation),就会自动放到子线程中 并发执行,同时具有:串行、并发功能。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

结合使用:
1.添加任务到队列中 addOperation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)addOperationQueue {
// 1.创建队列
//NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装操作
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
NSLog(@"op2--%@", [NSThread currentThread]);
}
}];
// 3.添加操作到队列
[queue addOperation:op1];
[queue addOperation:op2];
}
- (void)task {
for (int i = 0; i < 2; i++) {
NSLog(@"op1--%@", [NSThread currentThread]);
}
}

使用非主队列打印输出:

1
2
3
4
2016-02-13 16:54:53.312 多线程2.4[7731:198138] op1--<NSThread: 0x60800026f400>{number = 3, name = (null)}
2016-02-13 16:54:53.312 多线程2.4[7731:198134] op2--<NSThread: 0x6000002662c0>{number = 4, name = (null)}
2016-02-13 16:54:53.313 多线程2.4[7731:198138] op1--<NSThread: 0x60800026f400>{number = 3, name = (null)}
2016-02-13 16:54:53.313 多线程2.4[7731:198134] op2--<NSThread: 0x6000002662c0>{number = 4, name = (null)}

使用主队列打印输出:

1
2
3
4
2016-02-13 17:34:30.241 多线程2.4[8595:228001] op1--<NSThread: 0x600000072340>{number = 1, name = main}
2016-02-13 17:34:30.241 多线程2.4[8595:228001] op1--<NSThread: 0x600000072340>{number = 1, name = main}
2016-02-13 17:34:30.242 多线程2.4[8595:228001] op2--<NSThread: 0x600000072340>{number = 1, name = main}
2016-02-13 17:34:30.242 多线程2.4[8595:228001] op2--<NSThread: 0x600000072340>{number = 1, name = main}

2.添加任务到队列中 addOperationWithBlock:,简易方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加任务到队列中
[queue addOperationWithBlock:^{
for (NSInteger i= 0; i <2; i++) {
NSLog(@"op1--%@", [NSThread currentThread]);
}
}];
[queue addOperationWithBlock:^{
for (NSInteger i= 0; i <2; i++) {
NSLog(@"op2--%@", [NSThread currentThread]);
}
}];

打印输出:

1
2
3
4
2016-02-13 17:28:12.586 多线程2.4[8446:222668] op1--<NSThread: 0x600000262740>{number = 3, name = (null)}
2016-02-13 17:28:12.586 多线程2.4[8446:222667] op2--<NSThread: 0x608000263d40>{number = 4, name = (null)}
2016-02-13 17:28:12.587 多线程2.4[8446:222668] op1--<NSThread: 0x600000262740>{number = 3, name = (null)}
2016-02-13 17:28:12.587 多线程2.4[8446:222667] op2--<NSThread: 0x608000263d40>{number = 4, name = (null)}

7.非主队列控制串行和并行执行的关键

NSOperationQueue创建的其他队列 同时具有串行、并发功能,上边我们演示了并发功能,那么下面讲讲串行功能,这里有个关键参数:maxConcurrentOperationCount 队列最大并发数(同一时间最多几个任务可以执行)
误区:串行执行任务不等于只开一条线程(线程同步,要看任务的执行方式是顺序还是并发的)

maxConcurrentOperationCount 描述
.> 1 并发队列
= 1 串行队列
= 0 不会执行任务
= -1 特殊意义,最大值表示不受限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.设置队列最大并发数(同一时间最多几个任务可以执行)
queue.maxConcurrentOperationCount = 1;// 串行
// 3.封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---- %@",[NSThread currentThread]);
}];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---- %@",[NSThread currentThread]);
}];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3---- %@",[NSThread currentThread]);
}];
// 4.添加到队列
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];

最大并发数 =1,打印输出:

1
2
3
2016-02-13 17:59:24.702 多线程2.4[9096:244592] 1---- <NSThread: 0x600000071e80>{number = 3, name = (null)}
2016-02-13 17:59:24.703 多线程2.4[9096:244593] 2---- <NSThread: 0x608000077080>{number = 4, name = (null)}
2016-02-13 17:59:24.703 多线程2.4[9096:244592] 3---- <NSThread: 0x600000071e80>{number = 3, name = (null)}

最大并发数 =2,打印输出:

1
2
3
2016-02-13 18:03:17.608 多线程2.4[9178:246935] 2---- <NSThread: 0x600000267d80>{number = 4, name = (null)}
2016-02-13 18:03:17.608 多线程2.4[9178:246936] 1---- <NSThread: 0x600000262280>{number = 3, name = (null)}
2016-02-13 18:03:17.612 多线程2.4[9178:246936] 3---- <NSThread: 0x600000262280>{number = 3, name = (null)}

8.添加操作依赖和操作监听

NSOperation 有一个非常实用的功能,那就是 添加依赖addDependency:(也可以跨队列依赖),注意:这里不能相互依赖。
只有所有依赖的对象都已经完成操作,当前NSOperation对象才会开始执行操作。需要先添加依赖关系,再将操作添加到队列中。另外,通过removeDependency方法来删除依赖对象。
给操作任务 添加监听addExecutionBlock:,当任务完成后就会,走到这个Block块里面,
具体怎么添加依赖和监听如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)addDependency {
// 1.创建队列
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
// 2.封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---- %@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---- %@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3---- %@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4---- %@",[NSThread currentThread]);
}];
// 3.添加操作依赖,也可以跨队列依赖(注意:这里不能相互依赖)
[op1 addDependency:op4];
[op4 addDependency:op3];
// 4.添加操作监听
[op2 addExecutionBlock:^{
NSLog(@"监听op2--%@",[NSThread currentThread]);
}];
// 5.添加到队列
[queue1 addOperation:op1];
[queue1 addOperation:op2];
[queue1 addOperation:op3];
[queue2 addOperation:op4];
}

打印输出:

1
2
3
4
5
2016-02-13 19:04:12.166 多线程2.4[10372:280459] 2---- <NSThread: 0x60800007c440>{number = 3, name = (null)}
2016-02-13 19:04:12.166 多线程2.4[10372:280473] 监听op2-- <NSThread: 0x60800007cbc0>{number = 5, name = (null)}
2016-02-13 19:04:12.166 多线程2.4[10372:280462] 3---- <NSThread: 0x60000007aa40>{number = 4, name = (null)}
2016-02-13 19:04:12.168 多线程2.4[10372:280459] 4---- <NSThread: 0x60800007c440>{number = 3, name = (null)}
2016-02-13 19:04:12.169 多线程2.4[10372:280473] 1---- <NSThread: 0x60800007cbc0>{number = 5, name = (null)}

9.NSOperation线程间通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.封装任务
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://s15.sinaimg.cn/bmiddle/4c0b78455061c1b7f1d0e"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
NSLog(@"UI-- %@",[NSThread currentThread]);
}];
}];
// 3.添加操作到队列
[queue addOperation:op1];

打印输出:

1
2016-02-13 19:13:29.759 多线程2.4[10553:285311] UI-- <NSThread: 0x60800006d2c0>{number = 1, name = main}


10.管理操作:是操作队列的方法

队列中的任务也是有状态:已经执行完成的、正在执行、等待执行

应用场景:提高用户体验第一,当用户操作时,取消一切跟用户当前操作无关的进程,提升流畅度。(开始滚动的时候 暂停操作、滑动结束的时候 恢复操作、接收到内存警告 取消所有操作
1.添加操作依赖
2.管理操作:重点!是操作队列的方法

  • 暂停/恢复 取消操作,(暂停和取消,不能暂停或取消正在执行状态的任务,且取消不可以恢复)
  • 开启合适的线程数量!(最多不超过6条)
  • 一般开发的时候,会将操作队列设置成一个全局的变量(属性)

方法:

1
2
3
4
5
6
// 判断暂停状态,YES暂停 NO恢复
@property (getter=isSuspended) BOOL suspended;
// 取消(不可以恢复)
// 该方法内部调用了所有操作的cancel方法
- (void)cancelAllOperations;

好了,就到这里吧。当然,我讲的并不完整,可能有一些知识我并没有讲到,但作为常用方法,这些已经足够了。不过我在这里只是告诉你了一些方法的功能,只是怎么把他们用到合适的地方,就需要多多实践了,看我写的这么卖力,不打赏的话得点个喜欢也是极好的。

期待


  • 如果在阅读过程中遇到 error || new ideas,希望你能 messages 我,我会及时改正谢谢。

  • 点击右上角的 喜欢 和 订阅Rss 按钮,可以收藏本仓库,并在 Demo 更新时收到邮件通知。

❄︎ 本文结束    感谢简阅 ^_^. ❄︎

本文标题:iOS 模块详解—「多线程面试、工作」看我就 🐒 了 ^_^.

文章作者:寄己的路

原始链接:https://sunyonghui.github.io/iOSNET/Multithreading.html

版权声明: 署名-非商业性使用-禁止演绎 4.0 国际 本博客所有文章除特别声明外均为原创,转载务必请「注明出处链接(可点击) - 作者」,并通过E-mail等方式告知,谢谢合作!