美文网首页
RunLoop原理总结-iOS

RunLoop原理总结-iOS

作者: 桔子橙子柚子_F | 来源:发表于2020-05-26 17:40 被阅读0次

    1、简介

    运行循环,保持程序的持续运行,main函数中就默认启动了一个RunLoop,如果没有RunLoop程序会立即退出,有了RunLoop就可以实现在有操作的时候去执行,没有操作的时候停下来休息,节省CPU资源,提高性能。
    官方文档对RunLoop的讲解

    2、RunLoop的创建

    NSRunLoop是基于CFRunLoopRef的封装,CFRunLoopRef是在CoreFoundation框架内的,是开源的。
    __CFRunLoop结构体:

    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;  //对应的线程
        uint32_t _winthread;
        CFMutableSetRef _commonModes; //多个commonModes
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;  //当前运行的mode
        CFMutableSetRef _modes; //多个modes
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFTypeRef _counterpart;
    };
    

    核心方法是__CFRunloopRun(),截取其中的一部分,可以看到__CFRunLoopRun内部其实是一个do while循环。它在休眠时几乎不占用系统资源,这是由系统内核负责实现的

    void CFRunLoopRun(void) {   /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    
    
    runloop内部运行原理

    可以看到RunLoop在循环中,当接收到Input sources或者Timer sources时会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了

    3、NSTimer的创建

    在创建计时器NSTimer的时候我们通常会用到RunLoop(GCD创建的计时器是不基于runloop的),会把timer对象加到currentRunLoop中,并指定mode为NSDefaultRunLoopMode

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    

    RunLoopMode有

    - NSDefalutRunLoopMode          默认状态.空闲状态
    - UITrackingRunLoopMode       处理UI事件的mode,用于界面控件(ScrollView,tableView等等)追踪触摸滑动,保证界面滑动时不受其他Mode影响
    - UIInitializationRunLoopMode    刚启动App时进入的第一个Mode,启动完成后不再使用
    - NSRunLoopCommonModes     占位用的Mode,不是真正的Mode,默认在Defalut和Tracking这两种模式下
    - GSEventReceiveRunLoopMode   接受系统事件的内部 Mode,通常用不到
    

    但是在用户滑动ScrollView时,RunLoop会转到UITrackingRunLoopMode,这个时候timer就不会再继续运行,我们可以理解为runloop维护了一张map,以mode为key,以某种结构为value,我们命名为NSRunLoopState。
    在这个NSRunLoopState结构中,需要维护Input source和Timer source等事件源。
    当runloop切换到某种mode时,可以获取到对应的NSRunLoopState接口,并对该结构中的事件源进行处理,不能再处理其他mode中的事件源。
    为了解决这个问题,我们有两种方式
    1、设置计时器的runloopMode为NSRunLoopCommonModes
    2、开启线程去处理timer

    4、监听runloop的状态改变

    CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
         //创建监听者
         /*
         第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
         第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
         第三个参数 Boolean repeats:YES:持续监听 NO:不持续
         第四个参数 CFIndex order:优先级,一般填0即可
         第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
         */
         /*
         所有事件
         typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
         kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
         kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
         kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
         kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
         kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
         kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
         kCFRunLoopAllActivities = 0x0FFFFFFFU
         };
         */
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"RunLoop要处理Timers了");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"RunLoop要处理Sources了");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"RunLoop要休息了");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"RunLoop醒来了");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"RunLoop退出了");
                    break;
                    
                default:
                    break;
            }
        });
        
        // 给RunLoop添加监听者
        /*
         第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
         第二个参数 CFRunLoopObserverRef observer 监听者
         第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
         */
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    }
    

    5、RunLoop和线程

    只有主线程是默认开始RunLoop的,其他线程需要手动去开启RunLoop
    我们先看如下代码:

    dispatch_async(dispatch_queue_create("runloop.test", DISPATCH_QUEUE_CONCURRENT), ^{
            [self performSelector:@selector(log) withObject:self afterDelay:2];
        });
    
    -(void)log {
        NSLog(@"执行了log");
    }
    

    并不会打印“执行了log”,这是因为子线程默认不开启runloop,子线程中的任务执行完毕后,子线程就会被销毁
    我们把他改成

    dispatch_async(dispatch_queue_create("runloop.test", DISPATCH_QUEUE_CONCURRENT), ^{
            [self performSelector:@selector(log) withObject:self afterDelay:2];
            [[NSRunLoop currentRunLoop] run];
        });
    

    给子线程手动开启runloop,发现打印了“执行了log”,通过RunLoop可以实现线程保活、线程常驻

    6、RunLoop和自动释放池

    Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。
    那么什么时候释放,怎么释放呢?
    RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
    注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。

    相关文章

      网友评论

          本文标题:RunLoop原理总结-iOS

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