iOS NSRunLoop实现原理及应用

什么是runloop?

  RunLoop 是什么?RunLoop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

RunLoop Mode

Mode可以视为事件的管家,一个Mode管理着各种事件
在这里插入图片描述
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
苹果文档中提到的 Mode 有五个,分别是:

NSDefaultRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSEventTrackingRunLoopMode
NSRunLoopCommonModes

iOS 中公开暴露出来的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合)。
五种Mode的介绍如下图:
在这里插入图片描述

RunLoop 与线程的关系

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

RunLoop Source

Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem。

CFRunLoopSource

根据官方的描述,CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0和source1两个版本,source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,
source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调。
Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。官方也指出可以自定义Source,因此对于CFRunLoopSourceRef来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,详细情况可以查看官方文档。

CFRunLoopObserver

CFRunLoopObserver是观察者,可以观察RunLoop的各种状态,并抛出回调。
CFRunLoopObserver可以观察的状态有如下6种:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即将进入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件
    kCFRunLoopExit = (1UL << 7),//run loop已经退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。 Runloop Observer 可以监控上面的 Runloop 事件,具体流程如下图。
在这里插入图片描述

RunLoop应用

在这里插入图片描述
NSTimer:加入commonModel。防止scrollView滑动时timer事件不会触发的问题

UIEvent:事件响应流程这一块,后续会详细整理。

Autorelease和自动释放池相结合,在runloop进行休眠的时候进行内存的释放。
dispatch_get_main_queue():当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
AFNetworking:主要是加入source开启后台常驻线程。
参考文章:http://www.cocoachina.com/ios/20150601/11970.html
https://www.jianshu.com/p/23e3ff9619c3
更多优质文章,可以微信扫码关注:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/HHL110120/article/details/89393627