最近这几天研究了下Runloop,下面就来分享一下心得(有不好的地方请帮忙指出来,共同进步,谢谢!!!)
一:前言
>RunLoop是iOS和OS X开发中比较基础的一个概念,他是程序主线程基础设施的相关部分,可以说,没有线程就没有RunLoop,他就是为线程服务的.Cocoa程序提供了代码运行主程序的循环并自动启动和管理(辅助线程需要我们人工管理).iOS程序中UIApplication的run方法作为程序启动步骤的一部分,在程序正常启动的时候,就会自动启动循环(RunLoop).
>RunLoop的运行方式是一种概念,不仅仅是在Cocoa,在其他(例如:window等等)都有类似的方式来管理运行方式.如Android的Looper就是类似的机制.
二:RunLoop的概念
在我们使用app的时候,如果不去主动触发(例如:点击,触摸屏幕等)事件,那么什么事情也不会发生,但是当我们点击了屏幕上的某一个控件,那么程序会立即做出相关的反应(很神奇是不是).这就是RunLoop.
RunLoop,顾名思义,就是不断运行着的循环,你的线程进入并使用它来运行响应输入事件的处理程序.你的代码要提供实现循环部分的循环语句,换言之,就是要有while或者for循环(通俗点说在有事的事情处理事情,没有事情的时候就闲置着).它会一直处于"接受消息->等待->处理"的循环中,直到循环结束.
三:RunLoop的组成
iOS和OSX系统中,提供了这样的两个对象:NSRunLoop和CFRunLoop.
>CFRunLoop是基于CoreFoundation的,它是纯C的API,是线程安全的.
>NSRunLoop是基于Cocoa的,是对CFRunLoop的再封装,它提供了面向对象的API,但他不是线程安全的.
CFRunLoopRef 的代码是开源的,你可以在这里下载到,整个 CoreFoundation 的源码.为了方便跟踪和查看,你可以新建一个 Xcode 工程,把源码拖进去看.
苹果并不允许我们创建一个RunLoop,RunLoop通常都是由系统在创建线程的时候就创建好的,我们只是可以对其进行一些操作而已.在我们开辟新的线程的时候系统会自动的帮我们创建一个RunLoop(除主线程外都是隐式的),主线程的RunLoop不用我们管理,子线程的RunLoop需要我们进行管理.每次运行RunLoop(不管是显式还是隐式),都需要指定其RunLoopMode.
每一个线程里面包含一个RunLoop(图1-1),一个RunLoop中包含一个RunLoopMode,一个RunLoopMode包含着source(输入源,下面都会这样说)/timer(定时源,下面都会这样说)和observe(观察者).RunLoop接受的输入事件包含两部分:source(输入源)和timer(定时源).
>>source:传递异步事件,接受的消息通常来自于其他程序或者线程.
>>timer:传递同步事件,发生在特定的时间或者重复的时间间隔.
>>observe:除了处理源外,RunLoop还会生成各种的通知,可以注册observe来观察通知(如图1-4),
图1-3显示了RunLoop的概念结构以及各种源.
source来源取决于输入源的种类:基于端口的源(例子见基于端口的源)和自定义的源(例子见自定义输入源),基于端口的输入源监听程序相应的端口.自定义输入源则监听自定义的事件源。至于runloop,它不关心输入源的是基于端口的还是自定义的,系统会实现两种输入源供你使用.两类输入源的区别在于如何显示:基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送.
输入源传递异步消息给相应的处理例程,并调用runUntilDate:方法来退出(由在线程里面相关的NSRunLoop对象调用).定时源则直接传递消息给处理例程,但并不会退出run loop.
当你创建输入源,你需要将其分配给run loop中的一个或多个模式.模式只会在特定事件影响监听的源.大多数情况下,runloop运行在默认模式下,但是你也可以使其运行在自定义模式.若某一源在当前模式下不被监听,那么任何其生成的消息只有在runloop运行在其关联的模式下才会被传递.在run loop运行过程中,只有和模式相关的源才会被监视并允许他们传递事件消息(类似的,只有和模式相关的观察者才会被通知run loop的进程).和其他模式关联的源只有在run loop运行在其模式下才会运行,否则处于暂停状态.
timer(例子见基于timer的源)在预设的时间点同步方式传递消息.定时器是线程通知自己做某事的一种方法.例如:搜索控件可以使用定时器,当用户连续输入的时间超过一定时间时,就开始一次搜索.这样使用延迟时间,就可以让用户在搜索前有足够的时间来输入想要搜索的关键字.
尽管定时器可以产生基于时间的通知,但它并不是实时机制.和输入源一样,定时器也和你的runloop的特定模式相关.如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到runloop运行在相应的模式下.类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次runloop开始相应的处理程序.如果run loop不再运行,那定时器也将永远不启动.例子见1-7.
你可以配置定时器仅工作一次还是重复工作.重复工作定时器会基于安排好的时间而非实际时间调度它自己运行.举个例子:如果定时器被设定在某一特定时间开始并5秒重复一次,那么定时器会在那个特定时间后5秒启动,即使在那个特定的触发时间延迟了.如果定时器被延迟以至于它错过了一个或多个触发时间,那么定时器会在下一个最近的触发事件启动,而后面会按照触发间隔正常执行.
observe:我们可以给RunLoop添加observe来监视RunLoop执行不同的阶段情况.源是合适的同步事件或异步事件发生时的触发,而观察者是RunLoop运行时的触发.我们可以将RunLoop与以下事件进行关联:如图1-6:
<kCFRunLoopEntry:入口
<kCFRunLoopBeforeTimers:准备处理一个定时器
<kCFRunLoopBeforeSources:准备处理一个输入源
<kCFRunLoopBeforeWaiting:准备进入休眠状态
<kCFRunLoopAfterWaiting:刚从休眠中唤醒
<kCFRunLoopExit:退出
<kCFRunLoopAllActivities:包含上面所有的
接下来就是创建observe并添加进你的RunLoop里面,你可以创建一个CFRunLoopObserverRef类型的实例。它会追踪你自定义的回调函数以及其它你感兴趣的活动(例子我们设置的感兴趣的内容是kCFRunLoopAllActivities监视所有阶段),例子见图1-4.
上面的 Source/Timer/Observer 被统称为 mode item(1-7),一个 item 可以被同时加入多个 mode.但一个 item被重复加入同一个 mode 时是不会有效果的.如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环.
RunLoop模式:
在CoreFoundation里面的CFRunLoopRef有五个类:
除了上面四个,还有一个CFRunLoopModeRef,不过他并没有对外暴漏,只是通过 CFRunLoopRef 的接口进行了封装.
CFRunLoopSourceRef是事件产生的地方.有两个版本source0和source1.(基于端口和自定义的).
source0值包含了一个回调,他并不能主动触发,使用时,需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件(具体例子见自定义输入源:).
Source1 包含了一个 mach_port 和一个回调(函数指针,被用于通过内核和其他线程相互发送消息.这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会提到.
CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer 是可以混用.其包含一个时间长度和一个回调(函数指针).当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调(具体例子见基于timer的源(配置定时源)).
CFRunLoopObserverRef是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化(例子见图1-4).
CFRunLoopMode 和 CFRunLoop 的结构大致如下:
这里有个概念叫 "_commonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到RunLoop的"commonModes"中).每当 RunLoop 的内容发生变化时,RunLoop 都会自动将_commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里.
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和
UITrackingRunLoopMode.这两个 Mode 都已经被标记为"Common"属性.DefaultMode 是 App
平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态.当你创建一个 Timer 并加到DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为TrackingRunLoopMode,但这时 Timer 并不会被回调(暂停).有时你需要一个Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode.还有一种方式,就是将 Timer加入到顶层的 RunLoop 的"commonModeItems"中."commonModeItems" 被 RunLoop自动更新到所有具有"Common"属性的 Mode 里去.如图:1-7
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:
>CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
>CFRunLoopRunInMode(CFStringRef modeName, ...);
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode.
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes
(NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为
"Common".使用时注意区分这个字符串和其他 mode name.
你可以调用CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)来添加自定义的model,例如:1-8,定义了个CustomRunLoopMode添加进commentModel中.当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef.对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除.
四:RunLoop的使用
首先来说说苹果中利用RunLoop实现的功能吧.(AutoreleasePool,事件响应,手势识别,界面更新,定时器,PerformSelecter)
>AutoreleasePool
当app启动后.苹果首先在主线程里面注册了两个observe.第一个observe监测的事件是 kCFRunLoopEntry,其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池.其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前.
第二个 Observer 监视了两个事件:kCFRunLoopBeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;kCFRunLoopExit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池.这个Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后.
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
>事件响应
苹果注册了一个 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事件都是在这个回调中完成的.
>手势识别
当上面的 _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 界面.
>定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的.一个 NSTimer
注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件.例如 10:00,10:10,10:20这几个时间点.RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer.Timer 有个属性叫做 Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差.
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行.就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了.
CADisplayLink是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个Source).如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉.Facebook 开源的AsyncDisplayKit 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析.
>PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中.所以如果当前线程没有 RunLoop,则这个方法会失效.
当调用 performSelector:onThread: 时,实际上其会创建一个Timer加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效.
五:其他
RunLoop的事件队列:每次运行run loop,线程的run loop对会自动处理之前未处理的消息,并通知相关的观察者.具体的顺序如下
1.通知观察者runloop已经启动
2.如果有timer即将触发时,通知观察者
3.如果有非基于端口的源即将触发时,通知观察者
4. 触发任何准备好的非基于端口的源
5.如果基于端口的源准备好并且处于等待状态,立即启动,并进入步骤9
6.通知观察者线程将要进入休眠
7.将线程置于休眠知道任一下面的时间发生:
>基于端口的源被触发
>timer被触发
>runloop运行时间到了过期时间
>runloop被显示的唤醒
8.通知观察者线程将要被唤醒
9.处理未处理的事件:
>如果用户定义的定时器启动,处理定时器事件并重启run loop.进入步骤2
>如果输入源启动,传递相应的消息
>如果run loop被显式唤醒而且时间还没超时,重启run loop.进入步骤2
10.通知观察者runloop结束
什么时候使用RunLoop?官方给出的建议是:
>需要使用Port或者自定义Input Source与其他线程进行通讯。
>需要在线程中使用Timer。
>需要在线程上使用performSelector*方法。
>需要让线程执行周期性的工作。
最后,该说下自定义源了:
自定义输入源:
这个过程分几个步骤:---定义/创建(一个源)---安装(将输入源安装到所在Run Loop中)---注册(将输入源注册到客户端,协调输入源的客户端)---调用(通知输入源,开始工作)
由于是由自己创建输入源来处理自定义消息,实际配置选是灵活配置的.调度例程,处理例程和取消例程都是你创建自定义输入源时最关键的例程.然而输入源其他的大部分行为都发生在这些例程的外部.比如,由你决定数据传输到输入源的机制,还有输入源和其他线程的通信机制也是由你决定.
图5-1显示了一个自定义输入源的配置的例子.在该例中,程序的主线程(main Thread)维护了输入源的引用,输入源所需的自定义命令缓冲区和输入源所在的runloop.当主线程有任务(Task)需要分发给工作线程时,主线程会给命令缓冲区发送命令和必须的信息来通知工作线程开始执行任务(Wake up).(因为主线程和输入源所在工作线程都可以访问命令缓冲区,因此这些访问必须是同步的)一旦命令传送出去,主线程会通知输入源并且唤醒工作线程的runloop.而一收到唤醒命令,run loop会调用输入源的处理程序,由它来执行命令缓冲区中相应的命令.
最后见例子:例子中首先封装了一个事件源,其中ZXRunLoopSource对象管理着命令缓冲区并以此来接收其他线程的消息.例子同样给出了RunLoopContext对象的定义,它是一个用于传递RunLoopSource对象和run loop引用给程序主线程的一个容器.如图5-2.
安装输入源到RunLoop.
列表5-3显示了RunLoopSource的init和addToCurrentRunLoop的方法.Init方法创建CFRunLoopSourceRef的不透明类型,该类型必须被附加到runloop里面.它把RunLoopSource对象做为上下文引用参数,以便回调例程持有该对象的一个引用指针.输入源的安装只在工作线程调用addToCurrentRunLoop方法才发生,此时RunLoopSourceScheduledRoutine被调用.一旦输入源被添加到runloop,线程就运行run loop并等待事件.
协调输入源的客户端
为了让添加的输入源有用,你需要维护它并从其他线程给它发送信号.输入源的主要工作就是将与输入源相关的线程置于休眠状态直到有事件发生.这就意味着程序中的要有其他线程(该例子中是mainthread)知道该输入源信息并有办法与之通信.
通知客户端关于你输入源信息的方法之一就是当你的输入源开始安装到你的runloop上面后发送注册请求.你把输入源注册到任意数量的客户端,或者通过由代理将输入源注册到感兴趣的客户端那.列表5-5显示了应用委托定义的注册方法以及它在RunLoopSource对象的调度函数被调用时如何运行.该方法接收RunLoopSource提供的RunLoopContext对象,然后将其添加到它自己的源列表里面.另外,还显示了输入源从run loop移除时候的使用来取消注册例程
运行RunLoop(5-6)
注意:在辅助线程启动RunLoop的前必须添加一道输入源或者定时源,否则就会在一开始运行后就退出RunLoop.
获取当前线程的RunLoop和主线程的RunLoop:
>在cocoa中,使用[NSRunLoop currentRunLoop]和[NSRunLoop mainRunLoop]分别获取.
>在Corefoundation中,使用CFRunLoopGetCurrent()和CFRunLoopGetMain()来分别获取.
启动RunLoop只对辅助线程有效,一个run loop通常必须包含一个输入源或定时器来监听事件,有几种方法可以启动RunLoop:
>无条件的启动
>设置超时时间
>特定的模式
无条件的进入RunLoop是最不提倡的,因为这样会使你的线程一直处于一个循环中,让我们对RunLoop本身的控制很少.我们可以进行添加和删除源和定时器,但是退出的唯一方法就是杀死他.
设置超时时间是一种比较好的方法,这样的话RunLoop可以运行到某一事件到达或者规定的时间到期.如果是事件到达了,消息会交给相应的程序来处理,然后退出,可以重启RunLoop来等待下一事件;如果是规定的时间到期了,我们只需简单的重新启动RunLoop就行了.
注意的是
通知输入源(5-7)
基于timer的源(配置定时源)
创建一个定时源,需要我们做的就只是创建一个定时器对象并把它调度到你的run loop.Cocoa程序中使用NSTimer类来创建一个新的定时器对象,而CoreFoundation中使用CFRunLoopTimerRef不透明类型.本质上,NSTimer类是CoreFoundation的简单扩展,它提供了便利的特征,例如能使用相同的方法创建和调配定时器.
首先看用系统的timer(5-8)
上面例子中就是首先添加observe观察当前RunLoop的活动,然后创建timer,并添加进当前RunLoop中.
再看自定义timer(5-9)
看图1-3,selector也是特殊的基于自定义的源.理论上来说,允许在当前线程向任何线程上执行发送消息,和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题.不像基于端口的源,一个selector执行完后会自动从run loop里面移除.
表5-9列出了NSObject中可在其它线程执行的selector.由于这些方法时定义在NSObject中,你可以在任何可以访问Objective-C对象的线程里面使用它们,包括POSIX的所有线程。这些方法实际上并没有创建新的线程执行selector.
最后附上demo