RunLoop
获取 RunLoop
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。
RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。
你只能在一个线程的内部获取其 RunLoop(主线程除外)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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); _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); }
OSSpinLockUnLock(&loopsLock); return loop; }
CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); }
CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }
|
结构
runloop 的相关类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
mode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct __CFRunLoopMode { CFStringRef _name; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; ... };
struct __CFRunLoop { CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; ... };
|
一个 Mode,通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中,来标记自己为 common。
每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有 Mode 里。
所以 common 并不是一个真正的 mode。
RunLoop 的内部逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); }
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); if (__CFRunLoopModeIsEmpty(currentMode)) return;
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO; int retVal = 0; do {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); __CFRunLoopDoBlocks(runloop, currentMode);
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); __CFRunLoopDoBlocks(runloop, currentMode);
if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; }
if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); }
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); }
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
handle_msg:
if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) }
else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); }
else { CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } }
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) { retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) { retVal = kCFRunLoopRunFinished; }
} while (retVal == 0); }
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }
|
apple 使用 runloop
apple 注册的 runloop mode
app 启动后的 runloop 状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| CFRunLoop { current mode = kCFRunLoopDefaultMode common modes = { UITrackingRunLoopMode kCFRunLoopDefaultMode }
common mode items = {
CFRunLoopSource {order =-1, { callout = _UIApplicationHandleEventQueue}} CFRunLoopSource {order =-1, { callout = PurpleEventSignalCallback }} CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}}
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}}
CFRunLoopObserver {order = -2147483647, activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler} CFRunLoopObserver {order = 0, activities = 0x20, callout = _UIGestureRecognizerUpdateObserver} CFRunLoopObserver {order = 1999000, activities = 0xa0, callout = _afterCACommitHandler} CFRunLoopObserver {order = 2000000, activities = 0xa0, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} CFRunLoopObserver {order = 2147483647, activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler}
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 = { }, sources1 = { }, observers = { }, timers = { }, },
CFRunLoopMode { sources0 = { }, sources1 = { }, observers = { }, timers = { }, },
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), } } }
|
系统默认注册了 5 个 Mode:
kCFRunLoopDefaultMode: App 的默认 Mode,通常主线程是在这个 Mode 下运行的。
UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
AutoreleasePool
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 了。
事件响应
苹果注册了一个 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 时,即使一帧的卡顿也会让用户有所察觉。
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop 会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。
但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
RunLoop 的运用
创建一个长时间存活的线程
我们会经常把一些耗费时间的操作放在子线程处理,不影响主线程 UI 更新。
子线程会在处理完任务后进行销毁
如果需要经常在子线程里执行任务,创建销毁线程会造成资源浪费,使用 RunLoop 来创建一个常驻的子线程会更高效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } }
+ (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; }
|
界面滑动时保证 NSTimer 正常运行
1 2 3 4 5 6 7
| NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(foo) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; [timer fire];
|
保证界面流畅
AsyncDisplayKit
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:
在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
RunLoop 监测主线程卡顿
在主线程的 RunLoop 中添加一个 observer,检测 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 的时间并记录。
连续 3 次超时 80ms 认为卡顿
戴铭-GCDFetchFeed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| @interface SMLagMonitor() { int timeoutCount; CFRunLoopObserverRef runLoopObserver; @public dispatch_semaphore_t dispatchSemaphore; CFRunLoopActivity runLoopActivity; } @property (nonatomic, strong) NSTimer *cpuMonitorTimer; @end
@implementation SMLagMonitor
#pragma mark - Interface + (instancetype)shareInstance { static id instance = nil; static dispatch_once_t dispatchOnce; dispatch_once(&dispatchOnce, ^{ instance = [[self alloc] init]; }); return instance; }
- (void)beginMonitor { self.isMonitoring = YES; self.cpuMonitorTimer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(updateCPUInfo) userInfo:nil repeats:YES]; if (runLoopObserver) { return; } dispatchSemaphore = dispatch_semaphore_create(0); CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL}; runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (YES) { long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, STUCKMONITORRATE * NSEC_PER_MSEC)); if (semaphoreWait != 0) { if (!runLoopObserver) { timeoutCount = 0; dispatchSemaphore = 0; runLoopActivity = 0; return; } if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) { if (++timeoutCount < 3) { continue; }
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSString *stackStr = [SMCallStack callStackWithType:SMCallStackTypeMain]; SMCallStackModel *model = [[SMCallStackModel alloc] init]; model.stackStr = stackStr; model.isStuck = YES; [[[SMLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}]; }); } } timeoutCount = 0; } });
}
- (void)endMonitor { self.isMonitoring = NO; [self.cpuMonitorTimer invalidate]; if (!runLoopObserver) { return; } CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes); CFRelease(runLoopObserver); runLoopObserver = NULL; }
#pragma mark - Private - (void)updateCPUInfo { [SMCPUMonitor updateCPU]; }
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info; lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore; dispatch_semaphore_signal(semaphore); }
|
参考
深入理解 RunLoop by:ibireme