RunLoop学习笔记

最近开发中遇到了一些问题,再次又看起了RunLoop的相关知识,发现“纸上得来终觉浅”,还是得看看源码。首先整理一些比较好的RunLoop概念介绍和入门的文章:

sunnyxx线下Runloop分享会

《深入理解RunLoop》 Garan no Dou

基本概念

一个视频一篇文章,基本就能对RunLoop的运作机制有个大致了解了,总的来说:

1.RunLoop通常和线程是一对一的关系(一个线程中只能有一个RunLoop,但这个RunLoop中允许有子RunLoop的存在),这点很好理解,因为从源码上来看,RunLoop就是一个do-while的阻塞线程的循环,因此不可能在同一线程中同时有两个RunLoop。

2.iOS中,许多事件管理、分发都与跑在主线程的RunLoop有关(NSTimer的实现,UI绘制的调用,用户交互事件的响应等等),RunLoop把这些事件分为:Timer事件、Source事件(包含source0、source1,source0可以理解为用户级事件,如触摸事件,是不能唤醒RunLoop的,source1是系统级的事件,RunLoop在休眠时监听特定的port,当这些port有消息传来时,RunLoop就会被唤醒。)

3.RunLoop本身是一个do-while循环,但其运行机制还有一个概念,叫RunLoopMode,上述的timer、source事件,都要在指定RunLoopMode下才能执行,RLM可以理解为:在每循环中,只处理特定mode的事件,如果要处理其他mode下的事件,则要退出循环,切换mode,再重新开启循环(重启RunLoop),RLM本身是一个很好理解的概念,但有一个比较特殊的Mode叫做CommonMode(NSRunLoopCommonModes),这个mode并不是一个具体的mode,而是一个”属性”,当一个事件源(timer、source)将自己标记为”Common”时,他可以在所有有”Common”标记的mode中执行,反之,只有有”Common”标记的mode,才可以执行有”Common”标记的事件源。

在开发中,最常遇到的问题就是NSTimer需要在NSRunLoopCommonModes中跑,才能保证不论是普通状态还是滑动状态,都能被执行,这种情况就是上述所说的,一个事件源在CommonModes下可以执行的情况。

4.Observer,这个比较好理解,和KVO差不多,RunLoop在干各种事情之前会通知自己的观察者,具体枚举有哪些事件网上一查就有了。

看看源码

我看的这个CF的版本是CF-1153.18,不同版本有一些略微的差异,但核心逻辑是一样的。

整个RunLoop的do-while离不开两个东西:CFRunLoop和CFRunLoopMode,所以先来看看这两个东西的定义:

CFRunLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

水平有限,所以只能简单讲一下自己看得懂的部分:

wakeUpPort就是当RunLoop休眠时,把它唤醒的那个有source1消息传来的port;
_commonModes,当一个RLM将自己标记为“Common”时,RunLoop就会把这个RLM放到这个Set中;
__commonModeItems则是那些指定可以在CommonModes下执行的事件集合;
__currentMode是当前跑着的mode,modes是所有可以跑的mode集合;
_blocks_head_blocks_tail共同维护一个事件链表,所有提交到RunLoop的事件都以block的方式存储在RunLoop中,struct _block_item的结构非常简单:

1
2
3
4
5
6
struct _block_item {
struct _block_item *_next;
CFTypeRef _mode; // CFString or CFSet
void (^_block)(void);
};

就是一个对具体代码块block的链式封装。

CFRunLoopMode

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
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 USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};

RLM的定义稍微长一点,
_name就是RLM的标识符了,RunLoop通过这个字段来区别不同的RLM;

_sources0,_sources1_timers,两个source集合,一个timer集合,对应source0、source1事件和计时器事件;

_portToV1SourceMap,这个应该是将port和source1事件建立映射的map,因为source1事件是可以通过port来唤醒Runloop的;

_portSet 当前mode监听的port集合,也就是说这些port是可能发消息过来的。

