Thread RunLoop
一般一个线程一次只执行一个任务,执行完成后就会退出。如果需要一个机制,让线程能随时响应处理新的任务并长时间存在不退出,那么它就是RunLoop。
RunLoop就是一个不断调度工作和处理事件的事件处理循环,对事件处理过程进行了更好的抽象和封装,让客户代码不用关心底层琐碎的消息处理实现的同时,能够轻松动态地添加删除执行任务。目的是让线程有任务时忙碌工作,没任务时休眠节能。
RunLoop不是完全自动的。需要给线程设定代码在合适的时候启动并响应事件。Cocoa和Core Foundation都提供了runloop objects帮助配置和管理线程的RunLoop。每个线程都有关联的RunLoop,主线程启动时自动启动,子线程需要显式地启动。
RunLoop剖析
RunLoop接收两种不同输入事件:输入源(input source)传递异步事件,通常来自于其他线程或程序。定时源(timer source)传递同步事件,发生在特定时间或者重复的时间间隔。这两种源事件到达时都使用对应回调函数处理。
RunLoop模式
RunLoop模式是所有输入源定时源和观察者(CFRunLoopObserverRef实例)的集合。RunLoop每次只以某个模式运行,只有相关的源才会被监视、传递、处理事件,只有相关的观察者会被通知。否则,不相关的源不被监视,不相关观察者不被通知。Cocoa和Core Foundation定了一个默认模式和几个常用模式:
- Default(NSDefaultRunLoopMode-Cocoa , kCFRunLoopDefaultMode-CoreFoundation) 默认模式是最常用的模式。大部分时间你应该用这种模式启动RunLoop配置事件源。
- Connection(NSConnectionReplyMode-Cocoa) Cocoa用这个模式结合NSConnection对象来检测网络响应。你自己很少会用到这个模式。
- Modal(NSModalPanelRunLoopMode-Cocoa) Cocoa用这个模式辨别出模态界面想要的事件。
- Event tracking(NSEventTrackingRunLoopMode-Cocoa) Cocoa用这个模式在MouseDrag循环中和其他需要跟踪用户的界面循环中限制事件输入。
- Common modes(NSRunLoopCommonModes-Cocoa, kCFRunLoopCommonModes-CoreFoundation) 这是一个常用模式的可配置组合。关联常用模式就是关联组合里每个模式。Cocoa程序里常用模式默认包括default
,modal,event tracking。Core Foundation只有default。你可以用CFRunLoopAddCommonMode添加自定义模式到常用模式中。
创建自定义模式必须添加至少一个事件源或者观察者,否则无效。使用特定模式可以过滤事件。比如大多数时间运行在默认模式上,模态界面运行在”Modal”模式下,此时只有模态模式相关信息能传递给线程。
输入源
输入源异步发送消息到线程。事件来源取决于种类:端口源和自定义源。端口源监听程序相应端口,自定义源监听自定义事件源。区别在于如何触发:端口源由内核自动发送,自定义源则需要手动从其他线程发送。
基于端口的输入源
Cocoa不需要直接创建输入源,只需要创建NSPort对象并添加到RunLoop,它会自己处理创建和配置属于远。Core Foundation必须手动创建端口和输入源用CFMachPortRef,CFMessagePortRef,FSocketRef来创建合适的对象。
自定义输入源
创建自定义输入源使用CFRunLoopSourceRef,使用回调函数来进行配置。Core Foundation会在配置、处理、移除调用回调函数。
Cocoa执行Selector源
Cocoa定义了允许你在任何线程执行selector的自定义输入源。和端口源一样,执行selector的请求在目标线程上串行排列,避免许多线程上多个方法一起运行引起的同步问题。不像端口源,一个selector源执行完后会自动从RunLoop里面移除。RunLoop每次循环处理所有(而不是一个)排列的selector的调用。
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
};
通过使用Core Foundation注册为RunLoop的观察者来接收RunLoop行为的通知。和同步或异步事件发生时才触发的事件源不同,RunLoop观察者是在RunLoop本身运行的特定时刻候触发。和定时器类似,RunLoop观察者(在创建时指定)可以只用一次(RunLoop启动后,观察者会从RunLoop里移除)或循环使用。
RunLoop事件队列
何时使用RunLoop
不需要在任何情况下都启动线程的RunLoop。比如,用线程处理一个预先定义的长时间(虽然长)任务就不用。RunLoop只在需更多交互时才需要:
- 使用端口或自定义输入源来和其他线程通信
- 使用线程的定时器
- Cocoa中使用任何performSelector方法
- 使线程周期性工作
获得RunLoop对象
//Cocoa
NSRunLoop cocoa = [NSRunLoop currentRunLoop];
//Core Foundation
CFRunLoopRef coreFoundation = CFRunLoopGetCurrent();
配置RunLoop
子线程运行RunLoop前,必须添加至少一个事件源。如果没有任何事件源,RunLoop会在启动之际立马退出。使用CFRunLoopAddObserver将观察者添加到RunLoop:
- (void)threadMain {
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer) {
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do {
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--; 
} while (loopCount);
}
启动RunLoop
无条件的启动RunLoop最简单的,但也最不推荐。因为这会使线程永久循环,失去控制。虽然可以添加或删除事件源,但是退出RunLoop的唯一方法是杀死它,也没有任何办法可以让RunLoop运行在自定义模式下。
更好的办法是用设置超时时间来启动RunLoop,这样RunLoop运作直到事件到达或者规定时间超时。如果是事件到达,RunLoop会传递消息给相应的处理方法然后退出。如果是规定时间超时直接退出。重新启动RunLoop进行下一次循环。
特定模式启动RunLoop可以限制传递给RunLoop的事件源的类型。
- (void)skeletonThreadMain {
BOOL done = NO;
do {
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) {
done = YES;
}
} while (!done);
}
退出RunLoop
如果可以,尽量使用设置超时时,它让RunLoop退出前完成所有正常操作,包括通知观察者。显式停止(CFRunLoopStop)和超时结果相似,Runloop把所有剩余的通知发送出去再退出。与设置超时的不同的是,显式停止可以在无条件启动的Runloop里面使用。
尽管移除所有事件源也能导致RunLoop退出,但这并不可靠。系统会添加输入源来处理所需事件。因为移除事件源未必会考虑到这些输入源,可能导致无法退出RunLoop。
线程安全和RunLoop对象
线程的否安全取决于API操纵RunLoop的API。Core Foundation的函数是线程安全的,可以被任意线程调用。修改RunLoop配置并执行操作,最好在RunLoop所属线程完成。
Cocoa的NSRunLoop则不具有线程安全性。 如果使用NSRunLoop来修改RunLoop,应该在RunLoop所属线程完成。给其他线程的RunLoop添加事件源可能导致崩溃或产生不可预知的行为。
创建配置自定义输入源
- 输入源要处理的信息。
- 一个联系客户代码和输入源的调度方法。
- 一个处理客户代码请求的执行方法。
- 一个使输入源无效的取消方法。
主线程给子线程分发任务时,主线程给命令缓冲区填充命令和信息(主线程和拥有输入源的子线程必须同步访问命令缓冲区),然后通知(会唤醒)子线程RunLoop开始工作,子线程RunLoop会调用输入源处理函数来执行命令缓冲区中相应的命令。
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
@interface RunLoopSource : NSObject
- (instancetype)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
- (void)sourceFired;
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
@implementation RunLoopSource {
CFRunLoopSourceRef _runLoopSource;
NSMutableArray *_commands;
}
- (instancetype)init {
self = [super init];
if (self) {
CFRunLoopSourceContext context = {
0,
(__bridge void *)(self),
NULL,
NULL,
NULL,
NULL,
NULL,
RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine
};
_runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
_commands = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addToCurrentRunLoop {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
}
- (void)invalidate {}
- (void)sourceFired {}
- (void)addCommand:(NSInteger)command withData:(id)data {}
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop {
CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(runloop);
}
@end
@interface RunLoopContext : NSObject
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
- (instancetype)initWithSource:(RunLoopSource *)src andLoop:(CFRunLoopRef)loop;
@end
@implementation RunLoopContext {
RunLoopSource *_source;
CFRunLoopRef _runLoop;
}
- (instancetype)initWithSource:(RunLoopSource *)src andLoop:(CFRunLoopRef)loop {
self = [super init];
if (self) {
_source = src;
_runLoop = loop;
}
return self;
}
@end
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode) {
id appDelegate = [[NSApplication sharedApplication] delegate];
RunLoopSource *obj = (__bridge RunLoopSource *)info;
RunLoopContext *theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[appDelegate performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:NO];
}
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode) {
id del = [[NSApplication sharedApplication] delegate];
RunLoopSource *obj = (__bridge RunLoopSource *)info;
RunLoopContext *theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(removeSource:) withObject:theContext waitUntilDone:YES];
}
void RunLoopSourcePerformRoutine (void *info) {
RunLoopSource *obj = (__bridge RunLoopSource *)info;
[obj sourceFired];
}
配置定时源
//Cocoa
- (void)addTimer {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(timeFeedback:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timeFeedback:(NSTimer *)timer {}
//Core Foundation
- (void)addTimer {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0.3, 0, 0, &myCFTimerCallback, &context);
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
}
myCFTimerCallback(CFRunLoopTimerRef timer, void *info) {}
RunLoop与线程
Apple不允许直接创建RunLoop,只提供了两个自动获取API:CFRunLoopGetMain()和CFRunLoopGetCurrent(),大致如下:
static CFMutableDictionaryRef loopsDic;
static CFSpinLock_t loopsLock;
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
//注册回调在线程销毁的时候销毁RunLoop
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRun LoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
从代码看出,线程和RunLoop是一一对应的,映射关系保存在一个全局的Dictionary里。线程的RunLoop不自动创建,只有线程主动获取时才创建(第一次调用会确保主线程的RunLoop创建),在对应线程销毁时销毁。
RunLoop的Public API
CoreFoundation有5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
一个RunLoop有若干个Mode,一个Mode有若干个Source|Timer|Observer(统称为Mode Item)。RunLoop每次只能运行在一个Mode中。切换Mode必须先退出运行,再以Mode进入。
CFRunLoopSourceRef分Source0和Source1:Source0只有一个回调函数。需要先CFRunLoopSourceSignal(source)标记带处理,再CFRunLoopWakeUp(runloop)唤醒runloop,以此实现自定义输入源。Source1有一个mach_port和一个回调函数,以此实现基于端口的输入源。
RunLoop的Mode
CFRunLoopMode和CFRunLoop的大致结构:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef>
CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef>
CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef>
CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef>
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set<CFStringRef>
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set<CFRunLoopModeRef>
...
};
通过将Mode的_name添加到RunLoop的_commonModes中使其成为commonMode。当RunLoop内容变化时,RunLoop都会自动将_commonModeItems里的ModeItem同步到所有Common Mode里。
App启动后RunLoop的状态:
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}
common mode items = {
// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}
// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}
// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},
modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},
CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},
CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}
AutoreleasePool
App启动后,Apple在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。第一个Observer监视Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。其优先级最高,保证创建行为发生在其他所有回调之前。第二个Observer监视了两个事件:BeforeWaiting(即将进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()从而释放旧池创建新池;Exit(即将退出Loop) 时调用_objc_autoreleasePoolPop()来释放旧池。其优先级最低,保证释放行为生在其他所有回调之后。RunLoop处理事件过程中的代码被自动释放池的创建和释放环绕着,所以不会出现内存泄漏。
事件响应
App启动后,Apple在主线程RunLoop里注册了一个Source1(基于mach port的)用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先IOKit.framework生成一个IOHIDEvent事件,发送给SpringBoard(接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event),SpringBoard再用mach port转发给对应的App。随后该Source1会被触发并回调_UIApplicationHandleEventQueue(),把IOHIDEvent处理封装成UIEvent分发给UIWindow等。
手势识别
当_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel打断当前touchesBegin/Move/End,随后将对应UIGestureRecognizer标记为待处理。App启动后,Apple在主线程RunLoop注册了一个Observer监测BeforeWaiting(即将进入休眠),其回调内会调用_UIGestureRecognizerUpdateObserver()获取所有待处理的 GestureRecognizer并执行回调。
界面更新
当在操作UI(改变Frame、更新UIView/CALayer层次)或者手动调用UIView/CALayer的setNeedsLayout/setNeedsDisplay后,这个UIView/CALayer被标记为待处理,并被提交到一个全局容器。App启动后,Apple在主线程RunLoop里注册了一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop),其回调内会调用_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()去遍历所有待处理的UIView/CAlayer并执行绘制和调整,最后更新UI界面。
定时器
NSTimer即(toll-free bridged)CFRunLoopTimerRef。一个NSTimer注册到RunLoop时会在未来重复时间点提前注册好事件,不过为了节省资源,并不会非常准确触发。Timer有个属性Tolerance(宽容度)表示时间点容许的最大误差。如果某个时间点被错过了,比如在执行一个长任务,那么对应的回调也会跳过去,不会延后执行。CADisplayLink是一个和屏幕刷新率一致的定时器(和NSTimer不一样,其用Source实现,且原理更复杂)。如果两次屏幕刷新之间在执行一个长任务,那么其中一帧就被跳过(和NSTimer相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会有所察觉。Facebook开源的AsyncDisplayLink(内部也用到RunLoop)就是为了解决界面卡顿的问题。
PerformSelecter
调用performSelector系列方法时,实际都会创建一个Timer并添加到对应线程RunLoop中,如果对应线程没有RunLoop则该方法会失效。
关于GCD
实际上RunLoop底层也会用到GCD,比如RunLoop是用dispatch_source_t实现的Timer。同时GCD的某些接口也用到RunLoop,例如dispatch_async()。当调用dispatch_async(dispatch_get_main_queue(), block)时,libDispatch会向主线程RunLoop发送消息,主线程RunLoop被唤醒并从消息中取得block,在回调__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()里执行这个block。但仅限于dispatch到主线程,dispatch到其他线程仍然是由libDispatch处理的。