李峰峰博客

RunLoop 实现原理

2020-07-27

1、RunLoop 的概念

我们都知道,APP 运行过程中有一个很重要的线程,就是主线程。但是,一般线程执行完任务后就会退出,而 APP 需要持续运行,所以就需要一个机制使主线程持续运行并随时处理用户事件,在 iOS 里,程序的持续运行就是通过 RunLoop 实现的。

RunLoop 的作用:

  • 保持程序持续运行;
    程序一启动就会开启一个主线程,主线程开启之后会自动运行一个主线程对应的 RunLoop,RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行;

  • 处理 App 中的各种事件
    比如:触摸事件,定时器事件等;

  • 节省 CPU 资源,提高程序性能
    程序运行起来时,当什么操作都没有做的时候,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop 就会立马起来去做事情;

在 iOS 中,NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,CFRunLoopRef 这些 API 都是线程安全的,Apple 在其文档上对 NSRunLoop 非线程安全的提示:

通常不将 RunLoop 类视为线程安全的,并且只能在当前线程的上下文中调用其方法。永远不要尝试调用在不同线程中运行的 RunLoop 对象的方法,因为这样做可能会导致意外的结果。

CFRunLoopRef 是开源的,源码下载地址:http://opensource.apple.com/tarballs/CF/

为了源码阅读更容易,便于理解 RunLoop 代码关键逻辑,本文所贴出的源码部分有删减,只留下了关键部分。

2、RunLoop 的数据结构

在 CoreFoundation 中,RunLoop 主要有 5 个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

(1) CFRunLoopRef

NSRunLoop 是基于 CFRunLoopRef 封装的,提供了面向对象的 API,接下来看下 NSRunLoop(即 CFRunLoopRef)的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop
{
// ......

// runloop 所对应线程
pthread_t _pthread;

// 存放所有标记为 common 的 mode
CFMutableSetRef _commonModes;

// 存放 common mode item 的集合(source、timer、observer)
CFMutableSetRef _commonModeItems;

// 当前所在 mode
CFRunLoopModeRef _currentMode;

// 存放 mode 的集合
CFMutableSetRef _modes;

// ......
};

根据以上源码可知,RunLoop 也是一个结构体,即 __CFRunLoop,并且可以看到其中几个关键的成员变量:

  • _commonModes
    RunLoop 的内容发生变化时,RunLoop 会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到 _commonModes 中所有 Mode 里。主线程的 RunLoop 中 kCFRunLoopDefaultModeUITrackingRunLoopMode 都已经被标记为“Common”。
    我们可以通过下面方法把一个 Mode 标记为 “Common”:
    CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);

  • _commonModeItems
    添加 mode item 的时候,如果 modeName 传入 NSRunLoopCommonModes,则该 mode item 会被保存到 RunLoop 的 _commonModeItems 中,例如:
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  • _currentMode
    runloop 当前所在 mode

  • _modes
    存放 mode 的集合

也就是说,RunLoop 可以有多个 mode(CFRunLoopModeRef) 对象,但是同一时间只能运行某一种特定的 Mode。

CFRunLoop 对外暴露的管理 Mode 接口只有下面 2 个:

1
2
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

Mode 暴露的管理 mode item 的接口有下面几个:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

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

(2) CFRunLoopModeRef

CFRunLoopModeRef 其实是指向 __CFRunLoopMode 结构体的指针,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode
{
// ...

// mode 的名称
CFStringRef _name;

// mode 是否停止
Boolean _stopped;

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

// ...

};

从以上源码可知,每个 mode 对象中,可以存储多个 source、observer、timer(source/observer/timer 被统称为 mode item)。

系统默认注册了 5 个 Mode:

  • kCFRunLoopDefaultMode
    App的默认 Mode,通常主线程是在这个 Mode 下运行的,是大多数操作使用的模式。一般情况下,使用此模式来启动运行循环并配置输入源。

  • UIInitializationRunLoopMode
    在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

  • UITrackingRunLoopMode
    界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

  • GSEventReceiveRunLoopMode
    接受系统事件的内部 Mode。

  • kCFRunLoopCommonModes
    这是一个占位用的 Mode,作为标记 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 用,并不是一种真正的 Mode

(3) CFRunLoopSourceRef

