美文网首页iOS进阶
iOS底层原理探索—RunLoop的应用

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

作者: 劳模007_Mars | 来源:发表于2019-08-06 23:56 被阅读2次

    探索底层原理,积累从点滴做起。大家好,我是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

    • 解决NSTimer失效问题
    • 控制线程生命周期(线程保活)
    • 自动释放池
    • 监控应用卡顿
    • 性能优化

    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

    相关文章

      网友评论

        本文标题:iOS底层原理探索—RunLoop的应用

        本文链接:https://www.haomeiwen.com/subject/nhxidctx.html