一、前言
- 因为RunLoop的源码涉及到C语言,所以比较抽象。
- 在苹果网站上下载 RunLoop 的源码,地址为:https://opensource.apple.com/tarballs/CF/
- 找到 CFRunLoop.c 文件,进行学习。
- 需要找到RunLoop 的入口文件,也就是上个笔记上写的 ‘通知Observers:进入Loop’ 这句话。
二、如何找到 RunLoop 的入口
- 新建一个iOS项目,在ViewController.m 文件中 写一个
touchesBegan:withEvent
方法,在这个方法中打上断点. - 为何在 这个方法中打上断点,因为 点击事件就是由 RunLoop来处理的。在这个方法中打上断点,就可以看到到底是RunLoop的哪一个函数来进行调用的。也就是要在 这个方法中看下函数调用栈。
- 函数调用栈
- 可以看到函数调用栈是从下往上开始查找的。最开始的入口是
UIApplicationMain
, 依次是:GSEventRunModal
、CFRunLoopRunSpecific
、__CFRunLoopRun
、__CFRunLoopDoSources0
、__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
、直到 调用touchesBegan:withEvent:
方法 - 我们可以在runloop.c文件中查找
CFRunLoopRunSpecific
函数。也就是 用CFRunLoopRunSpecific
函数当 runloop的入口。 - 可以在
CFRunLoop.c
文件中搜索到函数:SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
三、理顺 CFRunLoopRunSpecific 函数的流程
注: 只保留 CFRunLoopRunSpecific 中主要的代码。
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知 Observers: 进入 Loop
// currentMode : 进入到某种模式
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 具体要做的事情
// __CFRunLoopRun 这个可以在函数栈中看到。
result == __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers: 退出 Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
四、__CFRunLoopRun 函数做了哪些事情
- 在这个函数中你会看到一个
do { ... ... } while(0 == retVal)
的循环。这个循环的含义是,如果 do 后面的大括号 里面的代码返回值等于 0 (也就是 while 条件成立的话),会一直执行 do 里面的代码 - 如果 条件一直成立,它就会一直循环的做do里面的事情。
- do while 循环 里面的内容,就是 runloop 具体要做的事情
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知 Observers: 即将处理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers: 即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理 blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理 source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 如果返回 Yes ,再次处理blcoks
if (sourceHandledThisLoop) {
// 处理 blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 判断 有没有 Sources1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Sources1, 就跳转到 handle_msg
goto handle_msg;
}
didDispatchPortLastTime = false;
// 通知 Observers: 即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
do {
// 等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 如果没有消息来唤醒,就会阻塞在这里。一直等到消息来唤醒才会往下执行代码。
} while (1);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers:结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被timer唤醒) {
// 处理 Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被 GCD 唤醒) {
// 处理 GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {// 被 Source1 唤醒
// 处理 Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 处理 blocks
__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;
}
} while (0 == retVal);
return retVal;
}
五、 RunLoop 每个阶段里面所调用的函数
每一个步骤里面都会调用一个很长的方法,这个方法是处理 UIKit 的。
01、通知Observers:进入Loop:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
04、处理Blocks:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
08、通知Observers:结束休眠(被某个消息唤醒)
- 01> 处理Timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- 02> 处理GCD Async To Main Queue :
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
- 03> 处理Source1 :
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
09、处理Blocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
例如:
(一)、 NSTimer
- 当到了
_CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
,在上面的 #1 则是 Foundation 框架的 NSFireTimer 了。
(二)、特殊的GCD
- 一般来说 GCD 有自己的逻辑来处理。
- GCD 有很多东西是不依赖 RunLoop 处理的。
- 也就是说: GCD 是 GCD ; RunLoop 是 RunLoop 。 他们是分开的。
- 但GCD 有一种情况,会交给 RunLoop 来处理。如下图:
- 打印它的函数栈
六、休眠的细节
(一)、复习
- 在
07:通知Observers:开始休眠(等待消息唤醒)
这条如果没有消息,就开始睡觉。 - 开始睡觉,可以当成是线程阻塞,不会继续往下走。
- 代码为:
do {
// 等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 如果没有消息来唤醒,就会阻塞在这里。一直等到消息来唤醒才会往下执行代码。
} while (1);
- 休眠意味着当前线程不做事情,CPU 不会给 当前线程任何资源,当前线程就没有事情可做,就睡觉。代码就会停留在这个位置,不会再往下走。
- 一旦用户唤醒它,才会继续往下走。线程开始做事情。
(二)、线程阻塞是如何实现的
-
如果 写成
while(1){... ... }
形式的线程阻塞,是通过代码进行阻塞,当前线程并没有休息。因为这句代码是死循环,一直在判断条件,条件成句,执行语句;条件成句,执行语句;一直在执行代码,相当于当前线程一直在执行代码,没有休息。这样 cpu 并没有休息。 -
但
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
这句代码,会直接让 当前线程睡觉。 -
怎么会达到这种效果?
- 在 RunLoop.c -> __CFRunLoopServiceMachPort 方法中,有
mach_msg(msg,MACH_RCV_MSG|...)
这行代码 - 当看到 mach_msg 代表 内核 函数。它就能直接睡觉。
- 要想达到直接睡觉,只有内核层面才能办到。
- 在 RunLoop.c -> __CFRunLoopServiceMachPort 方法中,有
-
API 分为:
- 内核层面API : 非常底层的API,操作系统底层的API。
- 例如: 让系统直接死掉的API、让系统直接休眠的API或者直接操作硬件层面的一些代码。
- 一般不开放给 程序员 使用。因为比较危险。
- 应用层面API :
- 开放给 程序员使用。可以搭建一些界面、发送网络请求之类的。
- 内核层面API : 非常底层的API,操作系统底层的API。
-
RunLoop休眠的实现原理
- 有两大类,用户方面和内核方面。当用户调用
mach_msg()
方法时,mach_msg()
会直接调用 内核里面的mach_msg()
. - 内核的
mach_msg()
方法等待消息。如果没有没有消息就让线程休眠,有消息就唤醒线程。 - 当有消息的时候,内核的
mach_msg()
会告诉 用户 , 处理消息。
七、面试题:
- runloop内部实现逻辑?
- 就是上边的运行逻辑图
- runloop和线程的关系?
- 一对一的关系
- runloop 是怎么响应用户操作的, 具体流程是什么样的?
- 首先是由 Source1 捕捉系统事件。
- 相当于一旦用户点击屏幕,先是由 Source1 去处理这个事件,然后 Source1 把这个事件包装事件队列 EventQueen,.
- 事件队列 EventQueen 是由 source0 处理。
- 说说runLoop的几种状态说说runLoop的几种状态
- kCFRunLoopEntry: 进入 RunLoop 循环
- kCFRunLoopBeforeTimers: 处理 定时器(Timers
- kCFRunLoopBeforeSources: 处理 Sources
- kCFRunLoopBeforeWaiting: 休眠之前
- kCFRunLoopAfterWaiting: 被唤醒
- kCFRunLoopExit: 退出当前 RunLoop
- runloop的mode作用是什么?
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
作用:
- 隔离。
- 将不同的Source0/Source1/Timer/Observer隔离开来。
这样每一组都不会互相影响。