CFRunLoopSourceRef 是事件源(输入源)。其分为 source0source1

  • source0
    非基于 port 的,接收点击事件,触摸事件等 APP 内部事件,也就是用户触发的事件。这种 source 是不能主动唤醒 RunLoop 的。
    使用时,需要先调用 :
    CFRunLoopSourceSignal(source)
    将这个 Source 标记为待处理,然后再调用:
    CFRunLoopWakeUp(runloop)
    来主动唤醒 RunLoop,让其处理这个事件。

  • source1
    基于 Port 的,能主动唤醒 RunLoop,通过内核和其他线程通信,接收分发系统事件;触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理。

关于 Port 内容后文会进行总结。

CFRunLoopSourceRef 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource
{
// ...

// source 的优先级,值为小,优先级越高
CFIndex _order; /* immutable */

// runloop 集合
CFMutableBagRef _runLoops;

// 联合体,表示 source 是 source0 还是 source1
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;

// ...
};

(4) CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 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
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
};

CFRunLoopObserverRef 源码如下:
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

struct __CFRunLoopObserver
{
// ...

// observer 对应的 runloop
CFRunLoopRef _runLoop;

// observer 观察了多少个 runloop
CFIndex _rlCount;

CFOptionFlags _activities; /* immutable */

// observer 优先级
CFIndex _order; /* immutable */

// observer 回调函数
CFRunLoopObserverCallBack _callout; /* immutable */

// ...
};

typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

例如,监听 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
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
// 把 observer 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

(5) CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,我们常用的 NSTimer 其实就是 CFRunLoopTimerRef ,他们之间是 toll-free bridged 的,可以相互转换。其包含一个时间长度和一个回调(函数指针)。
当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。

总结:
一个 RunLoop 中,只能对应一个线程,但是可以包含多个 Mode,每个 mode,可以包含多个 source、observer、timer,其关系如下:

  • RunLoop 启动时只能选择其中一个 Mode,作为 currentMode。
  • 如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入。
  • 不同 Mode 的 Source0/Source1/Timer/Observer 能分隔开来,互不影响。
  • 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出。

3、RunLoop 的执行流程

当 APP 没有任何任务的时候,RunLoop 会进入休眠,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情。当下次有任务的时候,例如用户点击了屏幕,RunLoop 就会结束休眠开始处理用户的点击事件。所以,为了看到 RunLoop 执行流程,可以在点击事件里加个断点,查看 RunLoop 相关的方法调用栈:

根据上图发现,分析 RunLoop 执行流程,可以从 CFRunLoopRunSpecific__CFRunLoopRun 函数入手,而对 CFRunLoopRunSpecific 函数的调用,可以在源码中找到,是在 CFRunLoopRun 函数中,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void CFRunLoopRun(void) {   /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

由以上源码可知:

  • 默认底层是通过 CFRunLoopRun 开启 RunLoop 的,并且超时时间设置的非常大:1.0e10,可以理解为不超时。
  • 我们也可以通过 CFRunLoopRunInMode 函数设置自定义启动方式,可以自定义超时时间、mode。

然后进入 CFRunLoopRunSpecific 函数,这是 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
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

/// __CFRunLoopRun中 具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

/// 11. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

int32_t retVal = 0;
do {

// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

// 4. 处理block
__CFRunLoopDoBlocks(rl, rlm);

// 5. 处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 如果处理Source0的结果是true
if (sourceHandledThisLoop) {
// 再次处理block
__CFRunLoopDoBlocks(rl, rlm);
}

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

// 6. 如果有Source1 (基于port) 处于ready状态,直接处理这个Source1然后跳转去处理消息。
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 如果有Source1, 就跳转到handle_msg
goto handle_msg;
}

// 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);

// 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程:
// 一个基于 port 的Source 的事件。
// 一个 Timer 到时间了
// RunLoop 自身的超时时间到了
// 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);


__CFRunLoopUnsetSleeping(rl);
// 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);


// 收到消息,处理消息。
handle_msg:;
if (/* 被timer唤醒 */) {
// 01. 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (/* 被gcd唤醒 */) {
// 02. 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Source1唤醒
// 处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
}

// 9. 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

// 10. 设置返回值, 根据不同的结果, 处理不同操作
if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (0 == retVal);

return retVal;
}

由以上源码可知,RunLoop 内部是一个 do-while 循环;当调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop 执行流程可用下面这张图概括:

