美文网首页
iOS-Runloop原理与应用

iOS-Runloop原理与应用

作者: CDLOG | 来源:发表于2021-03-10 16:53 被阅读0次

    Runloop:运行循环-死循环

    主要目的:提高性能,有事情就干,没事情休眠。
    参考https://blog.csdn.net/callauxiliary/article/details/107419854

    主要应用

    1,保证线程一直运行,处理事件,比如触摸事件,时钟事件,都是由runloop完成。
    2,优化卡顿:将一次runloop执行完的任务,放到多次runloop中执行。
    3,UI滑动时计时不准确的问题,设置定时器的Mode为:NSRunLoopCommonModes。
    4,需要在线程上使用performSelector*****方法(运行时方法)。例如

    让UITableView、UICollectionView等延迟加载图片
    
    [imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
    
    这算一个优化点,这里用的defaultMode,就是滚动的时候不加载图片,停止滚动加载图片
    

    Runloop运行原理

    当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。
    Runloop的生命周期:在第一次获取时创建,在线程结束时销毁。

    Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
    系统默认注册了5个mode

        a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    
            b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    
            c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    
            d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    
            e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
    

    RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。

    主要分为三大步骤:

    1、首先根据modeName找到对应的Mode

    2、如果model里没有Source/Timer/Observer,直接返回

    3、如果model有Source/Timer/Observer,就会即将进入runloop


    image.png

    详细步骤

    简单来讲就是一个do-while循环,有输入源就唤醒,没有就处于休眠状态,官方解释链接。具体过程如下:

    1. 通知观察者开始进入 Runloop
    2. 通知观察者开始处理 Timer 事件
    3. 通知观察者将要处理非基于 port 的事件
    4. 启动准备好的事件
    5. 如果基于 port 的事件已经准备好,立即启动。并进入步骤 9
    6. 通知观察者进入休眠状态
    7. 线程进入休眠直到以下事件出现
      • 基于 port 的事件源出现
      • 定时器启动
      • 设置的时间已经超时
      • RunLoop 被唤醒
    8. 通知观察者线程将被唤醒
    9. 处理已经进入的事件
      • 如果用户自定义的计时器启动,处理事件并重启 Runloop。转到第二步
      • 输入源启动,传递消息
      • 如果 Runloop 被显示唤醒且没有超时,重启 Runloop。转到第二步
    10. 通知观察者 Runloop 已经退出

    Runloop组成

    两种对象:NSRunLoop 和 CFRunLoopRef。

    CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

    RunLoop共包含5个类,但公开的只有Source、Timer、Observer相关的三个类。
    1,Timder,时钟
    2,source,事件源:一切事件的来源,按照函数的调用栈分为source0:非系统内核事件,source1:系统内核事件。
    3,observer,观察者,观察的runloop的循环周期。

    苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

    APP启动过程

    1,当点击APP时,操作系统开启一条线程执行程序的main函数。这个线程就是这个程序的主线程。这个线程是一个常驻线程,因为这个线程的Runloop被开启了,不会被线程池释放。

    线程和runloop 的关系

    线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)

    Runloop的作用

    1,保证线程不退出。
    2,监听所有的事件,比如触摸事件,时钟事件,网络事件等。
    子线程要监听事件,就必须开启子线程的runloop。

    验证将时钟添加到runloop才可以实现监听

    1,scheduledTimerWithTimeInterval初始化方法默认是添加到当前线程的runloop的,所以不用显示的添加。
    这种方法添加是NSDefaultRunLoopMode模式,UI事件会中断响应。

    -(void)test{
    
        [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        
    }
    
    -(void)timeerFunC{
        static int num;
        NSLog(@"%d",num);
        num++;
    }
    

    3,子线程结束runloop

    -(void)test{
        //要放在子线程,设置runloop时间,时间过了没有执行的任务,子线程就会结束,并释放
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
            //将时钟手动添加到当前的runloop,否者时钟方法不会执行
            [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
            //当为yes时,取消RunLoop循环
            while (!_finish) {
                NSLog(@"来了11");
                [[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:1]];
            }
            NSLog(@"来了");
        });
    }
    
    -(void)timeerFunC{
        static int num;
        NSLog(@"%d",num);
        num++;
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"22222");
        _finish = YES;
    }
    

    2,timerWithTimeInterval初始化方法需要手动将时钟添加到当前线程的runloop中,否则时钟不能被监听。

    -(void)test{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //将时钟手动添加到当前的runloop,否者时钟方法不会执行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    
    -(void)timeerFunC{
        static int num;
        NSLog(@"%d",num);
        num++;
    }
    
    

    runloop的三种Mode

    1,当对象使用NSDefaultRunLoopMode(默认模式,一般处理网络事件和Timer事件),在runloop执行UI事件的时候,会暂停响应该对象的事件。
    2,当对象使用UITrackingRunLoopMode(UI模式,一般处理UI事件),只有在runloop执行UI事件的时候,才会会暂停响应该对象的事件。
    3,当对象使用NSRunLoopCommonModes(占位模式,这个不是一个真的模式,只是同时添加了上面两种模式),在runloop执行UI事件的时候,也会响应该对象的事件(最理想的结果)。

    开启当前线程的runloop

    子线程处理需要监听的事件(需要子线程活着一直监听的情况),可以启动子线程的runloop,让子线程成为常驻线程,避免被释放,从而可以响应监听的事件。唯一让线程不被释放的方法就去启动他的runloop。
    解决NSTimer在滑动时停止工作的问题的办法
    1,设置定时器的Mode为:NSRunLoopCommonModes
    2,将NSTimder放到子线程并且开启runloop,name就是算是NSDefaultRunLoopMode,滑动UI也不会影响Timer。
    NSTimder处理的事件一般放在子线程,并开启子线程的runloop,避免影响主线程处理事件。

    -(void)test{
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
            //将时钟手动添加到当前的runloop,否者时钟方法不会执行
            [[NSRunLoop currentRunLoop]addTimer:timer forMode: NSDefaultRunLoopMode ];
            //手动开启当前线程的runloop,这是一个死循环
            [[NSRunLoop currentRunLoop]run];
            NSLog(@"这里不会走了");
        });
    }
    
    -(void)timeerFunC{
        static int num;
        NSLog(@"%d",num);
        num++;
    }
    

    GCD实现定时器

    Dispatch Source创建定时器timer,优于NSTimer
    必须要声明timer属性强应用,不然会被释放

    @property (nonatomic, strong) dispatch_source_t timer;
    
    -(void)test2{
        NSLog(@"启动");
        _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
        dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
        dispatch_source_set_event_handler(_timer, ^{
            NSLog(@"间隔一秒执行%@",[NSThread currentThread]);
        });
        dispatch_resume(_timer);
        NSLog(@"启动");
    }
    

    优化练习

    tableview在快速滑动时,高清图片造成卡顿优化。
    原因:tableview在快速滑动时,runloop必须在一次循环内,渲染所有的图片。
    思路:runloop每次循环只加载一张图片,例如一个屏幕总的可以显示15张图片,那么就分15次加入到runloop中加载。相当于把一次做完的事情,分成了15次。例如监听kCFRunLoopBeforeWaiting事件,回调函数加载图片,加载完成后删除这个任务。每次加载图片后都会再次到达kCFRunLoopBeforeWaiting,还有图片任务就会继续执行。
    通过CFRunLoopObserverRef监听runloop 的状态,函数指针回调,Ref是引用,指针的意思。
    步骤
    1,添加runloop观察者,观察runloop 的状态变化。
    2,将加载图片的代码块放到数组中。
    3,在观察者回调中,拿出数组中加载图片的代码执行。

    相关文章

      网友评论

          本文标题:iOS-Runloop原理与应用

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