结识RunLoop

1、是啥?

处理事件的循环,简单来说它就是用来处理事件的一个对象,它给我们提供了两个对象,一个是基于CoreFoundation框架的CFRunloopRef,相对比较底层一点,另外一个就是基于CFRunloopRef封装的NSRunloop,我们通常通过NSRunlop完成相关操作;

2、有什么作用?

  1. 保持线程的鲜活;(让线程不会执行一次任务后就退出结束)
  2. 在有任务时工作,
    没有时休眠,减少CPU的压力;(在有输入源工作,没有时会sleep)
  3. 处理各种事件源;

3、它和线程有什么关系呢?

runloop和线程是一一对应的关系,也就是说一个线程对应一个runloop,程序启动后系统会创建一个全局的字典保存runloop,线程是key,runloop是对应的value,而且runloop采用类似懒加载的形式,子线程的runloop第一次去获取才会去创建,也就是根据线程去全局的字典中去找,找不到创建,但是需要注意的是主线程的runloop是程序启动就会自动获取的,CFRunloopRef中可以用CFRunLoopGetMain() 和 CFRunLoopGetCurrent()获取.

4、RunLoopMode和ModeItem

Runloop启动时需要指定一个RunloopMode才能运行,而且一个Runloop可以有多个RunloopMode,如果你要切换模式,则需要先退出当前的RunloopMode,一个RunloopMode又由多个modeItem组成,但是至少得有一个modeItem;下面介绍一下modeItem:
1. modeItem:简单来说既是源头,既是触发Runloop干事的源;
2. runlop中的源有输入源Input Source(各种触发事件),定时源Timer Source(定时器),观察者(Observer);
3. runloopMode可以认为是如何分配安排它内部上面各种源的执行方式和顺序;

<1> 输入源Input Source

(1)基于端口输入源(Source1):它是内核驱动的,苹果官方的介绍是这样的:


屏幕快照 2017-10-21 11.32.14.png

也就是说我们根本不需要创建source1,只需要创建一个端口对象NSPort,然后将这个port添加到当前runloop就好,当我们向该端口发送消息时,端口对象会为我们创建source1,主动唤醒runloop并触发相应回调;我们可以使用CFMachPortRef,CFMessagePortRef或CFSocketRef创建合适的端口对象;

(2)自定义输入源(Source0):
让Source0执行回调需要手动标记Source0为待处理状态,还需要呼醒Source0所在的RunLoop

<2>定时源Timer Source:
<3>观察者(Observer):

 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

我们可以手动添加observe监听runloop的状态做相应的回调:

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
   
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);

5、runloop的内部逻辑:

  1. 首先根据modeName找到对应mode,modeitems为空,直接return;
  2. 通知observers即将进入loop,(Entry)创建自动释放池autoreleasePool;
  3. 通知observers即将处理Timer;
  4. 通知observers即将处理Source0;
  5. 执行Source0加入的block回调;
  6. 如果有Source1的消息,需要立刻处理这个Source1;所以直接跳转到10;
  7. 如果没有待处理的消息,通知observers即将进入休眠(sleep);(销毁自动释放池并创建新的自动释放池)
  8. 如果有以下的情况发生,通知observers runloop即将被唤醒;
    a. source1事件;
    b. 一个Timer时间到了;
    c. runloop启动时设置的最大超过时间;
    d. 手动唤醒;调用CFRunLoopSourceSignal(source)并CFRunLoopWakeUp(runloop)唤醒runloop;
  9. 通知observers即将被唤醒;
  10. 处理唤醒时收到的消息;跳转到3;
  11. 如果有以下的情况发生,通知observers即将退出;
    <1>设置了runloop最大的超时时间;
    <2>modeitems为空;
    <3>手动停止,CFRunLoopStop();

6、runloop的启动和关闭;

(1)runloop的启动:

run:使用run启动runloop时CFRunLoopStop()是无法退出的,如果想退出那么只能让modeitem里面空;但是使用CFRunLoopRun()是可以直接用CFRunLoopStop()退出的;

runUntilDate:runloop运行直到某个时间点结束;

runMode:beforeDate:在某个时间点之前在,某个RunLoopMode下运行runloop;

(2)runloop的关闭:

设置超时时间:以上的二三中情况;
手动结束CFRunLoopStop():以上的第一种情况;

7、runloop的应用:

<1>AFNetWorking;

下面是每个runloop文章中必提的一段代码:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

这段代码我们需要注意两个问题:
1,为什么这里的请求创建的子线程需要用单例?
2,为什么在获取当前线程的runloop的时候向它添加一个NSMachPort并run?

答:AFN是对于NSURLConnection的封装,但是请求的结果是异步返回的,如果不给该线程添加runloop,那么请求结束后就会推出,这时候我们就需要给他添加runloop保证线程鲜活,添加NSMachPort是因为runloop的启动是必须有一个modeitem的,所以添加了一个source进去,挂起该线程;至于用单例是为了防止不断创建线程导致内存的一直涨;

<2>UITableView-FDTemplateLayoutCell;

  1. FDTemplateLayoutCell是百度团队开发一个高度预缓存的框架;它会在页面处于空闲状态时计算并缓存cell的高度,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验;对于cell可变的界面比如聊天优化是非常客观的;
  2. 它的原理也是利用runloop;它该当前的runloop注册了一个observe,监听runloop即将休眠的时候,也就是上面的步骤7时,runloop处理了所有的source事件后即将进入睡眠时,监听到后回调计算cell的高度;

<3>定时器滑动同时;
<4>GCD;
<5>PerformSelecter;

参考本章:
1,http://www.jianshu.com/u/3e55748920d2
2,https://blog.ibireme.com/2015/05/18/runloop/

你可能感兴趣的:(结识RunLoop)