另外会发现发现有一些条件判断:
#if USE_DISPATCH_SOURCE_FOR_TIMERS,在sunnyxx的分享视频中提到过,GCD和CF的timer实现原理是不同的,不过从源码中可以看到,CF也可以借用GCD来实现自己的timer机制。
#if USE_MK_TIMER_TOO,MK_TIMER应该就是Mach Kernal Timer的意思,就是系统内核的timer。
#if DEPLOYMENT_TARGET_WINDOWS,当前部署的平台是Windows,由于CF就是C写的,因此也可以在Win上面跑。
在接下来贴的源码中,我**将会删除一部分在该定义下的相关代码**,因为一般不会跑这些逻辑(其实是我看不太懂- -)。

RunLoop ,Run!

大致对两个结构体有所了解之后,就可以跑起来了,RunLoop的核心逻辑都在方法__CFRunLoopRun()中,这是一个不算短的方法,一点一点的来看:

先从参数看起:

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

参数名还是比较友好的,基本看名字就知道有什么作用,这里的seconds是一个超时时间,用于决定是否退出RunLoop,stopAfterHandle是一个控制命令,标示是否处理完事件后就退出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
    //判断是否结束Runloop
if (__CFRunLoopIsStopped(rl)) { //RunLoop结束了,(没source,没timer,没observer)
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) { // mode结束了,可能发生mode切换
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}

//发消息的port,初始化为NULL
mach_port_name_t dispatchPort = MACH_PORT_NULL;
//判断是否在主线程
Boolean libdispatchQSafe = pthread_main_np() &&
((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) ||
(!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&
0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));


//如果当前在主线程 && runloop是主线程的runloop &&
// 现在要跑的mode是commonModes之一(被标记为common的mode) ,
// 那么就把mainqueue的port赋值给dispatchPort用于接收消息
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) &&
CFSetContainsValue(rl->_commonModes, rlm->_name))
dispatchPort = _dispatch_get_main_queue_port_4CF();


#if USE_DISPATCH_SOURCE_FOR_TIMERS
//这个if pass的话,就要从当前mode的queue中拿个mode,后续来用。
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif

//用gcd timer来实现RunLoop超时机制
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));

if(seconds<=0.0){
<br/>
seconds = 0.0;
timeout_context->termTSR=0ULL;
}else if(seconds<=TIMER_INTERVAL_LIMIT)
{<br/>
//是一个有效超时时间
//根据当前线程在对应线程创建timer.
dispatch_queue_t queue = pthread_main_np() ?
__CFDispatchQueueGetGenericMatchingMain() :
__CFDispatchQueueGetGenericBackground();
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

dispatch_retain(timeout_timer);

timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);

//当前时间+超时间隔,应该就是超时的时间了。
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);

dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context

//超时回调和取消的回调
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);

// void dispatch_source_set_timer(
// dispatch_source_t source, // timer object
// dispatch_time_t start,//开始时间
// uint64_t interval,//时间间隔,纳秒级别的
// uint64_t leeway);//系统可延迟的时间,纳秒级别的,就是精度
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL); //估计是和系统时间耦合的一种算法。
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer); // 开始跑timer
/*
超时机制设了一个间隔为infinite的timer,在超时的时刻跑一次。模拟在指定时刻跑一次且仅跑一次的功能。
*/


} else { // infinite timeout
seconds = 9999999999.0;
}

//两个标志位。
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;

// do-while

上面一段代码都在为RunLoop的启动做准备工作,根据是否在主线程,为dispatchPort设好了初值,利用GCD设置好了RunLoop的超时机制(注意是RunLoop本身的超时机制,而不是用GCD代替RunLoop内部处理的timer机制)。

准备工作基本完成后,准备进入核心的do-while:

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
       do { 
//应该也是标志位。
voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
voucher_t voucherCopy = NULL;
//存放message的缓存池
uint8_t msg_buffer[3 * 1024];

//message 和 livePort,livePort应该接下来会被唤醒runloop的source1的port赋值。
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;


//取出当前mode中的port集合。
__CFPortSet waitSet = rlm->_portSet;

//设置当前runloop可以被唤醒
__CFRunLoopUnsetIgnoreWakeUps(rl);

//通知observer,要开始处理timers和sources了
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

//开始跑当前mode下的各种事件(遍历RunLoop中的_block_item链表)
__CFRunLoopDoBlocks(rl, rlm);

//挨个跑source0. source0 可能往block_item list里加了东西?代码没看出来...
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {// 如果跑了source0
__CFRunLoopDoBlocks(rl, rlm); //可能加了新block_item再跑一边block??
}

// source0 处理了东西 ,或者说超时
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

//如果之前拿到了mainQueue的port,而且还未处理这个port , 第一次循环不会进来,因为didDispatchPortLastTime初始化为true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
//读缓冲区消息
msg = (mach_msg_header_t *)msg_buffer;

//从port拿数据(source1事件触发port)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
//跳到 handle msg
goto handle_msg;
}

}


