深入理解RunLoop

340 阅读10分钟

RunLoop 的概念

RunLoop实现了让线程随时处理事件但并不退出。例如EventLoop。 RunLoop管理了其需要处理的事件和消息,并提供接口函数来执行EventLoop的逻辑。线程执行了函数之后就会一直处于函数内部的“接受消息-等待-处理”的循环中,直到循环结束(比如传入quit消息),函数返回。

RunLoop与线程的关系

RunLoop和线程的关系是一一对应的。 苹果不允许直接创建RunLoop。只能通过获取函数来内部创建,全局的Dic,key是线程,value就是RunLoop,当第一次进入Dic无RunLoop时先创建一个,如果有的话根据Key,就返回RunLoop。所以RunLoop的创建是发生在第一次获取线程的RunLoop时,不获取就不会创建。RunLoop的销毁发生在线程结束时。

RunLoop对外的接口

image
借用一下大神的图片,RunLoop中内部关系是这样的。

每个RunLoop中都有若干个Mode,每个Mode中又包含若干个Source/Timer/Observer。每次调用RunLoop时,只能进入一个Mode,这个Mode就叫做CurrentMode。要切换Mode的话就得退出Loop,重新进入指定Mode。原因是为了分隔开不同组的Source/Timer/Observer,不能互相影响。

Source 是事件产生的地方,分为Source0和Source1。Source0只包含了回调指针,所以不能主动触发事件,需要调用SourceSignal将Source标记为处理,然后手动唤醒WakeUp这个RunLoop才能处理事件。Source1包含了mach_port和回调指针,通常被用于通过内核和其他线程相互发送消息,可以主动唤醒RunLoop来处理事件。

Timer是基于时间的触发器。包含了一个时间长度和回调指针。加入RunLoop中,当时间长度到了时,RunLoop就会被唤醒执行回调。

Observer是观察者,每个Observer都包含了回调指针,当RunLoop状态发生变化是就能通过回调接收到变化,比如,即将进入Loop,处理Timer,处理Source,进入休眠,休眠唤醒,退出Loop。

每个Source/Timer/Observer 统称为mode item。如果一个Mode中一个item中都没,Runlopp会直接退出,不进入循环。

RunLoop的Mode

大致结构

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
...
};

“CommonModes”:Mode将自己标记为“Common”属性,就是通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中。每当RunLoop的内容发生变化时,RunLoop会自动将_commonModeItems里的Source/Timer/Observer 同步到具有“Common”标记的所有Mode里。

主线程的RunLoop中有两个预置的Mode:kCFRunLoopDefaultMode和UITrackingRunLoopMode,都标记成“Common”属性。kCFRunLoopDefaultMode是App平时所处的状态。UITrackingRunLoopMode是追踪ScrollView滑动的状态。比如创建Timer加入Default时,Timer就会得到重复回调,但当滑动TableView时,RunLoop会将Mode切换为TrackingRunLoopMode,这时Timer就不会被回调,也不会影响滑动操作。如果你需要在两个Mode中都得到回调,一个方法是分别加入这两个Mode。另一个方式就是加入顶层的RunLoop的“commonModeItems”中,“commonModeItems”被RunLoop自动更新到所有具有“Common”属性的Mode中去。

RunLoop暴露出来只有两个接口, CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); CFRunLoopRunInMode(CFStringRef modeName, ...); 只能通过mode name来控制内部mode,当传入mode时,如果RunLoop没有对应mode,就会自动创建mode。RunLoop内部的mode只能增加不能删除。

kCFRunLoopCommonModes 可以来操作Common Items,或者标记Mode为“Common”。

RunLoop 内部逻辑

image
借用一下大神的图片,RunLoop中内部逻辑是这样的。

1.用DefaultMode启动或者指定Mode启动(可以设置超时时间)
2.根据modeName找到对应Mode,如果没有Source/Timer/Observer,直接返回。
3.通知Observer:RunLoop即将进入loop。
4.进入Loop。
do {
5.通知Observer:RunLoop即将触发Timer。
6.通知Observer:RunLoop即将触发Source0回调。
7.触发Source0回调。
8.如果有Source1处于ready状态,直接处理Source1然后处理消息。
9.通知Observer:RunLoop即将休眠。
10.线程休眠,直到被某个事件唤醒(比如:基于port的Source事件,Timer时间到了,RunLoop超时,手动调用)
然后是上面所述事件的触发后执行的事件
}while ()
11.通知Observer:RunLoop即将退出。
实际上就是一个循环,当RunLoopRun时,就会一直停留在这个循环里面,直到超时或被停止,才会返回。

RunLoop底层实现(暂时看不懂)

RunLoop实现的功能

AutoreleasePool

do {
1.通知Observers:即将进入RunLoop。并创建AutoreleasePool。
2.通知Observers:即将触发Timer回调。
3.通知Observers:即将触发Source0回调。
4.通知Observers:即将休眠。
5.通知Observers:线程被唤醒。比如:被Timer唤醒,被dispatch唤醒执行dispatch_async放入main queue的block,被Source1的事件唤醒执行事件。
} while()
6.通知Observers:即将退出RunLoop。并释放AutoreleasePool。

