目录
文章目录
RunLoop简介
运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能
Ps:也可以理解为事件循环,绝对不止是死循环这么简单的一个回答。实质上就是runloop内部状态的转换。
1.用户态:应用程序都是在用户态,平时开发用到的api等都是用户态的操作
2.内核态:系统调用,牵涉到操作系统,底层内核相关的指令。
RunLoop对象
在 CoreFoundation 框架为 CFRunLoopRef 对象,它提供了纯 C 函数的 API,并且这些 API 是线程安全的;
在 Foundation 框架中用 NSRunLoop 对象来表示,它是基于 CFRunLoopRef 的封装,提供的是面向对象的 API,但这些 API 不是线程安全的。
RunLoop的构成
CFRunLoopRef (NSRunloop)
CFRunLoopModeRef (NSRunloopMode)
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
一个RunLoop包含若干个Mode(CFRunLoopModeRef),每个Mode又包含若干个source/timer/observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。
CFRunLoopModeRef (NSRunloopMode)
公开的 mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode。
还有一个伪Mode是kCFRunLoopCommonModes(NSRunLoopCommonModes) 若干个mode的集合。
程序运行的大多时候都处于该 DefaultMode 下,滑动 tableView 或 scrollerView 时为了界面流畅而用的TrackingRunLoopMode。
一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件 。
(tableview滑动时timer停止的原因,可将timer默认的DefaultMode改成CommonModes)
CFRunLoopSourceRef
官方文档在概念上把 source 分为三类:Port-Based Sources,Custom Input Sources,Cocoa Perform Selector Sources。在源码中根据标记则分为source0 和 source1。
Source0 : 负责App内部事件,由App负责管理触发,例如UIEvent、UITouch事件。包含了一个回调,不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒 RunLoop,让其处理这个事件。
-performSelector:onThread:withObject:waitUntilDone: inModes:创建的是source0任务。
Source1 : 包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒runloop,接收分发系统事件。
Source1和Timer都属于端口事件源,不同的是所有的Timer都共用一个端口(Timer Port),而每个Source1都有不同的对应端口。
Source0属于input Source中的一部分,Input Source还包括cuntom自定义源,由其他线程手动发出。
CFRunLoopTimerRef
定时器、即NSTimer,还可以由方法 performSelector:afterDelay:来触发(本质上 afterDelay, 底层就是启动了 timer )。
CFRunLoopObserverRef
监听 RunLoop 7种状态的变化:
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变
RunLoop中几个类的关系图
RunLoop和线程间的关系
每条线程都有唯一的一个与之对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
主线程的RunLoop会自动创建,在UIApplicationMain函数中,保证了程序的持续运行,子线程的RunLoop需要主动创建(子线程timer不起作用的原因)
RunLoop在第一次获取时创建(有点像懒加载),在线程结束时销毁,依赖于线程
获取主线程或当前线程对应的 RunLoop,只能通过 CFRunLoopGetMain(NSRunLoop.mainRunLoop) 或 CFRunLoopGetCurrent (NSRunLoop.currentRunLoop)获取。
RunLoop的运行逻辑
runloop 整个的运行逻辑都是在于三个重要的对象如何运作:source (输入源)、timer (定时器)、observer (观察者)。
RunLoop运行逻辑
runloop 的运行逻辑就是 do-while 循环下运用观察者(observer)模式(或者说是消息发送),根据7种状态的变化,处理事件输入源(source)和定时器(timer)。
RunLoop 实际应用
1、自动释放池
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 了。
2、事件响应
App响应触摸事件
当一个硬件事件(触摸/锁屏/摇晃/加速等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收, 随后由mach port 转发给需要的App进程。【系统接收部分】
(1).APP进程的mach port接收来自SpringBoard的触摸事件,主线程的runloop被唤醒,触发source1回调。
(2).source1回调又触发了一个source0回调,调用_UIApplicationHandleEventQueue() 进行应用内部的分发,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
(3).source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队后UIApplication为触摸事件寻找最佳响应者。
(4).寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。
3、手势识别
调用_UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
4、界面刷新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
5、GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
6、定时器
NSTimer 其实就是 CFRunLoopTimerRef
performSelecter:afterDelay: 实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中
当调用 performSelector:onThread: 时如果waitUntilDone为NO,如果对应线程没有 RunLoop 该方法也会失效。
7、常驻线程
子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。
因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。可以为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort 对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源。
代表:AF2.x (AF3.0不再需要线程常驻)
8、异步渲染
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行