runloop

深入理解 runloop

本文参考了多位前辈的文章、视频和源码进行学习、以及总结加深理解。

  • iOS线下分享《RunLoop》by 孙源@sunnyxx
  • 深入理解RunLoop
    RunLoop 是 iOS 和 OSX 开发中非常基础的一个概念。runloop是与线程相关的基础架构的一部分。runloop是指用于安排工作,并协调接收传入事件的事件处理循环。runloop的目的是在有工作时保持线程忙,并在没有工作时时让线程进入休眠状态。本文从源码入手,理解 runloop 原理,以及相关自动释放池、延迟回调、触摸事件、屏幕刷新等功能。

RunLoop 概念

一般来讲,一个线程一次只执行一个任务,任务执行完成后线程就退出了,runloop 就是能让线程保持随时能处理任务但不退出的一个机制。这种机制就是 Event Loop 模型,实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。这种说法比较抽象,下面会结合开发过程中使用到的例子来配合源码进行理解。

RunLoop 和线程

每个线程会对应一个 RunLoop ,当你首次在线程(非主线程)中访问 RunLoop 时会自动创建一个 RunLoop ,线程销毁时销毁对应 Runloop,而主线程的 RunLoop 在main函数中会一直保持运行状态。
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

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
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}

/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

OSSpinLockUnLock(&loopsLock);
return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

RunLoop 机制

主线程几乎所有的函数都是从下面的6个函数中发起的。

1
2
3
4
5
6
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

Source0 示例

  • CFRunloop 和 Thread 是一一对应,但是 CFRunloop 自身可以支持嵌套结构。
  • RunloopMode:runloop 必须在某种 mode 下执行
  • CFRunloopTimer:NSTimer,performSelector:afterDelay, CADisplayLink(和系统刷新频率一致)
  • CFRunloopSource: Source是runloop 的数据源(protocol),定义了两个 Version 的 Source
    1: Source0 用来管理 App内部事件、App 自身负责管理(如 UIEnvent、CFSocket)
    2: Source1 由 runloop 和内核管理,mach port(轻量级得进程间通讯的方式) 驱动如 CFMachPort、CFMessagePort。
    3:有需要的话可以从中选择一个 Source 自己实现
  • CFRunloopOberver:向外部报告 Runloop 得状态变化,cocoa 中很多机制都是由 CFRunloopOberver触发,如 CAAnimation, AutoRelease
1
2
3
4
5
6
7
8
9
10
    /* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer事件
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source事件
kCFRunLoopBeforeWaiting = (1UL << 5),//即将休眠
kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒
kCFRunLoopExit = (1UL << 7),//退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

runloop 应用

RunloopMode

  • NSDefaultRunLoopMode,默认情况下使用
  • NSConnectionReplyMode, 开发者一般用不到,系统用来处理NSConnection相关事件
  • NSModalPanelRunLoopMode, 处理 modal panels 事件
  • UITrackingRunLoopMode, 用于处理拖拽和用户交互的模式
  • NSRunloopCommonModes: 集合模式,默认包括 Default,Modal,Event Tracking 三大模式,可以处理几乎所有事件
  • 应用场景举例:主线程的 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 里去。另外一种方法是为 timer 单独开启一个线程,和主线程 runloop 互不影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法1
//可以将 timer 分别加入到这两个 mode
[[NSRunLoop currentRunLoop] addTimer: self.timer forMode:UITrackingRunLoopMode];
// 方法2
// 将 timer 加入到 NSRunloopCommonModes 中。
[[NSRunLoop currentRunLoop] addTimer: self.timer forMode:NSRunLoopCommonModes];

// 方法3
// 将 timer 放到另一个线程中,然后开启另一个线程的 runloop,这样可以保证与主线程互不干扰,而现在主线程正在处理页面滑动
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handlTimer2:) userInfo:nil repeats:YES];
[[NSThread currentThread] setName:@"timerThread"];
[[NSRunLoop currentRunLoop] run];
});

AutoreleasePool

CFRunloopOberver 在 Runloop 两次 Sleep 之间对 AutoreleasePool 进行了 Push 和 Pop。也就是说触发了 CFRunloopOberver 监听会对 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,优先级最低,保证其释放池子发生在其他所有回调之后。
测试代码:

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
//添加observer监听

- (void)testRunloop {
//监听runloop
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), runLoopObserver, kCFRunLoopCommonModes);

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(handlTimerRunloop) userInfo:nil repeats:NO];

}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"进入RunLoop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"被唤醒");
break;
case kCFRunLoopExit:
NSLog(@"退出RunLoop");
break;
default:
break;
}

// NSLog(@"-----> activity is %lu", activity);
// NSLog(@"-----> runloop mode is %@", [[NSRunLoop currentRunLoop] currentMode]);
}

给 _wrapRunLoopWithAutoreleasePoolHandler 添加断点测试发现。
在进入Runloop之前(截图不好体现,可亲测)、即将休眠和退出Runloop后断点都进入了。

待续…