文章内容来自:
深入理解RunLoop
官方文档-Runloop
Runloop概念
- Runloop本身是一个对象(CFRunloopRef或是NSRunloop )
- 管理事件和消息
- 提供一种机制:接收到消息或是事件处于运行状态,否则处于休眠避免CPU资源的浪费
Runloop和线程的关系
线程和Runloop是一一对应的,系统持有一个全局的CFMutableDictionary来保存线程和Runloop;其中key是线程,value对应CFRunloopRef实例;
线程的最初创建如果不获取Runloop是不会自动创建的
Runloop组成
一个runloop中存在多个Mode,每个Mode中存在Source/Observer/Timer ;每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出runloop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响
CFRunloopSourceRef (事件产生)
- source0:包含一个回调的函数指针,它不能主动触发事件,需要调用函数将其标记,然后再唤醒runloop执行;
- source1:包含一个mach_port和函数指针,被用于处理内核和其他线程发送过来的消息,可以直接唤醒runloop;
CFRunloopTimerRef
CFRunloopTimerRef包含一个时间长度和函数指针,runloop会注册时间,当时间一到就会触发回调函数;
CFRunloopObserverRef
CFRunloopObserverRef包含一个函数指针,它可以监控runloop的状态,一旦状态改变就会触发回调函数;
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
};
Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环
Runloop的Mode
CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
RunLoop 的内部逻辑
当以一种Mode启动runloop时,CFRunLoopRunSpecific函数会根据ModeName找到对应的Mode,判断Mode内部是否存在source/observer/timer,如果存在会调用CFRunloopRun函数,CFRunloopRun内部存在一个while循环,runloop会通知observers将要分别启动timer、source0、source1,一旦启动observers中对应的回调函数将会被调用;runloop会通过mach_msg()函数接收系统消息,当这一切执行完毕,当前runloop就会退出!
AutoreleasePool
一个runloop循环可以理解创建一个AutoreleasePool,在启动和离开时分别触发AutoreleasePool的push和pop函数,在此次循环结束后完成内存的清理工作;
事件响应
- 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
- 触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
- _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
事件的分发有一个hit-testing的过程,这样能够找到最终处理时间的控件,进而执行自定义的逻辑!
手势识别
- 当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
- 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调
- 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
- 当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
- 苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面
关于网络请求
- CFSocket 是最底层的接口,只负责 socket 通信。
- CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
- NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
- NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。
与 Runloop 相关的实例
NSTimer注册的事件不被执行
日常开发中,与 runLoop 接触得最近可能就是通过 NSTimer 了。一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoop 的 default mode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode 。而这个时候, Timer 就不会运行。
有如下两种解决方案:
第一种: 设置RunLoop Mode,例如NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个Mode的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
第二种: 另一种解决Timer的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新UI。