我们在上一篇笔记中讲异步函数和同步函数,以及各种队列时,学习过不少GCD函数,今天再补充两个GCD中常用的函数:dispatch_after( )函数和dispatch_once( )函数。其中,dispatch_after( )函数常用于延时执行任务,而dispatch_once( )是一次性执行代码的函数,常用于单例设计模式中。
一、GCD中延时执行任务的函数
首先来看GCD中用于延时执行任务的函数。不过,在此之前,先来回忆一下OC中常用的两个延时执行任务的方法,以便和GCD中的延时执行任务函数做一个对比。
1、OC中常用的延时执行任务的方法
先来看第一个方法- performSelector: withObject: afterDelay:,它是声明在NSObject的分类当中的,可以进入其头文件看一下:
这就意味着,只要是任意一个继承自NSObject的类,都可以调用它。因此,它的使用场合比较灵活:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 输出字符串
NSLog(@"%s", __func__);
// 演示执行
[self delay];
}
// MARK:- 延时执行任务
- (void)delay {
// 延时执行任务方式
[self performSelector:@selector(printString:) withObject:@"我是参数" afterDelay:2.0];
/**
* performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>
* 第一个参数 : 需要传入一个方法,在这个方法中执行任务;
* 第二个参数 : 方法选择器中那个方法的参数,可以为空;
* 第三个参数 : NSTimeInterval其实就是double,它表示需要延时多长时间再执行前面方法选择器中的那个方法。
*/
}
// MARK:- 输出一段字符串
- (void)printString:(NSString *)str {
NSLog(@"%@---%@", str, [NSThread currentThread]);
}
这个方法经常使用,也非常简单。运行程序,然后点击屏幕,注意一下控制台打印信息的时间戳,看看是不是隔了两秒以后才执行打印任务的:
从上面的GIF图上看出,它的确可以办到,在我们设定两秒的延时以后才执行任务的。同时,要留意一下,它这个任务是在主线程中执行的。
除了上面这个- performSelector: withObject: afterDelay:方法以外,NSTimer中还有一个类方法可以控制延时执行任务:+ scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:。它的参数稍微多一点,但是也相对较容易:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 输出字符串
NSLog(@"%s", __func__);
// 延时执行任务
[self delay];
}
// MARK:- 延时执行任务
- (void)delay {
// 延时执行任务方式
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(printString:) userInfo:nil repeats:NO];
/**
* scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
* 第一个参数 : 表示延时多少秒执行;
* 第二个参数 : 表示目标对象,通常情况下就是self;
* 第三个参数 : 方法选择器,用于封装任务;
* 第四个参数 : 和NSTimer相关的信息,可以传空;
* 第五个参数 : 传YES表示持续重复调用方法选择器中的方法,传NO则只调用一次。
*/
}
// MARK:- 输出一段字符串
- (void)printString:(NSString *)str {
NSLog(@"%@---%@", str, [NSThread currentThread]);
}
关于上面这个方法,前三个参数都好理解,后面两个参数需要单独说一下。userInfo里面一般保存的是NSTimer相关的信息,通常可以传空;repeats后面需要传一个BOOL值,如果出入YES,就表示每间隔多少秒以后重复执行某个任务,而传NO则表示只需执行一次。运行程序看一下效果:
从控制台打印信息的时间戳来看,它也能办到两秒之后执行我们设定的任务。而且,这个任务也是在主线程当中去执行的。
2、GCD中延时执行任务的函数
看完了上面这两个OC中常用的延时执行任务的方法,在来看一下GCD中常用的延时执行任务的函数。一般情况下,敲一个dispatch_after,然后选择前面有代码块标识符的那一个函数就可以了,它的很多参数系统已经帮你填写好了:
选择这个代码块以后,你会发现,我们只需要填写需要延时的时长,以及需要延时执行的代码块就可以了:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 输出字符串
NSLog(@"%s", __func__);
// 延时执行任务
[self delay];
}
// MARK:- 延时执行任务
- (void)delay {
// GCD中延时执行任务的方法
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 延时两秒以后要执行的任务
NSLog(@"%@", [NSThread currentThread]);
});
/**
* dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
<#code to be executed after a specified delay#>
});
* 在这个GCD代码块中,参数delayInSeconds比较直白,就是表示延时多少秒;
* 后面的block代码块是用来封装任务的。
*/
}
运行程序,然后点击屏幕,看一下控制台打印信息的时间戳,它是不是在我们设定两秒的延时之后执行任务的:
关于上面这个dispatch_after( )函数,它有两个参数。其中,第一个参数是一个dispatch_time( )函数,第二个参数是我们非常熟悉的获取主队列的函数。并且,在dispatch_time( )这个函数中,它的第一个参数系统已经帮我们填写好了,DISPATCH_TIME_NOW表示从现在开始计算延时时间;它的第二个参数,前面一部分需要我们自己填,后面一部分NSEC_PER_SEC是一个常数,表示10的9次方纳秒,其作用就是将纳秒转换为秒(GCD的时间是精确到纳秒的)。
如果仅仅只是这样,那么就看不出GCD的这个延时执行任务的函数跟前面那两个方法有什么区别了。其实,GCD的这个延时执行任务的函数非常强大,它可以自由控制延时任务是在主线程中执行还是子线程中执行。比如说,下面,我们就把dispatch_after( )函数的第二个参数由主队列修改为全局并发队列,让延时任务在子线程中去执行:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 输出字符串
NSLog(@"%s", __func__);
// 延时执行任务
[self delay];
}
// MARK:- 延时执行任务
- (void)delay {
// GCD中延时执行任务的方法
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 延时两秒以后要执行的任务
NSLog(@"%@", [NSThread currentThread]);
});
}
运行程序,然后点击屏幕,注意看一下控制台打印信息,看看这个延时任务是不是真的是在子线程中去执行的:
二、GCD中一次性执行代码的函数
GCD中除了上面那个延时执行任务的函数之外,还有一个一次性执行代码的函数也很好用,尤其是在单例设计模式中用得最多。下面就简单的介绍一下这个函数。
1、GCD中的一次性代码函数
在使用GCD的一次性执行代码函数时,和上面一样,只需敲一个dispatch_once,然后选择前面有代码块标识符的那一行就可以了:
选择完毕以后,代码块的其它参数,系统已经帮你填写好了,你只需要将只需要执行一次的代码块封装进去就可以了:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 输出字符串
NSLog(@"%s", __func__);
// 一次性代码
[self singleUseCode];
}
// MARK:- 一次性代码:整个应用程序运行过程中只会执行一次
- (void)singleUseCode {
// GCD中一次性执行的代码块
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 输出一段消息
NSLog(@"%s, Line = %d", __FUNCTION__, __LINE__);
});
/**
*static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
<#code to be executed once#>
});
* onceToken是使用static修饰的,是一个全局变量,它在整个应用运行过程中只有一份内存,直到整个应用
* 都被释放时,onceToken才会被释放。因此,它可以保证这段代码在整个应用程序运行过程中只会被执行一次;
* 特别注意:一次性代码是不能放在懒加载中去执行的,否则,运行会出问题;
* 一次性代码通常用于单例设计模式中。
*/
}
运行程序,然后多次点击模拟器屏幕,看看我们封装进去的代码是不是只执行一次:
从上面的GIF图上看,尽管我们多次点击模拟器屏幕,但是,整个过程中,- singleUseCode方法只被执行了一次。GCD中的一次性执行代码函数,可以保证整个应用在运行期间,它里面的代码只会被执行一次。
2、GCD中一次性代码函数与懒加载的区别
了解完GCD中一次性执行代码的函数,我们可能会想到一iOS开发中的懒加载技术。那么,它和懒加载有什么却别呢?下面我们就演示一下它们之间到底有什么区别。
新建一个继承自NSObject的ESPerson类,在它里面声明一个dogs属性,然后再通过懒加载对这个属性进行懒加载:
@interface ESPerson : NSObject
@property (strong, nonatomic) NSArray *dogs;
@end
@implementation ESPerson
- (NSArray *)dogs {
if (!_dogs) {
_dogs = @[@"Wangcai"];
}
return _dogs;
}
@end
回到ViewController中,包含ESPerson的头文件,然后在- viewDidLoad方法中创建两个ESPerson对象,打印这两个对象的dogs属性的值:
- (void)viewDidLoad {
[super viewDidLoad];
// 创建ESPerson对象
ESPerson *p1 = [[ESPerson alloc] init];
ESPerson *p2 = [[ESPerson alloc] init];
NSLog(@"p1 = %@, p2 = %@", p1.dogs, p2.dogs);
}
运行程序,注意观察控制台打印出来的消息:
如果将GCD中一次性代码执行的函数放在懒加载中,结果会怎样呢?修改dogs属性的懒加载代码:
@implementation ESPerson
- (NSArray *)dogs {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_dogs = @[@"Wangcai"];
});
return _dogs;
}
@end
再次运行程序,然后看一下控制台打印出来的消息:
你会发现,p2的值竟然是空的!为什么会出现这种情况呢?首先,我们说过,GCD中一次性执行代码的函数,它在整个应用运行期间只会执行一次。当我们打印p1的值时,会调用dogs属性的getter方法,执行dispatch_once ( )函数给_dogs赋值;当我们打印p2的值时,会再次调用dogs属性的getter方法执行dispatch_once ( )函数给_dogs赋值,但是,dispatch_once ( )函数此时已经不可能再执行了,因此,也就不可能通过它再给_dogs赋值了,所以,最后的结果一定为空。
通过上面的实例可以说明,一次性代码函数和懒加载还是有区别的。一次性代码是在整个应用程序运行过程中只会被执行一次;而懒加载是在发现没有值的时候再执行,有值的时候不执行。这也说明,一次性代码函数是不能放在懒加载中的。
dispatch_after( )函数和dispatch_once( )函数的介绍到这里暂时先告一段落,后面会接着介绍GCD的其它知识。详细代码参见《GCDFunctions》。