scss
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
do{
//__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
//__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(rl, rlm);
__CFRunLoopDoSource0();
__CFRunLoopDoBlocks(rl, rlm);
//判断是否进入休眠
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
//__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
__CFRunLoopDoSource1(rl, rlm, rls, ...)
__CFRunLoopDoBlocks(rl, rlm);
//runloop是否继续
if (sourceHandledThisLoop && stopAfterHandle) {
}else {
}...
}
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
既然是一个循环,那么如果我们要做A,B, C三件事,如果A,B,C不是类似A会定义B的具体内容,B定义C的具体内容这样的关系,那么ABC在一个循环中的顺序就可以是随意的那么为什么一个runloop的真正循环是这样的?为什么首次进入在判断是否进入休眠之前先处理source0,Blocks?
为了方便分析今天的问题,我将其中的通知也都注释了
可以看到runloop处理的四种任务:timer source0 source1 Blocks
这四种任务最终都通向了函数调用这个终点(_dispatch_block_invoke())
,也就是进入函数调用栈执行,再出栈即完成使命
具体我们在说一下__CFRunLoopDoBlocks,它会检查是否有pending的block,有的话,就逐个执行。
这个block它是 经过 GCD 包装后的 block 结构体,不再是原始的闭包语法,我么就认为它是一个闭包即可,不影响理解。
那么这个block是从哪里获取的呢?这里就要说到__CFRunLoopDoBlocks 的双输入源
1、rl->_blocks_head 这是一个RunLoop 自己维护的block 链表
生产者:**CFRunLoopPerformBlock()
2、主队列:GCD 主队列是一个独立的 dispatch_queue_t
生产者:所有dispatch_async(main_queue, ...) 的 block 都入队到这里,performSelector(onMainThread)也是终使用的gcd添加block到主队列
消费者:RunLoop 通过 _dispatch_runloop_root_queue_perform_4CF()
从它取 block,这部分就在__CFRunLoopDoBlocks中实现
为什么设计两套机制
rl->_blocks_head
的优势:Mode 精细控制
objectivec
// 用户正在滚动 tableView(UITrackingRunLoopMode)
CFRunLoopPerformBlock(
CFRunLoopGetMain(),
CFSTR("UITrackingRunLoopMode"), // ← 只在这个 mode 下执行
updateUIBlock
);
如果用 dispatch_async
,block 会在 kCFRunLoopCommonModes
立即执行,可能打断滚动等UI更新
GCD 与 RunLoop 协作机制:"生产-消费"模型GCD:负责入队(enqueue),通过 dispatch_async
, performSelector
, Timer
等RunLoop:负责 出队并执行(dequeue & execute), 通过 __CFRunLoopDoBlocks()
这是一个标准的 "多生产者 - 单消费者" 模型(主线程消费)
谁可以向主队列"入队"?
1、GCD(最常见)
DispatchQueue.main.async { self.updateUI() }
python
DispatchQueue.main.async {
self.updateUI()
}
- 直接调用 GCD API
- block 被加入主队列的
pending
队列
2、Foundation / UIKit(间接使用 GCD)
php
// performSelector 系列
self.performSelector(onMainThread: #selector(loadData), with: nil, waitUntilDone: false)
// NSTimer 添加到 main runloop
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
- UIKit 内部调用
dispatch_async(main_queue, ...)
- 最终还是 GCD 入队
3、系统框架(如 KVO、Notification)
php
NotificationCenter.default.addObserver(
forName: .dataUpdated,
object: nil,
queue: .main) { _ in
self.refresh()
}
queue: .main,表示回调在主队列执行
- 内部使用
dispatch_async(main_queue, ...)
谁从主队列"出队"?(消费者)
只有一个:主线程的 RunLoop,
scss
runloop每次循环都会多次调用__CFRunLoopDoBlocks(),检查是否有pending的block,有的话,就逐个执行。
gcd和performselector都是主队列的生产者
从主队列这个视角看,gcd和performselector 、runloop就是一个多生产,单消费的模型
DispatchQueue.main.async
会成功将 block 入队到主队列- block 被存储在 GCD 的全局队列结构中
- 它在
application:didFinishLaunching
返回后,由 RunLoop 在下一次循环中执行消费掉
主队列 ≠ RunLoop,主队列是 GCD 的概念,RunLoop 是 CF 的概念,但 RunLoop 负责"消费"主队列中的任务。
可以看出这个__CFRunLoopDoBlocks在runloop的每次循环中将会被调用多次,因为在runloop执行过程中随时都可能生成新的blocks
swift
// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window?.makeKeyAndVisible()
// → UIKit 内部 performSelector
self.performSelector(#selector(setupUI), on: Thread.main, with: nil, waitUntilDone: false)
DispatchQueue.main.async {
self.loadData()
}
return true
}
这段代码实际做了什么,其中的函数在什么事机真正的执行?
主队列(main queue)在 main()
启动时就已创建,早于 application:didFinishLaunching
。
但是runlooprun()也就是runloop启动是在application:didFinishLaunching方法返回之后。
因此可以说application:didFinishLaunching中的操作无一例外,他们都不会是从runloop基础上放入调用栈的,而是直接入栈。
举个例子
js
self.window?.makeKeyAndVisible()
它是一个 同步方法调用 ,在当前线程(主线程)立即执行,设置窗口为「主窗口」(key window)并使其可见,它本身不创建任何 RunLoop Source,而是直接修改对象状态
注意这里只是修改状态,而这些状态真正表现到UI上,这部分UI 重绘的代码是在什么时机执行?
makeKeyAndVisible()
修改了窗口状态后,UIKit 内部会安排一次 UI 重绘 ,这个"安排"才是关键,UI更新等最终会被转换为source0事件源,由runloop来处理。
而performselector和gcd也是一样,他们立即执行,但是只是将对应的block放入对应的待执行列表,runloop运行起来以后在__CFRunLoopDoBlocks中执行这些block对应的任务
一旦runloop运行起来,基本就是gcd,用户点击等唤醒runloop,runloop找到对应的任务,一一安排加入函数调用栈执行,没有任务时就休眠等待。
开发者手动唤醒runloop的方法:
1、使用 GCD(推荐)GCD 向主线程的 Mach Port 发送消息,触发 Source1 事件,mach_msg() 提前返回,RunLoop 执行 block
2、使用 CFRunLoopWakeUp(),内部向 RunLoop 的 wakeUp port 发送一个空消息,触发 Source1 事件,RunLoop 从 mach_msg()
中醒来
3、发 Mach 消息
为什么设计成"先配置,再启动"?
原则 | 说明 |
---|---|
配置完整性 | 必须让你完成所有初始化(如设置 window、注册服务、检查登录状态)后,才开始响应事件 |
避免竞态 | 如果 RunLoop 先启动,可能会在你还没准备好时就收到通知、定时器、网络回调 |
控制权清晰 | UIApplicationMain 掌握启动流程,你只负责"配置",它负责"执行" |
兼容性 | 保证所有 App 都遵循相同的启动顺序 |
调用栈:
调用栈是动态确定 的,每一次函数调用都实时修改栈只有在程序运行起来后,调用栈才开始"生长"
js
1. 程序启动:
[main]
2. main 调用 UIApplicationMain:
[UIApplicationMain]
[main]
3. UIApplicationMain 调用 application:didFinishLaunching:
[application:didFinishLaunching]
[UIApplicationMain]
[main]
4. 执行 self.window?.makeKeyAndVisible() :
[makeKeyAndVisible] ← 临时帧
[application:didFinishLaunching]
[UIApplicationMain]
[main]
5. makeKeyAndVisible 返回:
[application:didFinishLaunching]
[UIApplicationMain]
[main]
6. application 函数返回:
[UIApplicationMain]
[main]
7. UIApplicationMain 进入 RunLoop 循环...
RunLoop 本身不操作栈,但它通过调度任务,间接导致调用栈的变化。 RunLoop 是"任务调度员",它决定"下一个谁上台表演";调用栈是"舞台上的演员",它记录"现在正在说什么台词"。调度员不背台词,但没有调度员,演员就不知道何时上场。
CPU 一直在执行"调用栈"------RunLoop 就是主线程调用栈上运行的一个"超级循环函数"。
不存在"执行调用栈" vs "处理 RunLoop" 的分离------RunLoop 是调用栈的一部分,调用栈是 CPU 执行的轨迹。
CPU 从 main()
开始,一路执行到 CFRunLoopRun()
,然后在这个循环里不断调度任务,所有这些都记录在主线程的调用栈中。
swift
func application(_ app: UIApplication, didFinishLaunchingWithOptions opts: [AnyHashable: Any]?) -> Bool {
print("Tick")
DispatchQueue.main.async {
self.loadData()
}
return true
}
print("Tick")
不是"任务",而是"当前执行流"
当前执行流 = 当前线程的调用栈(Call Stack)上正在执行的函数序列
- 它不是通过
dispatch_async
或performSelector
添加的 - 它是
application:didFinishLaunching
函数体内的同步代码 - 它在当前线程(主线程)上立即执行
- 它不需要 RunLoop 循环来驱动,更不需要放入主队列
维度 | 当前执行流 | 任务 |
---|---|---|
存储位置 | 调用栈(Stack) | 堆(Heap)或队列 |
生命周期 | 函数调用期间 | 从注册到执行完成 |
触发方式 | 函数调用 | 调度器(RunLoop/GCD)触发 |
是否阻塞 | 是(同步) | 否(异步) |
上下文 | 当前栈帧 | 捕获的变量(closure) |
例子 | print(), if, for |
async, Timer, performSelector |
前文提到makeKeyAndVisible()
修改了窗口状态后,UIKit 内部会安排一次 UI 重绘,这个"安排"才是关键,从这个关键开始我们逐渐解答标题中的疑问。
UI更新包括动画、 self.myView.setNeedsDisplay()
或 self.myView.setNeedsLayout()的调用等最终会被转换为source0事件源,例如一个方法test
js
test(){
//一些view的属性设置
myView.setNeedsDisplay() // 【调用 1】
//另一些view的属性设置
myView.setNeedsDisplay() // 【调用 2】
}
- 【调用 1】执行:myView的一个内部标志位(比如 _needsDisplay)被设置为true。系统会记下这个视图(
myView
)需要更新。同时,它会确保主线程RunLoop中用于处理UI更新的Source0事件已经被安排(scheduled)。如果这个 Source0 还没在队列里,就把它加进去。如果已经在队列里了,就什么都不做。
2.【调用 2】执行:它再次看到myView已经被标记为"需要重绘"了。所以它不会做任何额外的事情,不会创建第二个事件,也不会改变已有的安排。它只是简单地返回。
可以把它想象成一个待办事项清单:
-
第一次调用 setNeedsDisplay():把 " 重绘 myView " 加到清单里。
-
第二次调用 setNeedsDisplay():你看了看清单,"重绘 myView"这项已经在上面了。所以你不需要再做任何事。
source0,非系统事件,即用户自定义的事件,不能自动唤醒 runloop,需要先调用CFRunLoopSourceSignal(source)
将 source
置为待处理事件,然后再唤醒 runloop
让其处理这个事件source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调
这样理解下来,在判断是否休眠之前处理source0是非常合理的,毕竟UI都没有的话,用户无法操作,又何来source1事件源呢?
在判断是否休眠之前处理blocks也是非常合理的,毕竟runloop开始之前在application:didFinishLaunching方法中,开发者可能加入了很多blocks任务等待处理
那么为什么不在这时处理timers呢?
RunLoop 在进入休眠前,会主动查询"最近的 Timer 什么时候到期",然后把"这个时间差"作为 mach_msg()
休眠的最大时长,从而确保 RunLoop 不会"睡太久",Timer 一到点就能执行。
问题 | 解释 |
---|---|
"RunLoop 怎么知道 Timer 到期了?" | 它不知道,除非它"醒来"并检查时间 |
"能在休眠前检查吗?" | 能,但只能通过 计算"最近 Timer 到期时间" 来决定"睡多久" |
"为什么不直接执行?" | 因为 Timer 可能还没到期!你只能"限制休眠",不能"预执行" |
js
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);
}
}
最后,标题中的问题根本就不成立,为什么呢?看下部分__CFRunLoopRun源码
js
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
do {
if (kCFUseCollectableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
可以看出有source0消息处理时poll为0,也就是说基于我们之前在application:didFinishLaunching方法中的各种方法调用, 第一次进入runloop后__CFRunLoopServiceMachPort方法调用并不会休眠,而是直接返回,继续处理模式队列和timer,source1等,source0的处理放在source1之前也非常可以,要有个先来后到嘛。
有任何问题,欢迎读者、专家指正~