主线程是如何切换runloop的探究

我们都知道,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模式下运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值