通过上面的执行流程可以发现,RunLoop 处理了很多次 Block,即调用了很多次 __CFRunLoopDoBlocks,那这里处理的 Block 到底是什么 Block 呢?
前面提到了 __CFRunLoop 结构体中的一些常见成员,其实还有两个和 Block 相关的成员:

1
2
3
4
5
6
7
8
struct __CFRunLoop {
// ...

struct _block_item *_blocks_head; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的头指针
struct _block_item *_blocks_tail; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的尾指针

// ...
};

_blocks_head_blocks_tail 就是用于存放 CFRunLoopPerformBlock 函数添加的 Block 的,可见 RunLoop 是将添加的 Block 任务保存在双向链表中的。

我们可以通过 CFRunLoopPerformBlock 将一个 Block 任务加入到 RunLoop:

1
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void(block)( void));

可以看出添加 Block 任务的时候,是绑定到某个 runloop mode 的。调用上面的 api 之后,runloop 在执行的时候,会通过如下 API 执行对应 mode 中所有的 block:

1
__CFRunLoopDoBlocks(rl, rlm);

需要注意的是,CFRunLoopPerformBlock 不会主动唤醒 RunLoop,添加完 Block 之后可以使用 CFRunLoopWakeUp 来主动唤醒 RunLoop。

4、RunLoop 与线程的关系

CFRunLoop 是基于 pthread 来管理线程的,苹果不允许直接创建 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 获得当前线程的 RunLoop 对象,内部调用 _CFRunLoopGet0 函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 如果为空则t设置为主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// 如果不存在 RunLoop,则创建
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的 RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key 和 RunLoop-Value 保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}

// 从字典里面取 RunLoop:将线程作为 key 从字典里获取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);

// 如果 RunLoop 为空,则创建一个新的 RunLoop,所以 RunLoop 会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

// 创建好之后,以线程为 key,RunLoop 为 value,一对一存储在字典中,下次获取的时候,则直接返回字典内的 RunLoop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// do not release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
// 线程结束是销毁 loop
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 对象即可 [NSRunLoop currentRunLoop]。如果不获取,那子线程就不会创建与之相关联的 RunLoop,并且只能在一个线程的内部获取其 RunLoop。
当通过调用 [NSRunLoop currentRunLoop] 方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop 会被销毁。

Runloop 与线程的关系总结:

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象;
  • RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value
  • 调用 [NSRunLoop currentRunLoop] 方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将对应关系保存到字典里。
  • 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建;
  • RunLoop 在第一次获取时创建,在线程结束时销毁;

5、Runloop 的启动与退出

(1) 创建 Runloop

无法直接创建 RunLoop,但是 RunLoop 在第一次获取时自动创建,获取 RunLoop:

1
2
3
4
5
6
7
8
9
10
11
Foundation
// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop];
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop];

Core Foundation
// 获得当前线程的 RunLoop 对象
CFRunLoopGetCurrent();
// 获得主线程的 RunLoop 对象
CFRunLoopGetMain();

(2) 启动 Runloop

Apple 把 Runloop 启动方式分成了三种:

  • 无条件地(Unconditionally)
  • 有时间限制(With a set time limit)
  • 指定 Mode(In a particular mode)

这三种方式分别对应下面三个方法:

1
2
3
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
  • 第 1 种方式,本质就是在 NSDefaultRunLoopMode 模式下无限循环调用 runMode:beforeDate: 方法,在此期间会处理来自输入源的数据;
  • 第 2 种方式,本质也是在 NSDefaultRunLoopMode 模式下无限循环调用 runMode:beforeDate: 方法,区别在于它达到指定的超时时间后就不会再调用,在此期间会处理来自输入源的数据。
  • 第 3 种方式,Runloop 只会运行一次,达到指定超时时间或者第一个 input source 被处理,则 Runloop 就会退出,这个方法会阻塞当前线程,直到返回结果(YES:输入源被处理或者达到指定的超时值,NO:没有启动成功)。

(3) 退出 Runloop

相较于 Runloop 的启动,它的退出就比较简单了,只有两种方法:

  • 设置超时时间
  • 手动结束

针对前面提到的第 2、3 中启动方式,可以直接设置超时时间控制退出。如果想要手动退出,可以使用下面函数,其参数就是 Runloop 对象:

1
void CFRunLoopStop(CFRunLoopRef rl)

但是 Apple 文档中在介绍利用 CFRunLoopStop() 手动退出时提到:

The difference is that you can use this technique on run loops you started unconditionally.

