理解 iOS RunLoop

RunLoop

Run Loop 是一个事件处理循环,用于调度工作和处理收到的事件。Run Loop 的作用是在有工作要做的时候保持你的线程忙碌,当没有的时候让线程进入睡眠状态。

RunLoop 相关接口

RunLoop Ref

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

NSRunLoop主要接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface NSRunLoop : NSObject {

@property (class, readonly, strong) NSRunLoop *currentRunLoop;
@property (class, readonly, strong) NSRunLoop *mainRunLoop API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@property (nullable, readonly, copy) NSRunLoopMode currentMode;

- (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;

- (nullable NSDate *)limitDateForMode:(NSRunLoopMode)mode;
- (void)acceptInputForMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

@end

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
//CFRunloop.c

CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
// 获取 pthread 对应的 RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
//第一次初始化,并为主线程创建 RunLoop
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//从 Dictionary 获取 pthread 对应的 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 若没有对应 RunLoop,则创建一个
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程只有在第一次获取 RunLoop 时才会创建,线程结束后会销毁 RunLoop。除了主线程只能在一个线程的内部获取其 RunLoop。

RunLoop 的内部结构

下面官方的图给出了RunLoop的内部运行以及两种类型输入源:

RunLoop Structure

在 CoreFoundation 里面关于 RunLoop 有5个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
//CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;

typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

//CFRunLoop.c

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

__CFRunLoop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _ignoreWakeUps;
volatile uint32_t *_stopped;
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};

__CFRunLoopMode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
mach_port_t _timerPort;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
HANDLE _timerPort;
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
};

其中 CFRunLoopModeRef 并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。从上面两个结构体可以看到他们的关系如下:
RunLoop Ref Structure

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

RunLoop 的 Mode

__CFRunLoop结构体中:

1
2
3
4
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

苹果公开的RunLoop Mode:

1
2
3
4
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

UIKIT_EXTERN NSRunLoopMode const UITrackingRunLoopMode;

Mode 通过 ModeName进行区分,一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有 Mode 里。

如主线程的 RunLoop 里有两个预置的 Mode:NSDefaultRunLoopMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。如果想要滑动的时候不影响 Timer,解决办法有两个:

  • 将这个 Timer 分别加入这两个 Mode
  • 将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

RunLoop 的 Source

Input Sources

Input sources 以异步方式向你的线程传递事件。事件的来源取决于 Input sources 的类型,Input sources 包含两种类型:

  • Port-based input sources,监视应用程序的Mach端口
  • Custom input sources,监视事件的自定义来源

这两个类型对应 __CFRunLoopMode 结构体中:

1
2
CFMutableSetRef _sources0; //非基于port
CFMutableSetRef _sources1; //基于port

Source0: event事件,只含有回调,需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop。
Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop 的线程。

这两个来源唯一的区别是它们是如何发出信号的,Port-based input sources 由内核自动发出信号,自定义源必须从另一个线程手动发信号。

Port-Based Sources

Cocoa 和Core Foundation 为 Port-based input sources 提供了内置的支持:

  • Cocoa 中,不需要直接创建 input source,只需要创建 NSPort 对象,并添加到 Run Loop。Port 对象负责 input source 的创建、配置等工作

  • Core Foundation,需要手动创建 port 及其 run loop source

    两种方法都使用CFMachPortRef, CFMessagePortRef, 或者 CFSocketRef来创建对象。

Custom Input Sources

使用 CFRunLoopSourceRef 相关的方法来创建自定义的 input source,并使用回调进行配置。且需要自行定义事件传递机制。

Cocoa Perform Selector Sources

Perform Selector Sources 也属于 Custom Input Sources,由 Cocoa 预先定义。Cocoa 允许你直接调用接口在其他线程直接执行 selector,线程必须有 run loop 且正运行。这些 selector 将按序执行,且 selector source 在 selector 执行完后会将自己从 run loop 中删除。Run loop 在一个循环中会执行所有排队中的 selector。

Timer Sources

Timer Sources 在未来的预设时间将事件同步传递给线程。线程可以通过 Timer 来通知自己做某些事。Timer 的时间并非精确时间,受当前 run loop mode 是否支持该 timer source 以及当前 run loop 是否在执行其他操作影响。

