Runloop 知识点总结

https://segmentfault.com/a/1190000004938638

Runloop 组成


image.png

每个mode封装了自己需要响应的事件和需要通知的observer。这些事件的形式有两种:timer和input source。另外可以在mode中注册observer来观察runloop的运行状态。

  • timer:给线程发送同步事件,但是是在未来指定时间发送或者周期性的响应事件。

  • source:给线程发送异步事件,分为两种,source0和source1。source0需要手动在另一个线程手动触发(通过调用void CFRunLoopSourceSignal ( CFRunLoopSourceRef source );),CFSocket就是这种形式;source1是基于mach port的事件,程序只要把message塞给port,底层就会触发runloop中对应source1。目前CFMachPort和CFMessagePort是这种形式。以上描述来自CFRunloopSource Reference

  • observer:可以观察六种runloop的运行状态:

    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//所有状态
};

这里要提一句的是,timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。

source0:手势识别
source1:事件响应,CADisplayLink,系统事件
Timer:NSTimer CFRunLoopTimerRef

事件响应 (source1)

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别 (source0)

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

Autorelease pool

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 了。

NSTimer (基于runloop中的timer实现)

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

NSURLConnection (source0处理回调 & source1接收Socket消息)

iOS 中,关于网络请求的接口自下至上有如下几层:

CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
CFSocket 是最底层的接口,只负责 socket 通信。

CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。

NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。

NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

CADisplayLink

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source1)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。

你可能感兴趣的:(Runloop 知识点总结)