这里的解释非常容易产生误会,如果在阅读时没有注意到 exit 和 terminate 的微小差异就很容易掉进坑里,因为在 run 方法的文档中还有这句话:

If you want the run loop to terminate, you shouldn’t use this method

也就是说,前面三种 Runloop 启动方式,对应退出方式如下:

  • run
    无法退出

  • runUntilDate:
    只能通过设置超时时间进行退出

  • runMode:beforeDate:
    可以通过设置超时时间或者使用 CFRunLoopStop 方法来退出

CFRunLoopStop() 函数只会结束当前的 runMode:beforeDate: 调用,而不会结束后续的调用,这也就是为什么 Runloop 的文档中说 CFRunLoopStop() 可以 exit(退出) 一个 Runloop,而在 run 等方法的文档中又说这样会导致 Runloop 无法 terminate(终结)。

如果既让 Runloop 长时间运行,又要在必要时刻手动退出 Runloop,Apple 官方文档提供了推荐方式:

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

在对应线程中通过如下逻辑退出 Runloop:

1
2
shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());

6、RunLoop 的底层实现

(1) RunLoop 与 mach port

Apple 将 iOS 系统大致划分为下面 4 个层次:

Darwin 的架构如下:

Darwin 是 macOS 和 iOS 操作环境的操作系统部分,Darwin 是一种类 Unix 操作系统(即 Unix 衍生出的系统,在一定程度上继承了原始 Unix 特性),Darwin 的内核是 XNU,XNU 是 Apple 开发的用于 macOS、iOS、tvOS、watchOS 操作系统的内核,XNU 是 X is Not Unix 的缩写。它是一个宏内核 BSD 与微内核 Mach 混合内核,以期将两者的特性兼收并蓄,同时拥有两种内核的优点。

关于 iOS 系统架构相关更多内容,可以看下我的这篇博客:《深入解析 iOS 系统架构》

Mach:
Mach 是一个由卡内基梅隆大学开发的计算机操作系统微内核,Mach 核心之上可平行运行多个操作系统,XNU 内核以一个被深度定制的 Mach 内核作为基础。Mach 提供了诸如处理器调度、IPC (进程间通信)等少量且不可或缺的基础 API。在 Mach 中,所有东西都是“对象”,进程(在 Mach 中称为任务)、线程和虚拟内存都是对象。但是,在 Mach 架构中,对象间不能相互调用,对象间通信只能通过消息传递。“消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

BSD:
XNU 中的 BSD 代码来自 FreeBSD 内核,FreeBSD 是一种开放源代码的类 Unix 的操作系统,基于 BSD Unix 的源代码派生发展而来。BSD 层确保了 Darwin 系统的 UNIX 特性,真正的内核是 Mach,但是对外部隐藏。BSD 提供了更高层次的抽象 API,例如:基于 Mach 的任务之上的 Unix 进程模型、文件系统、网络协议栈等相关 API。

I/O Kit:
I/O Kit 为设备驱动提供了一个面向对象(C++)的一个框架,框架提供每种设备驱动需要的常见特性,以使驱动程序可以用更少的时间和代码完成。

用户态与内核态:
内核控制着操作系统最核心的部分,为了防止应用程序崩溃而导致的内核崩溃,内核与应用程序之间需要进行严格的分离。基于软件的分离会产生巨大的开销,因此现代的操作系统都是依靠硬件来分离。分离的结果就是用户态与内核态。

用户态和内核态的切换有两种类型:

  • 自愿转换:比如系统调用;
  • 非自愿转换:当发生异常、中断或处理器陷阱的时候,代码的执行会被挂起,并且保留发生错误时候的完整状态。控制权被转交给预定义的内核态错误处理程序或中断服务程序。

在 XNU 中,系统调用有四种类别:

  • BSD 系统调用
  • Mach 陷阱
  • 机器相关调用
  • 诊断调用

Mach 消息的发送和接收都是通过同一个 API 函数 mach_msg() 进行的,这个函数在用户态和内核态都有实现。mach_msg() 函数调用了一个 Mach 陷阱(trap),在用户态调用 mach_msg_trap() 会引发陷阱机制,切换到内核态,在内核态中,内核实现的 mach_msg() 会完成实际的工作,如下图:

RunLoop 的核心就是基于 mach port 的,其进入休眠时调用的函数是 mach_msg(),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。