App启动,苹果注册两个Observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler,第一个监视即将进入Loop,回调创建自动释放池,优先级最高,保证创建释放池发生在所有回调之前。第二个监视了两个事件:1.监视:准备进入休眠,调用释放旧池并创建新池。2.监视:即将退出Loop,来释放自动释放池,优先级最低,保证释放池子发生在所有回调之后。

事件响应

苹果注册了一个Source1来接收系统事件,回调函数为__IOHIDEventSystemClientQueueCallback。

当发生硬件事情(触摸/锁屏/摇晃等),由IOKit生成一个IOHIDEvent并由SpringBoard接收。SpringBoard只接收按键(锁屏/静音等),触摸,加速,传感器等等几种Event,随后用moch port 转发给需要的App进程。随后就会触发苹果的Source1的回调,并_UIApplicationHandleEventQueue() 进行内部的分发。

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

手势识别

_UIApplicationHandleEventQueue()识别手势之后,会先调用Cancel打断当前的touchesBegin/Move/End系列回调。随后系统将UIGestureRecognizer标记为待处理。

苹果注册了一个Observer监测Loop即将进入睡眠,这个Observer的回调 _UIGestureRecognizerUpdateObserver()内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。当有UIGestureRecognizer的变化(比如创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当操作UI时,比如改变Frame,更新UIView/CALayer的层次时,或者手动调用UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理,并提交到一个全局的容器。

苹果注册了一个Observer监听即将进入休眠和即将退出Loop,回调_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),这个函数会遍历所有的UIView/CALayer以执行实际的绘制和调整,并更新UI界面。

定时器

NSTimer其实就是LoopTimer,他们之间是对象桥接的。NSTimer注册到RunLoop后就会被RunLoop为其在重复的时间点注册号事件。RunLoop为了节约资源,并不会在非常准确的时间回调这个Timer。Timer有个属性叫Tolerance(宽容度),标示误差值。如果时间点被错过,会跳过去执行下一个时间点,不会延后执行。 CADisplayLink 是一个和屏幕刷新率一致的定时器,但原理比NSTimer更复杂,如果屏幕刷新中执行了很长的任务,中间就会被跳过去,那一帧就跳过去了,造成界面卡顿的感觉,Facebook开源的AsyncDisplayLink就是解决了界面刷新卡顿的问题,用到了RunLoop。

PerformSelecter

当调用NSObject的performSelecter方法时,内部会创建一个Timer放到当前线程的RunLoop中。如果没RunLoop,那么就不执行这个方法了。

关于GCD(这段看不懂暂时)

调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程RunLoop发送消息,RunLoop被唤醒之后取得block,在回调__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 执行block,但是仅限于dispatch到主线程,dispatch到其他线程由libDispatch处理。

关于网络请求

CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire

NSURLConnection的工作过程:
使用NSURLConnection时,会传入Delegate,当调用了[connection start],Delegate就会不停收到事件回调,实际上,start这个函数内部会获取CurrentRunLoop,然后在其的DefaultMode添加了4个Source0(需要手动触发)。CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。 当开始传输网络时,NSURLConnection创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中CFSocket线程是处理底层socket连接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket事件,并通过之前添加的Source0通知到上层的Delegate。具体看图:

image
借用一下大神的图片。 NSURLConnectionLoader中的RunLoop通过一些基于mach port的Source接收来自底层CFSocket的通知。当收到通知向CFMultiplexerSource等Source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知。CFMultiplexerSource会在Delegate线程的RunLoop对Delegate执行实际的回调。

RunLoop的实际应用举例

AFNetworking

AFURLConnectionOperation 这个类基于NSURLConnection构建的,希望能在后台线程接收Delegate回调。为此AFNetworking单独创建了一个线程,并在线程中启动了一个RunLoop。RunLoop启动必须要有Source/Timer/Observer,所以在runLoop run之前创建了一个新的NSMachPort添加进去,保证不让RunLoop退出。 当需要这个后台线程执行任务时,AFNetworking调用[performSelector:onThread]将任务扔到后台线程RunLoop中。

AsyncDisplayKit

UI线程出现繁重的任务就会卡顿,任务通常分为:排版,绘制,UI对象操作。
排版包括计算视图大小,计算文本高度,重新计算子视图的排版等操作。
绘制包括文本绘制(例如CoreText),图片绘制(例如预先解压),元素绘制(Quartz)等操作。
UI对象操作通常包括UIView/CALayer等UI对象的创建,设置属性和销毁。

排版和绘制可以扔到后台线程执行,最后UI操作只能在主线程执行。有时UI操作还需要依赖前面的操作结果(例如TextView创建时可能需要提前计算文本的大小)。ASDK做的,就是尽量把能放入后台的任务放入后台,不能的尽量推迟(比如视图的创建,属性的调整)。 ASDK创建了一个名为ASDisplayNode的对象,封装了UIView/CALayer,具有和UIView/CALyer相似的属性,比如frame,backgroundColor等。这些属性都可以在后台更改,通过Node操作这些属性,就可以把排版和绘制放到后台。但是总需要把这些属性同步到主线程的UIView/CALayer上。ASDK仿照QuartzCore/UIKit框架的模式,实现了一套类似界面更新机制:在主线程RunLoop中添加一个Observer,监听kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理任务时,处理同步属性的这些任务。

思路来源:ibireme大神 深入理解RunLoop

声明:都是对上面博客的引用,我自己的理解加上引用原博客的东西。