didDispatchPortLastTime = false;

//走到这,说明Runloop的这一圈已经处理了这圈该处理的timers,source0,以及block_items, 准备进入休眠。

//没处理source0 ,意思是source0已经处理完了。
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

//开始休眠
__CFRunLoopSetSleeping(rl);
// do not do any user callouts after this point (after notifying of sleeping)

// Must push the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced.

//把dispatchPort加入waitSet(当前mode的port集合)
__CFPortSetInsert(dispatchPort, waitSet);

__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);

//记录休眠时间
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

...


先看看休眠前循环干了些什么:
设置了标记变量、监听RLM的端口,通知Observer,调用__CFRunLoopDoBlocks()来遍历_block_item链表中的block(这个函数基本就是遍历链表,根据RunLoopMode决定是否执行block,并且重新构造链表的过程,有兴趣的可以自己看看,比较好理解),处理Source0事件(从这里可以看出,source0事件对RunLoop的唤醒没有任何影响和操作机会),在Source0事件执行完后,再次遍历执行一遍_block_item链表,我的猜测是在source0的回调中,可能新加入了一些事件,而RunLoop需要在这个时候跑这些事件。
需要注意的是,虽然此时RunLoop已经发出通知kCFRunLoopBeforeTimers,但其实其并不是立刻就查询timer是否到时然后执行,相反,是timer到时后,主动让RunLoop来处理自己的回调(后面源码会看到)。
接着如果没有source1的消息需要处理,且本次循环没有处理任何source0事件,就准备进入休眠。至于为什么要让RunLoop多转一圈,在循环没有处理任何source0才进行休眠,我的猜想是,类似于Pan手势触发的事件是一个连续事件,而source0事件是不能唤醒RunLoop的,所以即使当前循环处理完所有source0,不代表source0事件就真的被处理完了,所以要多一圈“空转”,来保证对这类连续事件的完整处理。
最后,把dispatchPort加入监听集合,保证主线程的source1事件也可以唤醒RunLoop。

接下来看看怎么个睡法:

1
2
3
4
5
6
7
8
9
10
11
if (kCFUseCollectableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;

//正常情况,只是监听source1事件
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);


虽然前面有一句调用__CFRunLoopSetSleeping(rl),但我觉得这只是设置一个标识,而真正保持休眠直到唤醒的阻塞逻辑应该是__CFRunLoopServiceMachPort(),这里很明显是监听waitSet中的port,将发消息过来的port赋值给livePort供后续使用,而这里通过之前是否处理了source0的返回结果poll来确定等待时间,也证实了这一观点:当处理了source0,poll为true,这轮休眠时间为0,只有当poll为false时,才真的一直休眠,直到有事件唤醒。

被唤醒之后自然要开始干事:

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
111
112
113
114
115
116
117
118
        __CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);

rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

//把dispatchPort再从waitSet里拿出来。
__CFPortSetRemove(dispatchPort, waitSet);

//当前runloop已经要醒来了,忽视重复唤醒操作
__CFRunLoopSetIgnoreWakeUps(rl);

//重置标志位
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
//observer
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

handle_msg:;
//已经醒了,忽视其他唤醒通知
__CFRunLoopSetIgnoreWakeUps(rl);

// 莫名其妙被叫醒
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) { //被指定端口叫醒
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}


#if USE_DISPATCH_SOURCE_FOR_TIMERS
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
//被内核计时器唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();


if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif

//被主线程port事件唤醒。
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else { //被source1唤醒
CFRUNLOOP_WAKEUP_FOR_SOURCE();

voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

//定位runloop source1
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {

mach_msg_header_t *reply = NULL;

sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}

}

// Restore the previous voucher
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);

}
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);



//再次执行block_items
__CFRunLoopDoBlocks(rl, rlm);


//更新返回值。
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);

} while (0 == retVal);