前面提到的 source1 就是基于 mach port 的,它用来接收系统事件。当对应系统事件发生后(例如用户点击了屏幕),最终会通过 mach port 将事件转发给需要的 App 进程。随后苹果注册的那个 source1 就会触发回调,RunLoop 被唤醒,APP 开始处理对应事件。

(2) RunLoop 输入源

Runloop 作为线程的入口用来响应传入事件,Runloop 从两种不同类型的源接收事件:

  • 输入源(Input Source)
    用于传输异步事件,通常是来自另一个线程或者其他程序的消息。输入源将异步事件传递给相应的处理程序,并调用 runUntilDate: 方法(在线程的关联 NSRunLoop 对象上调用)退出。

  • 定时器源(Timer Source)
    提供同步事件,预定的时间或者固定的时间间隔重复执行,计时器源将事件传递给其处理程序,但不会导致 Runloop 退出。

输入源(Input Source)
创建输入源时,可以将其分配给 Runloop 的一种或多种 mode。一般情况下应该在默认模式下运行 Runloop,但也可以指定自定义 mode。如果输入源不在当前监视的 mode 下,则它生成的任何事件都将保留,直到 Runloop 以正确的 mode 运行,输入源主要有:基于的端口的输入源、自定义输入源、Perform Selector 源。

基于的端口的输入源(Port-based Source)
监听应用程序的 Mach 端口,由内核自动发出信号,对应源码中的 source1

Cocoa 和 Core Foundation 都提供了创建基于的端口输入源相关的对象和函数,如果使用 Cocoa 提供的相关方法,不需要直接创建输入源,可以使用 NSPort 相关的方法来创建一个 Port 对象,并将该对象添加到 Runloop 中,该 Port 对象会负责创建和配置输入源。使用 Core Foundation 函数实现稍微复杂些,我们需要手动的创建 Port 和它的 Runloop 源。使用 CFMachPortRef, CFMessagePortRef, 或者 CFSocketRef 函数来创建适当地对象。

例如:

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
- (void)testsource1 {
// 声明两个端口
NSPort *mainPort = [NSMachPort port];
NSPort *threadPort = [NSMachPort port];
// 设置线程的端口的代理回调为自己
threadPort.delegate = self;

// 给主线程 Runloop 加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子线程
// 给子线程添加一个 Port,并运行子线程中的 Runloop
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

});

// 2 秒后,从主线程向子线程发送一条消息
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
// 发送一条消息
// 参数:date(发送时间)、msgid(消息标识)、components(发送消息附带参数)、reserved(预留参数,暂时用不到)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

});
}

// 这个 NSMachPort 收到消息的回调,注意这个参数,可以先给一个id
- (void)handlePortMessage:(id)message
{
NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
}

打印结果:

1
2
RunLoopTest[10368:5612468] 收到消息了,线程为:<NSThread: 0x6000015acf80>{number = 6, name = (null)}
RunLoopTest[10368:5612468] hello

自定义输入源(Custom input Source)
监听自定义事件源,必须从另一个线程手动发信号通知自定义源,对应源码中的 source0
可以使用 CoreFoundationCFRunLoopSourceRef 相关的函数来创建自定义输入源,可以使用多个回调函数配置自定义输入源,CoreFoundation 在必要时候调用这些函数来配置 source,处理传入的事件,并在从 Runloop 中移除 source 时将其移除。
除此之外,还需要定义事件的传递机制,这部分是运行在单独的线程上,负责向输入源提供数据并在适当的时候发出信号,事件的传递机制可自行定义。

例如:

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
@implementation ViewController{
CFRunLoopRef _runLoopRef;
CFRunLoopSourceRef _source;
CFRunLoopSourceContext _source_context;
}

- (void)viewDidLoad {
[super viewDidLoad];
[self testsource0];
}

- (void)testsource0 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{

NSLog(@"starting thread.......");

_runLoopRef = CFRunLoopGetCurrent();
// 初始化_source_context。
bzero(&_source_context, sizeof(_source_context));
// 这里创建了一个基于事件的源,绑定了一个函数
_source_context.perform = fire;
//参数
_source_context.info = "hello";

// 创建一个source
_source = CFRunLoopSourceCreate(NULL, 0, &_source_context);

// 将 source 添加到当前 RunLoop 中去
CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);

// 开启 RunLoop 第三个参数设置为 YES,执行完一次事件后返回
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);

NSLog(@"end thread.......");
});


dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

