RunLoop
惯例先上文档和源码:
CFRunloopRef 源码
RunLoop文档
Swift-Corelibs-foundation
参看我之前Runloop章节,RunLoop线程是一一对应的;以及若干个Mode、若干个commonModeItem,还有一个当前运行的CurrentMode。如果在RunLoop中需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
CFRunLoopSource
对应CFRunLoopModeRef,其结构如下
1 | struct __CFRunLoopSource { |
Source分为Source、Observer、Timer三种,他们统称为modeItem。
__CFRunLoopSource是事件产生的地方。Source有两个版本:Source0 和 Source1。
- source0 只包含了一个回调(函数指针),source0是需要手动触发的Source,它并不能主动触发事件,必须要先把它标记为signal状态。使用时,你需要先调用
CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,也就是通过uint32_t _bits来实现的,只有_bits标记Signaled状态才会被处理。然后手动调用CFRunLoopWakeUp(runloop)
来唤醒 RunLoop,让其处理这个事件。 - source1 包含了一个 mach_port 和一个回调(函数指针),被用于
通过内核和其他线程相互发送消息
。这种 Source 能主动唤醒 RunLoop 的线程。简单来说就是更加偏向于底层。
ource1除了多个了getPort。其余的字段含义和source0相同。作用就是当source被添加到mode中的时候,从这个函数中获得具体mach_port_t。
CFRunLoopTimer
对应RunLoopTimerRef,结构如下:
1 | struct __CFRunLoopTimer { |
它和 NSTimer 是toll-free bridged 的 , 根据上面的分析t一个timer可能会在多个mode中存在。
CFRunLoopObserver
1 | struct __CFRunLoopObserver { |
CFRunLoopObserver是观察者,可以观察RunLoop的各种状态,每个 Observer 都包含了一个回调(也就是上面的CFRunLoopObserverCallBack函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。状态定义在_CF_OPTIONS:
1 | /* Run Loop Observer Activities */ |
下面是回调函数的原型:
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
小结
根据上面的数据结构,总结出如下内容。
一个model中有多个item,这些item由source、observe、timer组成。对于我们来讲用的最多的应该是observe和timer,常常通过回调来得知当前runloop的状态,进行来优化应用程序(比如监控在waiting状态下,这个时候做一些优化的事情)。其次设置定时器执行定时任务也是很常见的。
__CFRunloopRun(核心!!)
1 | /** |
RunLoop操作
- 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
- 线程刚创建时并没有 RunLoop(没有加到对应的runloop字典中),如果你不主动获取,那它一直都不会有。
- RunLoop 的创建是发生在第一次获取时。一般是获取主线程的时候。
- RunLoop 的销毁是发生在线程结束时。
- 只能在一个线程的内部获取其 RunLoop(主线程除外),否则就这个Runloop就没有注册销毁回调。这一点是根据pthread_equal(t, pthread_self())后面的代码,如果是当前线程后面才会注册销毁回调。因为上面讲过Runlopp暴露给外部的创建方式只有CFRunLoopGetMain() 和 CFRunLoopGetCurrent()两种,所以这种情况不用考虑。下面是CFRunloop.h的头文件暴露接口,可以看到获取方式只有两种。
应用场景
还是YY大神的博客
1 | { |
Block
在最开始介绍CFRunloop的时候就简单提了一下其中关于block的两个字段blocks_head,blocks_tail。并且也提到在runloop周期中会对此调用__CFRunLoopDoBlocks来执行加入到这个runloop的block。下面从源码来说明一下block如何与runloop结合的。
先来看看最基本的block_item 数据结构,特别注意这里保存了runloop的model,决定了block是否应该执行。
1 | struct _block_item { |
在执行block的时候会传入
1 | /** |
通过上面分析可以知道:
- block其实在runloop中通过循环链表保存的
- 如果block可以加入到多个model下面,但是执行block只有在加入的那个model下才能之后,或者加入modle用common标记
- 每次调用__CFRunLoopDoBlocks,会把加入的block遍历执行,然后重置循环链表。
AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
手势识别
上面可以看到第二个observe就是_UIGestureRecognizerUpdateObserver,关于手势识别的。
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
上面可以看到第三和四个observe分别是_beforeCACommitHandler与_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv,是关于动画及界面更新的。
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
定时器
上面截图中还有个timer
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 来实现延迟执行,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
GCD
回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
<##>
});
网络请求
常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
Runloop在平时开发中的应用
- AFN线程保活
AF希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop。过程在networkRequestThreadEntryPoint中,因为RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中,具体内容对应start方法。
- AsyncDisplayKit
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
可以直接看源码进行分析AsyncDisplayKit,但是现在更名为Texture
RunLoop的启动和退出
- 启动RunLoop的三种方式
通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()可以获取当前线程的runloop。这三种方式无论通过哪一种方式启动runloop,如果没有1
2
3- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;一个输入源或者timer
附加于runloop
上,runloop
就会立刻退出。
(1) 第一种方式,runloop会一直运行下去(线程常驻),在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:
方法。
(2) 第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:
方法。
(3) 第三种方式,runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出。前两种启动方式会重复调用runMode:beforeDate:方法。
- 退出RunLoop的方式
(1) 第一种启动方式的退出方法
文档说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。
如果runloop没有input sources或者附加的timer,runloop就会退出。
虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。
(2)第二种启动方式的退出方法 runUntilDate:
可以通过 设置超时时间 来退出 runloop。
(3)第三种启动方式的退出方法 runMode:beforeDate:
通过这种方式启动,runloop只会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。
如果我们想控制 runloop 的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:
具体可以参考苹果文档给出的方案,如下:
1 | NSRunLoop *myLoop = [NSRunLoop currentRunLoop]; |
1 | //在关闭runloop的地方 |
总之
如果不想退出runloop可以使用第一种方式启动runloop;
使用第二种方式启动runloop,可以通过设置超时时间来退出;
使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。