RunLoop原理总结-iOS

2020-05-26  本文已影响0人  桔子橙子柚子_F

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会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。

上一篇下一篇

猜你喜欢

热点阅读