if (CFRunLoopIsWaiting(_runLoopRef)) {
NSLog(@"RunLoop 正在等待事件输入");
// 添加输入事件
CFRunLoopSourceSignal(_source);
// 唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
CFRunLoopWakeUp(_runLoopRef);
}else {
NSLog(@"RunLoop 正在处理事件");
// 添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
CFRunLoopSourceSignal(_source);
}
});

}

static void fire(void* info) {
NSLog(@"我现在正在处理后台任务");
printf("%s",info);
}

@end

打印结果:

1
2
3
4
RunLoopTest[10630:5649707] starting thread.......
RunLoopTest[10630:5649457] RunLoop 正在等待事件输入
RunLoopTest[10630:5649707] 我现在正在处理后台任务
RunLoopTest[10630:5649707] end thread.......

Perform Selector 源(Cocoa Perform Selector Source)
除了基于端口的源外,Cocoa 还定义了一个自定义输入源,允许在任何线程上 Perfrom Selector。与基于端口的源一样,Perfrom Selector 请求在目标线程上序列化,缓解了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同的是,Perform Selector 源在 Perfrom Selector 后会从 Runloop 中删除自己。

在另一个线程上 Perfrom Selector 时,目标线程必须具有活动的 Runloop。这意味对于我们创建的子线程,需要显式创建 Runloop。由于主线程的 Runloop 是自动创建的,所以可以在 applicationDidFinishLaunching:方法后随时 Perfrom Selector。Runloop 每次循环时,Runloop 都会处理所有的 Perfrom Selector 调用,而不是在每次循环时都只处理一个。

在其他线程上 Perfrom Selector 的相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在主线程的下一个 Runloop 周期中,执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

// 对拥有 NSThread 对象的任何线程执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

// 在下一个 Runloop 周期和指定时长延迟(可选)后,在当前线程上执行指定的选择器。由于它要等到下一个 Runloop 周期来执行选择器,所以这些方法提供了一个来自当前执行代码的自动最小延迟,多个选择器时会按照顺序依次执行。
// performSelector:afterDelay: 方法内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

// 取消使用 performSelector:withObject:afterDelay: 或 performSelector:withObject:afterDelay:inModes: 发送的到当前线程的消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

计时器源(Timer Source)
定时器源在预定时间内同步地将事件传递给线程,定时器可以让线程在对应时刻通知自己执行一些事务,尽管定时器是基于时间的通知方式,但是并不是真的时间机制。就像输入源,定时器在 Runloop 中也是和特定 Mode 相关联的。如果定时器没有处在 Runloop 正在监视的 Mode 中的话,该定时器是不会触发的。必须要等到 Runloop 在定时器支持的 Mode 中运行时,该定时器才会正常运行。如果定时器被触发时机正好是在 Runloop 执行任务中,那么这个定时器源的相关事件只有在 Runloop 下一次运行循环时才能被执行。如果 Runloop 停止运行,那么该定时器源的事件将永远没办法执行。

反复执行的定时器会根据它的触发时间自动配置,并不是真实的触发时间。例如,一个定时器设置的是每 5 秒触发一次,在真实时间上可能是有点延迟的,如果真实时间的延迟大于定时器触发时间的话,那么这次触发时机将被错过。

创建定时器源,可以使用下面两个方法:

1
2
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

上面这两个方法创建了定时器并添加到当前线程的默认 mode (NSDefaultRunLoopMode)中,但是也可以通过 NSRunLoop 的下面的实例方法来将 NSTimer 对象添加到其他 mode 中:
addTimer:forMode:

例如,下面两种实现方式效果是一样的:

1
2
3
4
5
6
7
/// 分开处理,我们可以通过更多的自定义方式来处理timer,比如添加到不同的NSDefaultRunLoopMode。
NSDate *futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer *myTimer = [[NSTimer alloc] initWithFireDate:futureDate interval:0.1 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];

/// 将创建和调度同时进行
[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];

7、RunLoop 目前的应用

(1) AutoreleasePool

  • RunLoop 的进入的时候会调用 objc_autoreleasePoolPush() 创建新的自动释放池。
  • RunLoop 的进入休眠的时候会调用 objc_autoreleasePoolPop()objc_autoreleasePoolPush() 销毁自动释放池、创建一个新的自动释放池。
  • RunLoop 即将退出时会调用 objc_autoreleasePoolPop() 释放自动自动释放池内对象。

