上一次仓促面试,给了一份面试题让我去做,题目的主要内容就是多线程的相关知识。其中有一题是这样的:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"这是1");
[self performSelector:@selector(test) withObject:nil afterDelay:0];
NSLog(@"这是3");
});
}
-(void)test{
NSLog(@"这是2");
}
让我写出程序的打印顺序。我写了1,3,2,结果不用说是错了,应该是1,3。然后回来就立马去查询相关资料,才知道错误原因。
API上面说的很清楚,该方法会在当前线程的runloop中添加定时器,但是我们使用的是异步执行+全局并发,就会开启子线程执行block中的任务,这个要是不知道,那就去补一下GCD相关知识。
performSelector:withObject:afterDelay: 的底层
- 不在 NSObject 中,而是在NSRunLoop 类中
- 带有 afterDelay 的方法,都是在 NSRunLoop 类中定义的
为何在 主线程就可以调用 test 方法,在GCD 中却不能调用 test 方法?
- 这是因为 performSelector:withObject:afterDelay: 类的底层调用了 NSTimer 定时器。
- 定时器是要添加到 runloop 中去的。
- 这句话的底层就是 往 runloop 中添加了一个定时器。
- 主线程 默认有 runloop,所以 在主线程 可以调用 test方法。
- 而子线程中没有 runloop ,所以 不会调用 test 方法。如果想要在 GCD 中调用 test 方法,需要自己开启 runloop 。
那么问题来了,怎么让它执行呢,有几个方法,下面一一介绍:
方法一:
[self performSelector:@selector(test) withObject:nil afterDelay:0];
既然我们是afterDelay是0秒之后,那么我们就稍微修改一下,用跟它很相近的方法:
[self performSelector:@selector(test) withObject:nil];
该方法跟上面方法最大的区别就是不再使用定时器,而是直接执行,这样就不存在Runloop启动不启动的问题了。修改后如下:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"这是1");
[self performSelector:@selector(test) withObject:nil];
NSLog(@"这是3");
});
}
-(void)test{
NSLog(@"这是2");
}
执行结果是:
2019-06-25 18:32:20.066710+0800 GCDTest[19808:1137360] 这是1
2019-06-25 18:32:20.066870+0800 GCDTest[19808:1137360] 这是2
2019-06-25 18:32:20.066973+0800 GCDTest[19808:1137360] 这是3
是不是很方便。
方法二
既然上面说到子线程中Runloop没有启动,那就给它一个Runloop让它启动。具体实现是:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"这是1");
[self performSelector:@selector(test) withObject:nil afterDelay:0];
NSLog(@"这是3");
//子线程开启Runloop,注意该方法要写在performSelector方法之后才有效
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}
-(void)test{
//子线程关闭Runloop
CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
NSLog(@"这是2");
}
打印结果是:
2019-06-26 11:28:31.507646+0800 GCDTest[6848:233750] 这是1
2019-06-26 11:28:31.507919+0800 GCDTest[6848:233750] 这是3
2019-06-26 11:28:31.508048+0800 GCDTest[6848:233750] 这是2
如果我们此时在test方法中再执行一个gcd方法,就不会执行,如:
-(void)test{
//子线程关闭Runloop
CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
NSLog(@"这是2");
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test2) withObject:nil afterDelay:1];
});
}
-(void)test2{
NSLog(@"不会执行");
}
打印结果还是:
2019-06-26 11:29:26.430061+0800 GCDTest[6864:237962] 这是1
2019-06-26 11:29:26.430325+0800 GCDTest[6864:237962] 这是3
2019-06-26 11:29:26.430482+0800 GCDTest[6864:237962] 这是2
这是因为子线程中的runloop已经被关闭了。
方法三
既然我们说主线程的runloop是默认开启的,子线程的没有开启,那么我们就把该方法放到主队列中执行就可以了。代码如下:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"这是1");
[self performSelector:@selector(test) withObject:nil afterDelay:0];
NSLog(@"这是3");
});
}
-(void)test{
NSLog(@"这是2");
}
-(void)test2{
NSLog(@"不会执行");
}
执行结果如下:
2019-06-26 11:33:29.554860+0800 GCDTest[6896:247259] 这是1
2019-06-26 11:33:29.555022+0800 GCDTest[6896:247259] 这是3
2019-06-26 11:33:29.555248+0800 GCDTest[6896:247259] 这是2
是不是完美解决了这个问题。
好了,觉得对你有帮助的话记得动手点个赞呦,关注我,会给你带来更多关于iOS底层的相关知识。