唤醒之后,设置一些标志位、记录好自己睡了多久(真健康),然后开始搞清楚到底是谁把自己弄醒了,头两个if不做任何操作,所以从第三个if开始看起,可以看到,不论是GCDTimer还是内核Timer,最终都使用__CFRunLoopDoTimers来跑逻辑;如果是被主线程的port唤醒,那么接收到消息后,将这个消息交由主线程处理;如果是被当前RLM指定的port唤醒,那么就处理Source1事件,逻辑与处理Source0类似,处理完成后又再一次的对_block_items进行遍历。

至此,do-while循环已经走完,__CFRunLoopRun()的最后几句逻辑是清理超时timer并返回retVal。

总结而论,在__CFRunLoopRun()中,我们首先按需设置好主线程端口、初始化好RunLoop超时机制,然后进入do-while循环,循环内,我们处理已在RunLoop中并且可以在当前RLM下执行的代码段(_block_items链表),处理source0,看情况决定是否真正进入休眠,唤醒后(或者根本没休眠),查询是否有timer需要执行、是否有主线程port消息需要转发、是否有source1需要执行,并根据参数和执行情况更新返回值,而后根据返回值确定是否继续循环。

所以看完代码有什么用?

所以现在你对RunLoop的这个圈是怎么跑的有一个大致了解了,但是…这有什么用呢?Facebook开发的布局引擎AsyncDisplayKit往主线程塞任务的时机就是在RunLoopBeforeWaiting(即将休眠)和RunLoopExit(离开RunLoop,一般是由于要切换RunLoop)消息之后执行的,也就是说,你可以通过监听Observer,来让你的逻辑达到“闲时执行”的效果,不过,如果没有很深的知识储备(像我一样..),或许了解了这个RunLoop并不能让你开发出一些黑科技,但或许有一天,你遇到了一个奇怪的bug,RunLoop能够给你提供一个解决方案。

写个Test吧

看完代码之后,写个Test来测试一下自己刚刚的理解是否是正确的,是个不错的选择:
自定义一个TestView,重写drawRect

1
2
3
4
5
6
- (void)drawRect:(CGRect)rect {
// Drawing code
NSLog(@"Draw Rect Begin!");
sleep(3);
NSLog(@"Draw Rect Ended!");
}

三秒来模拟复杂的计算,然后在ViewController中加入代码:

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
 -(void)viewDidLoad {
[super viewDidLoad];
[self registerRunloopNotifications];
[self setupTestView];
}
-(void)registerRunloopNotifications{
CFRunLoopActivity act = kCFRunLoopEntry | kCFRunLoopBeforeTimers | kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting | kCFRunLoopBeforeSources;
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, act, YES, INT_MAX, &_runloopCallback, NULL);

CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}

-(void)setupTestView{
TestView *t = [[TestView alloc] initWithFrame:self.view.bounds];
t.backgroundColor = [UIColor orangeColor];

self.testView = t;

[self.view addSubview:t];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self.testView setNeedsDisplay];
NSLog(@"SetNeedsDisplay");
});

}

这里我们在5秒之后提交了一个任务到主线程,在这里设个断点,让我们看看调用堆栈:

