iOS进阶

iOS底层原理探索—RunLoop的应用

2019-08-06  本文已影响2人  劳模007_Mars

探索底层原理,积累从点滴做起。大家好,我是Mars。

往期回顾

iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
iOS底层原理探索— block的本质(一)
iOS底层原理探索— block的本质(二)
iOS底层原理探索— Runtime之isa的本质
iOS底层原理探索— Runtime之class的本质
iOS底层原理探索— Runtime之消息机制
iOS底层原理探索—RunLoop的本质

我们在iOS底层原理探索—RunLoop的本质一文中带领大家从底层源码入手,分析了RunLoop的底层结构和运行逻辑,今天继续研究关于RunLoop的应用。

RunLoop的应用

基于RunLoop的特点,在以下场景中会用到RunLoop

1、解决NSTimer失效问题

当使用定时器NSTimer做计时或者指定时间执行某件事的时候,如果我们滑动UIScrollViewUITableView等可以滚动的控件时,定时器NSTimer就会暂停,当停止滑动以后,定时器NSTimer又会重新恢复计时。

这是由于我们创建定时器NSTimer,并把定时器添加到RunLoop中,如果没有给定时器设置运行模式Mode的话,系统会选择默认运行模式kCFRunLoopDefaultMode
当发生滑动操作时,RunLoopMode会自动切换成UITrackingRunLoopMode模式,定时器NSTimer仍然是kCFRunLoopDefaultMode的模式。由于RunLoop同一时间只能运行在某一种模式,因此定时器就会失效。当停止滑动后,RunLoop又会切换回kCFRunLoopDefaultMode模式,因此定时器又会重新启动。

所以要想解决定时器失效问题,就要保证定时器的ModeRunLoopMode同步。当RunLoopMode切换成UITrackingRunLoopMode模式时,定时器的Mode也要做出改变。这时我们就可以用NSRunLoopCommonModes来完成。

NSRunLoopCommonModes

NSRunLoopCommonModes是占位模式,并不是一种真正的模式。当设置了NSRunLoopCommonModes后,就会被标记成CommonModes。在RunLoop底层结构中,存在一个集合CFMutableSetRef,里面存放这一些通用的Mode,包括UITrackingRunLoopModekCFRunLoopDefaultMode

CFRunLoop结构.png

NSRunLoopCommonModes标记后,就可以在UITrackingRunLoopModekCFRunLoopDefaultMode这两种模式下运行了。
下面是具体代码:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    //设置定时器的Mode,将定时器添加到主线程RunLoop中
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
-(void)show
{
    NSLog(@"-------");
}

2、控制线程生命周期(线程保活)

我们来看下面这道面试题:

[NSObject performSelector: withObject: afterDelay:];这个方法在子线程中执行会有什么问题?

答案是不会执行,这个方法会在当前线程的RunLoop中添加定时器,但是由于子线程的RunLoop没有启动,所以定时器也不会计时,方法也不会执行
我们用代码验证一下:

测试代码.png

我们在子线程中执行performSelector: withObject: afterDelay:方法,通过打印可以看到,run方法已经执行完了,test方法都没有执行。

那么如何解决这个问题呢?既然子线程没有开启RunLoop,那么我们可以通过手动开启子线程的RunLoop,就可以执行test方法了。

线程保活后的测试代码.png
代码如上,我们通过在子线程中开启RunLoop,当点击屏幕时,test方法执行并完成了打印。通过打印线程,可以看出任务的执行的确在子线程。

但是有一个问题,我们在子线程的run方法中有打印end的输出,但是任务都执行完了,都没有打印end,这是为什么呢?

问题就出在[[NSRunLoop currentRunLoop] run];这句代码。首选我们看一下官方是如何解释的:

run方法官方解释.png
通过官方解,我们得知,RunLooprun方法是运行在NSDefaultRunLoopMode模式下,会重复调用runMode:beforeDate:方法,并且是无限循环。
这就说明,NSRunLooprun方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop),所以不会打印end

那么既然调用系统的run方法有瑕疵,我们就自己实现一下:

#import "ViewController.h"
#import "MThread.h"

@interface ViewController ()
@property (strong, nonatomic) MThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        //控制器非空,并且没有停止子线程的RunLoop
        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----", [NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (!self.thread) return;
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
//点击停止按钮,停止子线程的RunLoop
- (IBAction)stop {
    if (!self.thread) return;
    
    // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 清空线程
    self.thread = nil;
}
//控制器销毁的时候也要停止子线程的RunLoop

- (void)dealloc
{
        [self stop];
}
@end

3、 自动释放池

TimerSource也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
那么什么时候释放,怎么释放呢?
RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,TimerSource等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。

关于RunLoop的探索今天就告一段落,如有疑问欢迎留言交流。

更多技术知识请关注公众号
iOS进阶


iOS进阶.jpg
上一篇下一篇

猜你喜欢

热点阅读