(2) 事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

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

(3) 手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop 即将进入休眠) 事件,这个 Observe r的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

(4) 界面更新

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

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];

(5) 定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

(6) PerformSelector

当调用 NSObjectperformSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

performSelector:withObject: 只是发消息,不会有 Timer ,所以不会有上面的问题,在子线程调用,不需要开启 Runloop

(7) 关于 GCD

根据前面 RunLoop 的执行流程可以知道,GCD 也是可以唤醒 RunLoop 的,GCD 由子线程返回到 主线程,只有在这种情况下才会触发 RunLoop,会触发 RunLoop 的 Source 1 事件:

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"main queue");
});

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop 会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

(8) 关于网络请求

iOS 中,关于网络请求的接口有如下几层:

CFSocket 是最底层的接口,只负责 socket 通信。

  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。

  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。

+ NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking 2 和 Alamofire 工作于这一层。

下面主要介绍下 NSURLConnection 的工作过程。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了 4 个 Source0 (即需要手动触发的 Source)。其中 CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.CFSocket.privatecom.apple.NSURLConnectionLoader。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate

NSURLConnectionLoader 中的 RunLoop 接收来自底层 CFSocketSource1 通知。当收到通知后,在合适的时机向 Delegate 线程 RunLoop 发送 CFMultiplexerSourceSource0 通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。接收到 CFMultiplexerSource 通知后,Delegate 线程的 RunLoop 执行对应 Delegate 回调。

8、Runloop 开发中的使用场景

唯一需要显式运行 Runloop 是在创建子线程时。主线程的 Runloop 已自动创建并运行。对于子线程,需要自行判断是否需要 Runloop,如果需要,则开发者自行创建。例如,下列操作需要启动 Runloop:

  • 使用端口或自定义输入源与其他线程通信。
  • 在子线程上使用计时器。
  • 调用 performSelector… 相关方法。
  • 线程保活,以执行周期性任务。

以下是在实际开发是,Runloop 的一些使用场景:

(1) 线程保活

当子线程中的任务执行完毕后,线程就被会被立刻销毁。如果 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
KeepAliveThread.h
typedef void (^KeepAliveThreadTask)(void);

@interface KeepAliveThread : NSObject

// 在子线程执行任务
- (void)executeTask:(KeepAliveThreadTask)task;

// 结束线程
- (void)stop;

@end

KeepAliveThread.m
@interface KeepAliveThread()

@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL shouldKeepRunning;

@end

@implementation KeepAliveThread

#pragma mark - Public methods
- (instancetype)init {
if (self = [super init]) {
self.shouldKeepRunning = YES;

__weak typeof(self) weakSelf = self;

self.thread = [[NSThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && weakSelf.shouldKeepRunning) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];

[self.thread start];
}
return self;
}