定位到上文的代码,可以看到__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__正是在RunLoop被主线程监听port唤醒后调用的,再看看对应Log(_删除了部分Log,因为程序刚启动系统也有函数依赖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
2016-09-03 15:37:05.005 LearningRunLoop[1388:160760] Runloop Entry ! 
2016-09-03 15:37:05.005 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.005 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.006 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.007 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.008 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.009 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.009 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.009 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.009 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.009 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.010 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.010 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.010 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:05.010 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:05.010 LearningRunLoop[1388:160760] Runloop Before Waiting !
2016-09-03 15:37:06.981 LearningRunLoop[1388:160760] SetNeedsDisplay
2016-09-03 15:37:06.982 LearningRunLoop[1388:160760] Runloop Before Timers !
2016-09-03 15:37:06.982 LearningRunLoop[1388:160760] Runloop Before Sources !
2016-09-03 15:37:06.985 LearningRunLoop[1388:160760] Draw Rect Begin!
2016-09-03 15:37:09.986 LearningRunLoop[1388:160760] Draw Rect Ended!
2016-09-03 15:37:09.986 LearningRunLoop[1388:160760] Runloop Before Waiting !

从Log可以看出,系统启动后,依赖RunLoop跑了许多相关的逻辑后才让RunLoop休息,直到我们的dispatch_after触发… 等等?为什么RunLoop Entry过后没到五秒就跑出来SetNeedsDisplay的Log了?

让我们在setupTestView中的dispatch_after之前设置一个断点,看看此时的堆栈:

跟着堆栈往下看,可以看到VC和View的初始化是一个source0的操作,此时RunLoop已经开始跑了,但由于我们的代码还没开始跑呢,所以自然是收不到RunLoopEntry的通知的。但为什么过了一会我们又可以看到控制台RunLoop Entry的Log呢?

如果你还记得RunLoop有几个Mode,你就会知道这是怎么回事了

  1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  5. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

(摘自《深入理解RunLoop》 -Garan no Dou):

也就是说,viewDidLoad操作是在UIInitializationRunLoopMode这个mode下完成的,也就是说,虽然App跑起来之后,我们几乎都是在kCFRunLoopDefaultModeUITrackingRunLoopMode这两个mode下跑代码,但我们的初始化代码却是在另一个mode中执行的,虽然CFRunLoop.h中只暴露了kCFRunLoopDefaultMode
kCFRunLoopCommonModes两个CFStringRef,但在只要你在UIInitializationRunLoopMode跑完一圈之前,在对的地方加上:

1
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, (__bridge CFStringRef)@"UIInitializationRunLoopMode");

你还是可以成功监听到kCFRunLoopExit的消息的。

所以上述Timer“不准时”的问题,是因为我们在另一RLM中初始化了这个Timer,而这个Timer的mode是commonModes,因此可以跨mode计时,这个小实验也说明了一个事实:Timer的管理依赖于RunLoop而不是RLM,但Timer的计时逻辑是依赖于具体的RLM的(本例中,如果把Timer的mode换成defaultMode,则会在看到RunLoop Entry五秒后看到Timer Fire的Log)。

最后再来一个小小的实验,来证实上文中提到的Timer并不是在kCFRunLoopBeforeTimers通知后立刻去找到时的Timer来执行,而是先去处理_block_itemsource0时间,然后走休眠-唤醒流程,坐等Timer来找自己。

-(void)setupTestView中的dispatch略作修改:

1
2
3
4
5
6
7
8
9
10
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self.testView setNeedsDisplay];
NSLog(@"SetNeedsDisplay");

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

NSLog(@"Schedule Timer");
});

-(void)doTimer中向控制台输出timer fire。

1
2
3
4
5
6
7
8
9
2016-09-03 17:01:02.324 LearningRunLoop[1761:237255] SetNeedsDisplay
2016-09-03 17:01:02.325 LearningRunLoop[1761:237255] Schedule Timer
2016-09-03 17:01:02.325 LearningRunLoop[1761:237255] Runloop Before Timers !
2016-09-03 17:01:02.325 LearningRunLoop[1761:237255] Runloop Before Sources !
2016-09-03 17:01:02.328 LearningRunLoop[1761:237255] Draw Rect Begin!
2016-09-03 17:01:05.330 LearningRunLoop[1761:237255] Draw Rect Ended!
2016-09-03 17:01:05.330 LearningRunLoop[1761:237255] Runloop Before Waiting !
2016-09-03 17:01:05.331 LearningRunLoop[1761:237255] Runloop After Waiting !
2016-09-03 17:01:05.331 LearningRunLoop[1761:237255] Timer fire

可以看到,Timer硬生生的等了三秒多,才真正执行。

但不论如何,Timer最终还是执行了,这一点与在《深入理解RunLoop》中看到的不同。

最后再记录几个有关RunLoop的行为表现:

1.对于performSelector,当没有delay时,作为source0事件提交给RunLoop,当有delay时,封装成Timer事件提交给RunLoop。

2.触摸事件的处理逻辑是source0事件,但触摸事件本身是source1事件,将会唤醒RunLoop,然后将该触摸逻辑source0事件传给App的UIApplication单例进行事件分发。

3.屏幕绘制的逻辑既不在_block_item链表中,也不是source事件,而是CA注册了对RunLoop的监听,在Observer的回调中进行UI的绘制。目前从我的测试看起来应该是监听了BeforeWaiting事件。