Runloop 的 Observers

Observer 是对 run loop 运行状态事件的跟踪:

  • 进入 run loop
  • run loop 将要处理 timer
  • run loop 将要处理 input source
  • run loop 将要休眠
  • run loop 被唤醒,并且还未处理唤醒它的事件
  • 退出 run loop
1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

我们可以通过 CFRunLoopObserverRef 给程序添加 run loop observers,监听我们感兴趣的状态。我们可以创建一次性的或者重复监听的 observer。

RunLoop 事件顺序

RunLoop 内部逻辑如下:
RunLoop Step

RunLoop 使用

具体实现源码可参考我的GitHub:RunLoopDemo
使用NSRunLoopcurrentRunLoop、或者更为底层的CFRunLoopGetCurrent方法来获取当前线程的 run loop。当运行一个 run loop 之前,必须添加一个 input source 或者 timer,如果什么都没添加,run loop 将直接退出。当然,也可以使用CFRunLoopAddObserver函数将CFRunLoopObserverRef类型的 observer 添加到 run loop。

配置 Run Loop Observer

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
- (void)threadMain
{
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create a run loop observer and attach it to the run loop.
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);
}

// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

NSInteger loopCount = 10;
do
{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}

运行/退出 Run Loop

运行 run loop 的几种方法:

  • 无条件(不推荐)
  • 带时间限制
  • 指定 mode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)skeletonThreadMain
{
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;

// Add your sources or timers to the run loop and do any other setup.

do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;

// Check for any other exit conditions here and set the
// done variable as needed.
}
while (!done);

// Clean up code here. Be sure to release any allocated autorelease pools.
}

退出 run loop 的几种方法:

  • run loop 有时间限制
  • 直接告诉 run loop 结束运行
  • run loop 中没有有效的 input source 或者 timer(不推荐,因为可控性不强)

Custom Input source

创建一个 custom inpust source 需要:

  • 需要处理的信息
  • 给外部调用的调度方法
  • 外部请求处理方法
  • 取消方法用来使 input source 失效

Custom Input Source

引用 apple 部分代码:

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
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}

- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;

// Handler method
- (void)sourceFired;

// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;

@end

// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;

- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (id)init
{
CFRunLoopSourceContext context = {0, 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);
}

初始化CFRunLoopSourceContext的三个回调:

  • RunLoopSourceScheduleRoutineCFRunLoopAddSource触发
  • RunLoopSourceCancelRoutineCFRunLoopSourceInvalidate触发
  • RunLoopSourcePerformRoutineCFRunLoopSourceSignal触发

CFRunLoopSourceSignal将 source 标记为待处理后,还需要调用CFRunLoopWakeUp唤醒线程:

1
2
3
4
5
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}

Timer Sources

Timer Sources 相对比较简单,只需要创建 timer 对象并添加到 run loop。scheduledTimerWithTimeInterval:...接口会创建 timer 并添加到当前 run loop 的 default mode,且无法指定 mode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];

// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];

也可以使用Core Foundation相关接口:

1
2
3
4
5
6
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);

CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

Port-Based Input Sources

官方给的例子中NSMessagePort在iOS中并不支持,这里就不再研究了。

RunLoop 的应用及注意事项

我们唯一需要使用 run loop 的情况是为应用程序创建辅助线程:

  • 使用 ports 或 custom input sources 与其他线程通信。
  • 线程中使用timer
  • 使用performSelector…方法
  • 线程执行定期任务

退出 RunLoop

CFRunLoopStop可以用来结束runloop:

This function forces rl to stop running and return control to the function that called CFRunLoopRun() or CFRunLoopRunInMode(_:_:_:) for the current run loop activation.

但是[NSRunLoop run]的文档说明如下:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

所以CFRunLoopStop无法结束[NSRunLoop run]。且[NSRunLoop run]不能依赖于通过删除 source 或 timer 来控制 run loop 结束。

Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

官方给出示例:

1
2
3
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

performSelector… 和 NSTimer

performSelector...NSTimer依赖 run loop,若当前线程没有 run loop 或 run loop 已经停止,则这两个方法将无效。

Cotin Yang wechat
欢迎订阅我的微信公众号 CotinDev
小小地鼓励一下吧~😘