我们都知道,iOS的tableView能做到滑动很平滑,一部分是依赖于runloop的mode的切换。当系统检测到有scrollerview滑动时,系统就会将当前进程的主线程切换到UITrackingRunLoopMode,直到滑动结束,又会切换到NSDefaultRunLoopMode。这个过程听起来很奇妙,那么他是怎么做到的呢,我们能不能在需要的时候也这么做呢?
答案是肯定的,我们可以模拟这个过程,我的思路是这样的:由于主线程不是一个runloop干净的线程,所以我们另外启动一个子线程,
//使用GCD启动一个子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
});
为了保证线程不退出,我们添加一个timer, 让其在下NSDefaultRunLoopMode模式下运行,按正常情况下,下面的代码会在一直打印“timer1 fired”,该子线程会永远运行在NSDefaultRunLoopMode模式下。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//timer1运行在default mode
NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
但是主线程是可以切换mode的,那么这样肯定不行,我们只有改变runloop的启动方式,只有用更好控制的runMode: beforeDate:这个函数,大家知道这个函数运行起来的runloop,会在指定的mode下运行一次便会退出,但是由于我们添加的是timer,所有不会主动退出,但是我们可以采用CFRunLoopStop()主动退出,只有runloop能退出,我们才又机会切换mode,下面是实现代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//timer1运行在default mode
NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
self.currentMode = kCFRunLoopDefaultMode;
while (1) {
[[NSRunLoop currentRunLoop] runMode:self.currentMode beforeDate:[NSDate distantFuture]];
}
});
这样写就可以通过控制currentMode这个全局变量来控制runloop下次运行的mode。我们模拟在touchbegan的时候切换到UITrackingRunLoopMode,touchend的时候又切换回NSDefaultRunLoopMode,为了显示runloop确实切换到了UITrackingRunLoopMode,我们启动另一个timer让其运行在UITrackingRunLoopMode,看打印日志就可以了。代码实现是这样的:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.rl = CFRunLoopGetCurrent();
//timer1运行在default mode
NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
//timer2运行在track Mode
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer2 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
//指定当前运行mode
self.currentMode = NSDefaultRunLoopMode;
while (1) {
[[NSRunLoop currentRunLoop] runMode:self.currentMode beforeDate:[NSDate distantFuture]];
}
});
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch began")
//touchbegan 切换成track mode
self.currentMode = UITrackingRunLoopMode;
CFRunLoopStop(self.rl);
}
- (void) touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch end");
//touchend 切换成kCFRunLoopDefaultMode
self.currentMode = kCFRunLoopDefaultMode;
CFRunLoopStop(self.rl);
}
代码运行起来后,就可以看到,在触摸之前只有timer1打印,在手指触摸时之后timer2打印
2020-06-05 14:48:39.932264+0800 [18468:8621222] timer1 fired
2020-06-05 14:48:40.932321+0800 [18468:8621222] timer1 fired
2020-06-05 14:48:41.934566+0800 [18468:8621222] timer1 fired
2020-06-05 14:48:42.934567+0800 [18468:8621222] timer1 fired
2020-06-05 14:48:43.344774+0800 [18468:8620979] touch began
2020-06-05 14:48:43.345429+0800 [18468:8621222] timer2 fired
2020-06-05 14:48:43.934576+0800 [18468:8621222] timer2 fired
2020-06-05 14:48:44.929843+0800 [18468:8621222] timer2 fired
2020-06-05 14:48:45.294261+0800 [18468:8620979] touch end
2020-06-05 14:48:45.294846+0800 [18468:8621222] timer1 fired
通过这样我们就模拟了系统的mode切换过程,也顺便知道了,为什么NSTimer需要设置在NSRunLoopCommonModes模式下运行。