一、GCD基础应用
1.1 dispatch_async()
- 往全局队列中异步添加一个任务。一般在block中完成耗时操作、网络请求、数据处理、图片处理、文本处理等。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
})
- 往主队列中异步添加一个任务。耗时操作处理完成了,回到主线程去刷新UI。
dispatch_async(dispatch_get_main_queue(), ^{
})
1.2 dispatch_after()
往主队列中添加一个延时操作。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
1.3 dispatch_once()
创建一个单例,load方法中进行方法交互等全局只会完成一次的操作。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
二、GCD进阶应用
2.1 自定义队列:dispatch_queue_t
除了主队列dispatch_get_main_queue()
和全局队列dispatch_get_global_queue(0, 0)
之外,GCD还提供API以供我们自行创建并管理队列。
- 串行队列中的主队列
dispatch_get_main_queue()
资源也是非常紧张,用来处理UI事件都不够,显然我们不能占用主队列来执行可能耗时的串行操作; - 并行队列中的全局队列
dispatch_get_global_queue(0, 0)
,确实是用的最多的。但就是因为用得太多了,我们往往不知道里面有多少操作在执行,是否有耗时的同步任务在执行。
串行队列和并行队列
- 串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.xy.serial.cn", DISPATCH_QUEUE_SERIAL);
- 并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.xy.concurrent.cn", DISPATCH_QUEUE_CONCURRENT);
- 异步添加任务
串行队列:任务仍然是同步执行
dispatch_async(serialQueue, ^{
});
并行队列:任务可以异步执行,实现真正的并发操作
dispatch_async(concurrentQueue, ^{
})
- 同步添加任务
串行队列:这里往往会造成死锁,循环等待导致的死锁
dispatch_sync(serialQueue, ^{
});
并行队列:任务可以仍然是同步执行
dispatch_sync(concurrentQueue, ^{
})
自定义队列dispatch_queue_t
比较适合单一的场景。在AFNetworking中就有这么一个队列:
static dispatch_queue_t url_session_manager_creation_queue() {
static dispatch_queue_t af_url_session_manager_creation_queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_url_session_manager_creation_queue = dispatch_queue_create("com.alamofire.networking.session.manager.creation", DISPATCH_QUEUE_SERIAL);
});
return af_url_session_manager_creation_queue;
}
而我们的业务场景往往会比较复杂,各种优先级、先后顺序,甚至任务之间还会有依赖关系,这个时候我们可以用NSOperationQueue
来管理操作,这个类是面向对象的队列,底层也是用GCD封装的,但一些上层接口可以非常方便的管理任务。
2.2 信号量:dispatch_semaphore_t
信号量的API
信号量的API非常简单,只有3个,但功能异常强大,而且效率非常高
dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量
dispatch_semaphore_create(long value)
接受一个long类型的参数,表示当前信号量大小,创建指定大小的信号量。如果是在异步线程中,这个大小也可以理解为同时能执行的任务数量
;
dispatch_semaphore_signal(dispatch_semaphore_t deem)
发送信号量。该函数会对信号量+1;
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待信号量。该函数会对信号量进行减1操作。如果减1后信号量小于0(即减1前信号量值为0),那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。
使用信号量,记住下面的操作:
- dispatch_semaphore_signal,信号量+1
- dispatch_semaphore_wait,信号量-1
- 信号量小于0时,等待...
信号量实现异步线程同步操作
为什么异步线程还需要同步操作?一切都是为了业务。可能会有5个异步任务,但需要控制其中的某三个任务按照顺序执行。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1:%@",[NSThread currentThread]);
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2:%@",[NSThread currentThread]);
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务3:%@",[NSThread currentThread]);
});
}
在这个场景中,可以保证按照“任务1-任务2-任务3”的顺序执行
信号量+调度组
这种场景时针对上面的一种扩展,真是的业务场景中,往往是当异步任务执行完成之后,就会回到主线程刷新UI。我们可以用信号量控制异步任务按顺序完成,并回到主线程:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
dispatch_group_t grp = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(grp, queue, ^{
NSLog(@"task1 begin : %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"task1 finish : %@",[NSThread currentThread]);
});
});
dispatch_group_async(grp, queue, ^{
NSLog(@"task2 begin : %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"task2 finish : %@",[NSThread currentThread]);
});
});
dispatch_group_notify(grp, dispatch_get_main_queue(), ^{
NSLog(@"refresh UI");
});
}
信号量实现线程安全
最常见的场景,就是循环中,异步往可变数组中添加元素。
可变数组在添加元素的时候,是需要动态去分配内存的。而在异步场景中,极有可能出现同时有多个线程往同一个内存中写入数据,这个时候计算机就会被玩坏了。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:100];
// 创建为1的信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
[arrayM addObject:[NSNumber numberWithInt:i]];
});
}
如何改进呢?看看万能的信号量是怎么工作的
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:100];
// 创建为1的信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
// 等待信号量
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
[arrayM addObject:[NSNumber numberWithInt:i]];
NSLog(@"%@",[NSNumber numberWithInt:i]);
// 发送信号量
dispatch_semaphore_signal(sem);
});
}
- 信号量初始值为0;
- 当第一个线程执行到dispatch_semaphore_wait后,信号量变为了-1;
- 当后面的线程执行到dispatch_semaphore_wait这行时,发现信号量为-1,只能乖乖地等待;
- 当第一个线程执行完add,并dispatch_semaphore_signal发送信号量之后,这个时候信号量变为了0;
- 当前面等待的线程发现信号量变为了0,可以执行后续代码了;
- 然后就回到第二步,一直循环
2345
的步骤;
在上述场景中,可以保证永远都只有一个线程在访问数组,所以是线程安全的。回顾在API介绍那里说的,1表示同时能通过的线程只有一个。如果你把它改为2,你会发现添加10000数的时间会比1快了接近一倍(只在测试中玩,前提是没有遇到闪退)。
YYImage中的信号量
在YYImage中,定义了这么一个变量,并且信号量的初始值设为1
,这个很关键。
dispatch_semaphore_t _preloadedLock;
_preloadedLock = dispatch_semaphore_create(1);
再看看代码调用的地方,简直就跟我们上面循环添加数组的操作一模一样。
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= _decoder.frameCount) return nil;
dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
UIImage *image = _preloadedFrames[index];
dispatch_semaphore_signal(_preloadedLock);
if (image) return image == (id)[NSNull null] ? nil : image;
return [_decoder frameAtIndex:index decodeForDisplay:YES].image;
}
关于信号量的介绍就到这里了,记住它最原本的用法,你会发现它可以做很多事,比如,实现一个读写锁,那还不是信手沾来。保护可变数组安全,除了用信号量,也完全可以用其他类型的锁,但除了锁之外,还可以用GCD提供的栅栏函数dispatch_barrier_async()
,也是一大利器,后面会介绍。
2.3 调度组:dispatch_group_t
前面的例子中其实已经看到了调度组的应用。调度组最大的作用就是在调度组中的异步任务执行完成之后,会发出一个通知,调度组可以在接受通知的地方指定要执行的队列。
dispatch_group_create()
创建一个调度组,不需要任何参数,是不是很喜欢这样的函数
dispatch_group_t group = dispatch_group_create();
dispatch_group_async()
这个函数可以往指定的调度组中添加带有任务的队列
- 第一个参数:指定的调度组;
- 第二个参数:执行任务的队列,可以是任何队列;
- 第三个参数:添加任务的block;
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 任务1
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 任务2
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 任务3
});
dispatch_group_enter()和dispatch_group_leave()
铁律:dispatch_group_enter()和dispatch_group_leave()一定要成对出现
GCD提供这种方式的调用,可以让我们更好的控制调度组中异步任务的完成时机。假如任务1也是一个异步任务
,可以不使用信号量的情况下,在任务1中调用dispatch_group_leave(group);
,这样就能保证异步任务1执行完成之后,才会表示这个任务的完成。
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//任务1
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//任务2
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//任务3
dispatch_group_leave(group);
});
dispatch_group_notify()
在调度组中的任务都执行完成之后,系统会触发dispatch_group_notify
里面的任务,我们可以将这个任务放到指定的队列上执行。
- 第一个参数:指定的调度组;
- 第二个参数:执行任务的队列,可以是任何队列;
- 第三个参数:添加任务的block;
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//刷新UI
});
调度组也是一种使用简单,功能强大的设计。
2.4 源:dispatch_source_t
dispatch_source_t
用的最多的就是自定义一个高效定时器。为什么高效呢?因为它是跟runloop同级别的,不需要依赖runloop实现计时。而NSTimer
、CADisplayLink
都依赖runloop,而且在实现上存在强持有的关系链:runloop->timer(link)->self
,所以在使用完定时器之后,都要手动的去invalidate
,并释放定时器,否则会导致self无法被释放。
除了用作定时器,dispatch_source_t
还有其他几种类型,有兴趣的可以自己去探索下更深层次的用法,dispatch_source_type_t
的定义如下:
* DISPATCH_SOURCE_TYPE_DATA_ADD: application defined data
* DISPATCH_SOURCE_TYPE_DATA_OR: application defined data
* DISPATCH_SOURCE_TYPE_DATA_REPLACE: application defined data
* DISPATCH_SOURCE_TYPE_MACH_SEND: dispatch_source_mach_send_flags_t
* DISPATCH_SOURCE_TYPE_MACH_RECV: dispatch_source_mach_recv_flags_t
* DISPATCH_SOURCE_TYPE_MEMORYPRESSURE dispatch_source_memorypressure_flags_t
* DISPATCH_SOURCE_TYPE_PROC: dispatch_source_proc_flags_t
* DISPATCH_SOURCE_TYPE_READ: estimated bytes available to read
* DISPATCH_SOURCE_TYPE_SIGNAL: number of signals delivered since
* the last handler invocation
* DISPATCH_SOURCE_TYPE_TIMER: number of times the timer has fired
* since the last handler invocation
* DISPATCH_SOURCE_TYPE_VNODE: dispatch_source_vnode_flags_t
* DISPATCH_SOURCE_TYPE_WRITE: estimated buffer space available
NSTimer
- 创建方法
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];
-
TimerInterval:
执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法 -
target
: 需要执行方法的对象。 -
selector
: 需要执行的方法 -
repeats
: 是否需要循环
- 释放方法
[timer invalidate];
timer = nil;
注意 :
在ARC机制下,调用创建方法后,target对象的计数器为1,直到执行完毕,自动减1。如果是循环执行的话,就必须手动关闭,否则不执行释放方法。
- 特性
存在延迟
不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop
和RunLoop Mode
有关,如果此RunLoop
正在执行一个连续性的运算,timer就会被延时触发。
重复性的timer
遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行
,并按照之前指定的周期继续执行。
必须加入Runloop
使用上面的创建方式,会自动把timer
加入MainRunloop
的NSDefaultRunLoopMode
中。如果使用以下方式创建定时器,就必须手动加入Runloop
:
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
CADisplayLink
- 创建方法
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- 释放方法
[self.displayLink invalidate];
self.displayLink = nil;
当把CADisplayLink
对象add到runloop
中后,selector就能被周期性调用
,类似于重复的NSTimer被启动了;
执行invalidate
操作时,CADisplayLink对象就会从runloop中移除
,selector调用也随即停止,类似于NSTimer的invalidate
方法。
- 特性
屏幕刷新时调用
CADisplayLink
可以让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器。CADisplayLink
以特殊模式注册到runloop
后,每当屏幕显示内容刷新结束的时候,runloop
就会像CADisplayLink
指定的target
发送一次指定的target
消息,CADisplayLink
类对应的selector
就会被调用一次。通常情况下,按照iOS设备屏幕的刷新帧率60次/秒
。
延迟
iOS设备刷新帧率是固定的,CADisplayLink
在正常情况下,每次屏幕刷新结束后都会被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕的刷新周期,就会导致跳过若干次回调调用机会。
如果CPU过于繁忙,无法保证60次/秒
的刷新帧率,也会导致跳过如干次回调调用就会,跳过次数取决于CPU的忙碌程度。
- 使用场景
从CADisplayLink
的原理上看,CADisplayLink
适合做界面的不停重绘,比如视频播放时需要不停地获取下一帧用于界面渲染。
- 重要属性
-
frameInterval
:"preferredFramesPerSecond", ios(3.1, 10.0), watchos(2.0, 3.0), tvos(9.0, 10.0))
,在iOS 10.0之后的系统废弃
NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。如果设置为2,则每隔一帧调用一次。 -
preferredFramesPerSecond
:API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0))
,iOS 10.0之后的系统可用
NSInteger类型的值,用来设置一秒调用多少次。默认值是0,表示以系统60次/秒
的速率回调。如果设置为1,则表示1秒只回调一次。 -
duration
:readOnly的CFTimeInterval值
表示两次两次屏幕刷新的之间的时间间隔。该属性在target
的selector
首次被调用之后才会被赋值。selector
的调用时间间隔为:duration x frameInterval
2.5 栅栏函数:dispatch_barrier_async()
- 创建方法
//需要将dispatch_source_t timer设置为成员变量,不然会立即释放
@property (nonatomic, strong) dispatch_source_t timer;
//定时器开始执行的延时时间
NSTimeInterval delayTime = 3.0f;
//定时器间隔时间
NSTimeInterval timeInterval = 3.0f;
//创建子线程队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//使用之前创建的队列来创建计时器
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置延时执行时间,delayTime为要延时的秒数
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC));
//设置计时器
dispatch_source_set_timer(_timer, startDelayTime, timeInterval * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
//执行事件
});
// 启动计时器
dispatch_resume(_timer);
- 停止方法
dispatch_source_cancel(_timer);
- 特性
由于DISPATCH_SOURCE_TYPE_TIMER默认是重复执行的,我们可以在handle里面调用cancel方法,以完成只执行一次的效果:
dispatch_source_set_event_handler(_timer, ^{
//执行事件
dispatch_source_cancel(_timer);
});
- 重点属性
- start
计时器起始时间,可以通过dispatch_time
创建,如果使用DISPATCH_TIME_NOW
,则创建后立即执行 - interval
计时器间隔时间,可以通过timeInterval * NSEC_PER_SEC
来设置,其中,
timeInterval
为对应的秒数 - leeway
这个参数的理解,我觉得http://www.dreamingwish.com上Seven's
同学的解释很直观也很易懂:“这个参数告诉系统我们需要计时器触发的精准程度。所有的计时器都不会保证100%精准,这个参数用来告诉系统你希望系统保证精准的努力程度。如果你希望一个计时器没五秒触发一次,并且越准越好,那么你传递0为参数。另外,如果是一个周期性任务,比如检查email,那么你会希望每十分钟检查一次,但是不用那么精准。所以你可以传入60,告诉系统60秒的误差是可接受的。这样有什么意义呢?简单来说,就是降低资源消耗。如果系统可以让cpu休息足够长的时间
,并在每次醒来的时候执行一个任务集合,而不是不断的醒来睡去以执行任务,那么系统会更高效。如果传入一个比较大的leeway给你的计时器,意味着你允许系统拖延你的计时器来将计时器任务与其他任务联合起来一起执行。
- 优点:
- 时间准确
- 可以使用子线程,解决定时间跑在主线程上卡UI问题
- 注意事项:
- 需要将dispatch_source_t timer设置为成员变量,不然会立即释放
- 定时器的状态问题,在自定义定时器的时候,一定要注意:
- 启动状态(resume之后):再次调用
dispatch_source_set_timer
、dispatch_resume
会出现异常; - 停止状态(cancel之后):再次调用
dispatch_source_cancel
也会出现异常。
- 启动状态(resume之后):再次调用
自定义XYTimer:Github传送门
可以结合
dispatch_suspend
和dispatch_resume
实现定时器的暂停和恢复,但暂停和恢复必须成对使用;为了避免多线程同时操作定时器的问题,你需要在自定义定时器里面使用锁保护定时器的安全;