- (void)executeTask:(KeepAliveThreadTask)task {
if (!self.thread || !task) return;
[self performSelector:@selector(p_executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}

- (void)stop {
if (!self.thread) return;

[self performSelector:@selector(p_stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)dealloc {
[self stop];
}

#pragma mark - Private methods
- (void)p_stop {
self.shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}

- (void)p_executeTask:(KeepAliveThreadTask)task {
task();
}

@end

(2) 保证 Timer 正常运行

创建 Timer 有下面两种方式,两种实现方式是等价的:

1
2
3
4
5
6
7
// 方式 1
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];

// 方式 2
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

当滑动 UIScrollView 时,主线程的 RunLoop 会切换到 UITrackingRunLoopMode 这个 Mode,执行的也是 UITrackingRunLoopMode 下的任务(Mode 中的 item),而 Timer 是添加在 NSDefaultRunLoopMode 下的,所以 Timer 任务并不会执行,只有当 UITrackingRunLoopMode 的任务执行完毕,Runloop 切换到 NSDefaultRunLoopMode 后,才会继续执行 Timer。解决方法很简单,我们只需要在添加 Timer 时,将 mode 设置为 NSRunLoopCommonModes 即可:

1
2
3
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];

如果是在子线程中使用 Timer,由于子线程的 Runloop 并不会自动创建,所以必须在子线程中创建并启动 Runloop,否则 Timer 无法正常运行,创建并启动 Runloop 方法:

1
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

由于子线程中不会涉及到 UI 更新,所以无需再主动将 Timer 添加到 NSRunLoopCommonModes 下。

(3) 利用 Runloop 优化 UITableView 加载图片时滑动卡顿问题

UITableView 滚动时,主线程的 Runloop 会切换到 UITrackingRunLoopMode 这个 Mode,我们可以在 NSDefaultRunLoopMode 中设置图片,避免一边滑动一边设置 image 引起的卡顿问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = ...
// ......
// 在 NSDefaultRunLoopMode 下设置图片
[self performSelector:@selector(p_loadImgeWithIndexPath:)
withObject:indexPath
afterDelay:0.0
inModes:@[NSDefaultRunLoopMode]];
// ......
return cell;
}

- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{

UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

// 子线程下载图片
[ImageDownload loadImageWithUrl:@"xxxx" success:^(UIImage *image) {
// 回到主线程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
}];
}

(4) 利用 Runloop 监控卡顿

根据 Runloop 的执行流程可以发现,Runloop 对我们业务逻辑的处理时间在两个阶段:

  • kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 之间
  • kCFRunLoopAfterWaiting 之后

所以,如果主线程 Runloop 处在 kCFRunLoopBeforeSources 时间过长,也就是迟迟无法将任务处理完成,顺利到达 kCFRunLoopBeforeWaiting 阶段,说明发生了卡顿。

同样的,如果 Runloop 处在 kCFRunLoopAfterWaiting 时间过长,也是发生了卡顿。

所以,如果我们要利用 Runloop 来监控卡顿的话,就要关注 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 两个阶段,一般卡顿时间超过 250ms 会被明显感知,所以,可以以连续 5 次卡顿时长超过 50ms 可以认为发生卡顿,或者根据需要调整统计阀值。以下是通过 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
@interface LagMonitor() {
int timeoutCount;
CFRunLoopObserverRef runLoopObserver;
@public
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopActivity runLoopActivity;
}

@end

@implementation LagMonitor

#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 {
NSLog(@"开始监控");
if (runLoopObserver) {
return;
}

// 创建信号量,注意这里信号量为 0
dispatchSemaphore = dispatch_semaphore_create(0);
// 创建 Observer
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
runLoopObserverCallBack,
&context);
// 将 Observer 添加到主线程的 RunLoop
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

// 在子线程持续监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
/**
信号量 P 操作,成功的话信号量会 - 1,这里超时时间为 50ms,即等待 50ms 后还没成功就返回失败
操作成功,信号量 - 1,返回值为 0;操作失败,返回值非 0
由于初始信号量为 0,这里会阻塞,直到 runLoopObserverCallBack 函数中对信号量做了 V 操作,即 RunLoop 状态发生改变的时候。
*/
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
// 发生超时,说明 RunLoop 保持在一个状态的时间超过了 50ms
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
// 如果是在 BeforeSources 或 AfterWaiting 这两个状态持续时间达到超时时间,就代表发生了卡顿
if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
// 出现五次出结果
if (++self->timeoutCount < 5) {
continue;
}
// 发生了卡顿,可以使用 PLCrashReporter 等收集堆栈并上报
NSLog(@"发生了卡顿,");
}
}
self->timeoutCount = 0;
}
});

}


// 结束监控
- (void)endMonitor {
NSLog(@"结束监控");
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
// 释放 Observer
CFRelease(runLoopObserver);
runLoopObserver = NULL;
}

#pragma mark - Private
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
LagMonitor *lagMonitor = (__bridge LagMonitor*)info;
lagMonitor->runLoopActivity = activity;

dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
// 对信号量进行 V 操作,信号量 + 1
dispatch_semaphore_signal(semaphore);
}

@end

上面只是统计卡顿的基础版本,如果真的使用到项目中上面逻辑还有不少需要优化的地方,例如:

  • 避免多次重复上报同一个卡顿堆栈
    • 可以先将堆栈保存到内存中,以堆栈栈顶函数为特征,如果相同认为整个堆栈是同一个,不重复上报。
  • 准确定位真正卡顿的堆栈
    • 假如主线程有三个任务,只有第一个是引起卡顿的任务,当开始上报卡顿时获取到的堆栈可能是后两个不耗时的任务的堆栈。这种情况可以每 50ms 甚至更短时间获取一次堆栈,只保留最近一定数量(例如最近 20 个)堆栈信息,当发生卡顿时相同堆栈数量最多的堆栈就是真正引起卡顿的堆栈。

目前也有一些比较成熟的卡顿监控方案,例如:matrix