RunLoop运行循环揭秘
# RunLoop运行循环揭秘
深入篇 | CFRunLoop源码结构、Mode与Source/Timer/Observer、线程保活、卡顿监控、滑动优化实战。
# 目录介绍
- 1. 案例引入
- 2. 架构概览
- 3. CFRunLoop源码剖析
- 4. Mode机制详解
- 5. Source——事件源
- 6. Timer——定时器
- 7. Observer——观察者
- 8. RunLoop与UI刷新
- 9. 线程保活与常驻线程
- 10. 综合案例串讲
# 1. 案例引入
# 1.1 一段诡异的卡顿
先看一个在直播 App 里真实出现的性能故障。产品上线后,用户反馈:滑动直播间列表时,轮播广告的自动翻页 NSTimer 竟然暂停了。广告停了,运营直接找过来——"为什么广告不再轮播?"
代码翻出来看:
// AdBannerView.m —— 轮播广告视图
@interface AdBannerView : UIView
@property (nonatomic, strong) NSTimer *autoScrollTimer;
@end
@implementation AdBannerView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 添加到主线程 RunLoop 的 default mode
self.autoScrollTimer = [NSTimer scheduledTimerWithTimeInterval:3.0
target:self
selector:@selector(scrollToNext)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)scrollToNext {
[UIView animateWithDuration:0.5 animations:^{
self.contentOffset = CGPointMake(self.currentIndex * self.bounds.size.width, 0);
}];
// ...
}
@end
现象:
- 测试环境:在只有几个直播间的列表里滑动,广告正常轮播——没问题
- 生产环境:直播列表有 4000+ 行,用户快速滑动时,广告 NSTimer 暂停,手指停下后 Timer 又恢复
- 更诡异的是——Timer 并不是被销毁了,而是 "中间跳过若干次,恢复后立即触发"
直觉怀疑:是不是内存警告导致 Timer 被释放了?还是 NSTimer 的 repeats:YES 有 bug?
# 1.2 顺藤摸到根因
带着这条线往下挖:
- 假设 1:Timer 被释放了?—— 加断点观察
autoScrollTimer的isValid,始终为YES,说明对象还在。否定。 - 假设 2:是不是
UIView.animateWithDuration阻塞了 Timer?—— UIView 动画在另一个 context 里跑,不应该阻塞 Timer 调用。 - 假设 3:那为什么 Timer "暂停"了?—— 在
scrollToNext里打日志,发现滑动期间一次都没调。翻阅文档发现一行关键描述:"The timer will not fire while the run loop is in tracking mode or other modes." - 假设 4:打开 Instruments 看 Time Profiler,滑动时主线程依旧在执行 cell 的渲染、图片解码——CPU 并没有空闲,所以 Timer 没机会执行?—— 不对,如果只是 CPU 忙,Timer 应该是延迟执行而不是彻底不触发。
追到 CFRunLoop 的源码才发现症结——CFRunLoop 内部靠 __CFRunLoopRun 这个大循环工作,每个循环体里:
- 通知 Observer:即将处理 Timer
- 通知 Observer:即将处理 Source0
- 处理 Source0
- 如果有 Source1,跳转到"处理 Source1"分支
- 通知 Observer:即将休眠
- 休眠(
mach_msg等待 Mach Port 消息) - 被唤醒后,根据唤醒原因走不同分支
关键在第 3-4 步:当 RunLoop 切到 UITrackingRunLoopMode(滑动追踪模式),里面只注册了 UI 事件相关的 Source/Timer——你用 scheduledTimerWithTimeInterval: 创建的 Timer 默认加在 NSDefaultRunLoopMode 下,滑动时 RunLoop 切走了,你的 Timer 跟着"失联"了。
- 假设 5:那如果不滑动了,Timer 会恢复吗?—— 手指离开屏幕后,RunLoop 切回
NSDefaultRunLoopMode,Timer 发现 "上一次本该在 3 秒前触发",立刻执行——这就是用户感知到的 "中间跳过若干次,恢复后立即触发"。
这一段事故里至少藏着 7 个原理点:
① RunLoop 不是一个简单的 while(1),它的核心循环长什么样? → 第 3 章
② 什么是 Mode?为什么 RunLoop 要切 Mode? → 第 4 章
③ NSTimer 不准的根因在哪? → 第 6 章
④ Source0 / Source1 是什么?为什么 UI 事件走 Source0? → 第 5 章
⑤ Observer 能观测到 RunLoop 的哪些状态? → 第 7 章
⑥ 子线程默认为什么没有 RunLoop?怎么"保活"一个线程? → 第 9 章
⑦ 怎么利用 RunLoop 做卡顿监控? → 第 7 章
# 1.3 我们要回答什么
这个广告轮播暂停的事故就是本篇的主线案例。每讲完一层原理,就解开一两个问号;最后在第 10 章,把整条链路兜回到 AdBannerView 上,回答清楚"为什么 Timer 在滑动时暂停,又怎样修复"。
本篇路线:
架构概览 (第 2 章)
↓
CFRunLoop 源码 (第 3 章) ─→ 解开"这个大循环到底在干什么"
↓
Mode → Source → Timer → Observer (第 4-7 章) ─→ 解开"滑动时 Timer 为什么暂停"
↓
UI 刷新 → 线程保活 (第 8-9 章) ─→ 实战武器库
↓
综合案例 (第 10 章) ─→ 案例彻底剖开
📌 本篇定位:这是 iOS 专栏中与 Runtime 并列的核心篇。理解了 RunLoop,才能解释"为什么 App 不退出的秘密"、"为什么主线程能同时处理触摸 + 动画 + 网络回调"、"NSTimer 怎么优化"、"卡顿监控怎么做"——本质上都是在玩弄 RunLoop 的状态切换。
# 2. 架构概览
# 2.1 RunLoop不是While(1)
疑惑:iOS App 的 main() 函数最后一行是 UIApplicationMain(...),它不 return——那程序为什么不会"卡死在那一行"?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
// ⬆️ 这行代码永远不会返回,但 App 依然流畅运行
}
}
论证:UIApplicationMain 内部启动了主线程的 RunLoop。RunLoop 不是死忙循环,而是**"有活干就干,没活干就睡"** 的节能机制。
幼稚版(耗电): RunLoop 版(节能):
while (1) { while (1) {
if (有事件) { if (有事件) {
处理事件(); 处理事件();
} else { } else {
// 空转,CPU 100% ❌ 休眠,等待唤醒 ⏸️ ✅
} }
} }
RunLoop 的本质由两部分循环构成:
外层 while: 活着,就继续
├── BeforeTimers ← 告诉 Observer:"我要看 Timer 了"
├── BeforeSources ← 告诉 Observer:"我要看 Source0 了"
├── 处理 Source0 ← 触摸事件、performSelector 等
├── 有 Source1? ← Mach Port 消息(跨线程通信)
│ ├── 有 → 立刻处理,回到循环开头
│ └── 无 ↓
├── BeforeWaiting ← "我准备睡觉了"
├── 休眠:mach_msg() 等待内核唤醒 ⏸️
├── AfterWaiting ← "我刚被叫醒了"
├── 被啥叫醒的?
│ ├── Timer → 处理 Timer
│ ├── Source1 → 处理 Mach Port 消息
│ ├── dispatch_main → 处理 GCD 主队列任务
│ └── 手动唤醒 → 继续
└── 回到循环开头
与 C++ 事件循环的本质区别:
| iOS RunLoop | Linux epoll / Windows IOCP | |
|---|---|---|
| 休眠机制 | mach_msg() 在内核等待 Mach Port | epoll_wait() 等待 fd 事件 |
| 事件源 | Source0(手动)、Source1(Mach Port)、Timer | fd 可读/可写、信号、定时器 |
| 用户态回调 | Observer(BeforeTimers/AfterWaiting 等 6 个时机) | 无(epoll 只返回就绪 fd 列表) |
| Mode 隔离 | 有(不同 Mode 下运行不同 Source/Timer/Observer 集合) | 无 |
| 内存管理 | ObjC 的 AutoreleasePool 推进依赖 RunLoop 的 Observer | 无 |
# 2.2 为什么切出五层结构
RunLoop 的设计不是 "一个大循环管一切",而是切成了 五层模型:
┌────────────────────────────────────────────────────────────────┐
│ CFRunLoop 对象 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ RunLoop Mode 集合 │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ NSDefaultRunLoopMode│ │UITrackingRunLoopMode│ ... │ │
│ │ │ │ │ │ │ │
│ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │ │ │
│ │ │ │ Source0 集合 │ │ │ │ Source0 集合 │ │ │ │
│ │ │ ├────────────────┤ │ │ ├────────────────┤ │ │ │
│ │ │ │ Source1 集合 │ │ │ │ Source1 集合 │ │ │ │
│ │ │ ├────────────────┤ │ │ ├────────────────┤ │ │ │
│ │ │ │ Timer 集合 │ │ │ │ Timer 集合 │ │ │ │
│ │ │ ├────────────────┤ │ │ ├────────────────┤ │ │ │
│ │ │ │ Observer 集合 │ │ │ │ Observer 集合 │ │ │ │
│ │ │ └────────────────┘ │ │ └────────────────┘ │ │ │
│ │ └────────────────────┘ └────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
五层逐级嵌套:
RunLoop → Mode → {Source0, Source1, Timer, Observer}
疑惑:为什么 RunLoop 要引入 Mode 这一层?直接把所有事件源(Source/Timer/Observer)平铺放不行吗?
论证:
事件优先级隔离的需要——当用户在快速滑动列表时(手指在屏幕上),系统切换到
UITrackingRunLoopMode。这个 Mode 里只注册了触摸相关的 Source 和定时器。那些"非紧急"的 Timer(比如轮播广告、网络状态检测)在NSDefaultRunLoopMode里,滑动期间不会被触发——CPU 资源全部留给 UI 渲染。滑完后切回NSDefaultRunLoopMode,非紧急 Timer 再照常运行。没有 Mode 的后果:假设平铺——列表滑动时,每 0.1 秒有一个网络状态检测 Timer 也要执行——UI 线程就得在"渲染 cell"和"检测网络"之间频繁切换,帧率从 60 FPS 掉到 30 FPS。Mode 是对主线程 CPU 时间的优先级调度器。
CommonModes 是"特权通道"——如果你确实希望某个 Timer 无论在什么 Mode 下都执行(比如倒计时),把它标记为
NSRunLoopCommonModes——它会同时注册到 Default 和 Tracking 两个 Mode 下。这相当于"VIP 通行证"。反向验证:Android 的
MessageQueue没有 Mode 概念——所以在滚动列表时,如果 Handler 投递了大量非紧急 Message,UI 线程照样逐一处理,这就是为什么 Android 滑动比 iOS 容易卡顿的原因之一(当然 Android 有Choreographer做 vsync 优先级调度,但晚了多年才引入)。
结论:Mode 不是"多余的抽象",而是主线程 CPU 时间片的多优先级调度器——把"触摸响应"和"定时任务"放在不同 Mode 下隔离执行,保证用户交互的最高优先级。理解了 Mode,就理解了第 1 章 Timer "暂停"的根本原因。
# 3. CFRunLoop源码剖析
# 3.1 __CFRunLoopRun核心循环
CFRunLoop 的核心是 C 函数 __CFRunLoopRun(在 CFRunLoop.c 中,约 400 行),它是整个 RunLoop 的灵魂。以下是极简但准确的伪代码(基于 CFRunLoop 开源版本):
void __CFRunLoopRun(CFRunLoopRef rl, CFStringRef modeName,
CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 0. 初始化:获取当前 Mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 1. 主循环开始
do {
// ===================== 阶段 A:通知 Observer =====================
// 通知:即将处理 Timer
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopBeforeTimers);
// 通知:即将处理 Source0
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopBeforeSources);
// ===================== 阶段 B:处理 Blocks =====================
__CFRunLoopDoBlocks(rl, currentMode);
// ===================== 阶段 C:处理 Source0 =====================
Boolean sourceHandled = __CFRunLoopDoSources0(rl, currentMode, stopAfterHandle);
// 触摸事件、performSelector:withObject:afterDelay: 等走这里
// ===================== 阶段 D:如果 Source0 处理了事件,再来一轮 =====================
if (sourceHandled) {
goto handle_msg; // ← 这也是为什么触摸事件可以立即得到响应
}
// ===================== 阶段 E:检查 Source1 =====================
// Source1 是基于 Mach Port 的,这里用 GCC 内置的 __builtin_expect 优化分支预测
Boolean poll = sourceHandled || (0 == timeout_context->termTSR);
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 检查 GCD 主队列有没有任务
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer),
&livePort, 0, &voucherState, NULL)) {
goto handle_msg; // GCD 有任务 → 直接去处理
}
}
// ===================== 阶段 F:休眠前通知 =====================
didDispatchPortLastTime = false;
// 通知:即将休眠
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopBeforeWaiting);
// ===================== 阶段 G:休眠! =====================
__CFRunLoopSetSleeping(rl); // 标记 RunLoop 为休眠状态
// mach_msg 等在内核上——阻塞,直到被下面的任意事件唤醒:
// 1. Timer 到期
// 2. Mach Port 收到消息(Source1 / GCD dispatch)
// 3. 手动唤醒 CFRunLoopWakeUp(rl)
mach_msg(msg, MACH_RCV_MSG | MACH_RCV_TIMEOUT,
timeout /* 如果有 Timer,这里的 timeout 是距离最近 Timer 的剩余时间 */);
// ===================== 阶段 H:被唤醒了 =====================
__CFRunLoopUnsetSleeping(rl);
// 通知:刚从休眠中醒来
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopAfterWaiting);
handle_msg:
// ===================== 阶段 I:处理唤醒原因 =====================
if (被 Timer 唤醒) {
__CFRunLoopDoTimers(rl, currentMode, mach_absolute_time());
// ↑ 处理所有超时的 Timer
} else if (被 GCD 唤醒) {
__CFRunLoopDoSource1(rl, currentMode, ...);
// ↑ 处理 Mach Port 消息
} else if (被 Source1 唤醒) {
__CFRunLoopDoSource1(rl, currentMode, ...);
}
// ===================== 阶段 J:再处理一下 Blocks =====================
__CFRunLoopDoBlocks(rl, currentMode);
// ===================== 阶段 K:判断是否继续 =====================
// 如果 stopAfterHandle 且刚处理了事件 → 退出
// 如果 Mode 为空 → 退出
// 如果超时 → 退出
retVal = 0;
} while (0 == retVal);
}
核心要点(必须牢记):
| 步骤 | 做什么 | 何时发生 |
|---|---|---|
BeforeTimers | 通知 Observer | 每次循环开始 |
BeforeSources | 通知 Observer | 处理 Source0 前 |
| 处理 Source0 | 触摸事件、performSelector | 有 Source0 需要处理时 |
BeforeWaiting | 通知 Observer:准备休眠 | 进入休眠前(关键时机) |
mach_msg 休眠 | RunLoop 真正停下来 | 没有任何事件时 |
AfterWaiting | 通知 Observer:刚被唤醒 | 被事件唤醒后(关键时机) |
| 处理 Timer / Source1 / GCD | 执行事件回调 | 有事件触发时 |
# 3.2 五大核心结构体
CFRunLoop 源码中的五个核心 C 结构体:
// 1. CFRunLoopRef —— 一个 RunLoop 对象
struct __CFRunLoop {
pthread_t _pthread; // 绑定的线程
CFMutableSetRef _commonModes; // 被标记为 Common 的 Mode 名字集合
CFMutableSetRef _commonModeItems; // 被标记为 Common 的 Item(Source/Observer/Timer)
CFRunLoopModeRef _currentMode; // 当前运行的 Mode
CFMutableSetRef _modes; // Mode 集合
};
// 2. CFRunLoopModeRef —— 一个 Mode
struct __CFRunLoopMode {
CFStringRef _name; // Mode 名字
CFMutableSetRef _sources0; // Source0 集合
CFMutableSetRef _sources1; // Source1 集合
CFMutableSetRef _observers; // Observer 集合
CFMutableArrayRef _timers; // Timer 数组(有序,按触发时间)
};
// 3. CFRunLoopSourceRef —— 事件源
struct __CFRunLoopSource {
CFMutableSetRef _runLoops; // 被哪些 RunLoop 引用
union {
CFRunLoopSourceContext *context; // Source0:version0
CFRunLoopSourceContext1 *context1; // Source1:version1
};
};
// 4. CFRunLoopTimerRef —— 定时器
struct __CFRunLoopTimer {
CFRunLoopRef _runLoop; // 所属 RunLoop
CFMutableSetRef _rlModes; // 在哪些 Mode 下有效
CFAbsoluteTime _nextFireDate; // 下次触发时间
CFTimeInterval _interval; // 时间间隔(0 表示不重复)
CFRunLoopTimerCallBack _callout; // 回调函数
};
// 5. CFRunLoopObserverRef —— 观察者
struct __CFRunLoopObserver {
CFRunLoopRef _runLoop; // 所属 RunLoop
CFOptionFlags _activities; // 观察哪些活动状态
CFIndex _order; // 优先级(越小越靠前)
CFRunLoopObserverCallBack _callout; // 回调函数
};
它们之间的关系(一张图):
线程 pthread_t
│
▼
CFRunLoop (每个线程最多一个)
│
├── _modes ──────────────────────────────────────┐
│ │
├── NSDefaultRunLoopMode │
│ ├── _sources0: {触摸, performSelector...} │
│ ├── _sources1: {GCD dispatch port} │
│ ├── _timers: {NSTimer 们} │
│ └── _observers: {AutoreleasePool, CA, ...} │
│ │
├── UITrackingRunLoopMode │
│ ├── _sources0: {触摸} │ ← 只有 UI 相关的
│ ├── _sources1: {GCD dispatch port}
│ └── _observers: {...} │
│ │
└── _commonModes (标记为 Common 的 Mode 名) │
└── _commonModeItems (在这些 Mode 间共享的 Items) │
# 3.3 线程与RunLoop绑定
疑惑:子线程有 RunLoop 吗?
论证:
NSLog(@"主线程 RunLoop: %@", [NSRunLoop mainRunLoop]); // ✅ 有
NSLog(@"当前线程 RunLoop: %@", [NSRunLoop currentRunLoop]); // ⚠️ 首次访问才创建
RunLoop 与线程是一一对应的,但默认不创建——只有在第一次调用 [NSRunLoop currentRunLoop] 或 CFRunLoopGetCurrent() 时,CFRunLoop 才会去查找线程的 TLS(Thread-Local Storage),如果没找到就创建一个新的并绑定。
// CFRunLoop.c 中的简化逻辑
CFRunLoopRef CFRunLoopGetCurrent(void) {
// 1. 从当前线程的 TLS 中取
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
// 2. 尝试从全局字典中取(按 pthread 做 key)
rl = __CFRunLoopFindForThread(pthread_self());
// 3. 都找不到 → 创建一个新的
rl = __CFRunLoopCreate(pthread_self());
__CFRunLoopSetForThread(rl);
return rl; // ← 注意:只创建了对象,但还没有 run!
}
关键细节:创建 RunLoop ≠ 启动 RunLoop。主线程的 RunLoop 是 App 启动时自动 run 的;子线程的 RunLoop 需要你手动调用 run 才会进入循环。而且一旦 RunLoop 退出循环(比如 Mode 里没有任何 Source/Timer),它会自动销毁。
这就引出了第 9 章的核心话题——线程保活。
# 4. Mode机制详解
# 4.1 Mode的隔离原理
疑惑:RunLoop 在运行时,怎么看"当前是哪个 Mode"?
一个 RunLoop 在任意时刻只能处于一个 Mode 下。当你调用 [runLoop runMode:NSDefaultRunLoopMode beforeDate:...] 时,RunLoop 的内部 _currentMode 就指向该 Mode。随后 RunLoop 的大循环里,只会处理这个 Mode 下的 Source0/Source1/Timer/Observer——其他 Mode 里的东西一概不管。
类比:
RunLoop = 一个「厨房」
Mode = 当前的「菜单」
当你切到 "早餐菜单":
→ 厨房只看早餐菜单上的菜(Source/Timer/Observer)
→ 午餐菜单上的菜虽然也在厨房,但厨子不看它
当你切到 "午餐菜单":
→ 早餐菜单上的菜被"挂起"
Mode 切换不是并行的——是串行切换。这就是第 1 章 NSTimer "暂停"的根本原因。
# 4.2 NSDefaultRunLoopMode
系统预定义的 Mode 有五种,实际常用三种:
| Mode 名称 | 说明 | 典型用途 |
|---|---|---|
NSDefaultRunLoopMode | 默认 Mode,App 空闲时主线程在此 | 大部分 Timer、网络回调 |
UITrackingRunLoopMode | UI 追踪 Mode,滑动/拖拽时主线程在此 | ScrollView 滚动优化 |
NSRunLoopCommonModes | 不是一个真实 Mode,是对 Common 标记的一组 Mode 的统称 | 让 Timer 同时注册到 Default + Tracking |
GSEventReceiveRunLoopMode | 系统接收事件(内部使用) | 几乎不用 |
UIInitializationRunLoopMode | App 启动瞬间的私有 Mode | 完全不用 |
# 4.3 UITrackingRunLoopMode陷阱
真实场景:
// 场景 1:滑动时暂停的 Timer
NSTimer *t = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{
// 只在 Default Mode 下触发,滑动时不触发
}];
// 场景 2:滑动时正常的 Timer
NSTimer *t = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^{
// ...
}];
[[NSRunLoop mainRunLoop] addTimer:t forMode:NSRunLoopCommonModes];
// ↑ 关键!CommonModes = Default + Tracking
陷阱更大的是 performSelector:withObject:afterDelay::
[self performSelector:@selector(doSomething)
withObject:nil
afterDelay:0.5];
这个方法的底层是创建一个 NSTimer 加到 Default Mode——如果在滑动期间调用了这行代码,doSomething 要等滑动停止才会触发。想让它不延迟?没有直接 API,必须改为用 dispatch_after 或把 Timer 手动注册到 CommonModes。
# 4.4 CommonModes的设计意图
NSRunLoopCommonModes 不是 Mode,它是一个标记机制:
// CFRunLoop 源码中的解释:
// 你可以把某个 Source / Timer / Observer 标记为"Common Mode Items"
// 然后 RunLoop 会自动把它添加到所有被标记为"Common"的 Mode 中
// 系统默认把这两个 Mode 标记为 Common:
// - NSDefaultRunLoopMode
// - UITrackingRunLoopMode
// 所以一个 Item 加到 NSRunLoopCommonModes → 实际被加入 Default + Tracking 两个 Mode
设计意图:给你一个"一次注册、多 Mode 生效"的快捷方式。当你希望某个 Timer 在滑动时也生效,不需要手动加两次——直接 addTimer:forMode:NSRunLoopCommonModes 即可。
# 5. Source——事件源
# 5.1 Source0触摸事件
疑惑:手指点击屏幕,这个事件是怎么进入 RunLoop 的?
完整链路:
手指触摸屏幕
│
▼
IOKit.framework 封装成一个 IOHIDEvent
│
▼
SpringBoard 通过 Mach Port 发给 App 进程
│
▼
App 的 __IOHIDEventSystemClientQueueCallback() 收到
│
▼
把 IOHIDEvent 封装成 Source0 投递到主线程 RunLoop
│
▼
RunLoop 在 __CFRunLoopDoSources0 里处理 Source0
│
▼
Source0 回调 → _UIApplicationHandleEventQueue
│
▼
UIKit 把事件传给 UIWindow → hitTest → 找到响应 View
│
▼
View 的 touchesBegan / target-action 被调用
Source0 vs Source1:
| Source0 | Source1 | |
|---|---|---|
| 触发方式 | 手动调用 CFRunLoopSourceSignal + CFRunLoopWakeUp | 自动:内核 Mach Port 有消息时唤醒 |
| 底层机制 | RunLoop 主动检查 | 基于 Mach Port(内核级 IPC) |
| 性能 | RunLoop 每次循环都要检查 | 休眠时被动等待,高效 |
| 典型用途 | UI 触摸事件(被包装成 Source0)、performSelector | 跨线程通信、GCD 主队列唤醒 |
# 5.2 Source1基于Mach Port
Source1 不关心"具体数据"——它只关心 "有个 Mach Port 收到消息了,需要处理"。具体数据由回调取。
// Source1 的核心回调
typedef struct {
void* info;
void (*perform)(void *info, CFDataRef msg, CFAllocatorRef allocator);
// ↑ msg 是从 Mach Port 读到的原始数据
} CFRunLoopSourceContext1;
GCD 主队列的唤醒就是走 Source1:dispatch_async(dispatch_get_main_queue(), block) → libdispatch 往主线程的 Mach Port _dispatch_main_q_port 发消息 → RunLoop 被 Source1 唤醒 → 执行 block。
# 5.3 自定义Source实战
// 自定义 Source0 —— 手动触发
CFRunLoopSourceContext context = {0};
context.perform = MySourcePerformCallback;
context.info = (__bridge void *)self;
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 触发(通常在另一个线程里):
CFRunLoopSourceSignal(source); // 标记为"有待处理"
CFRunLoopWakeUp(CFRunLoopGetCurrent()); // 唤醒 RunLoop
# 6. Timer——定时器
# 6.1 NSTimer与CFRunLoopTimerRef
NSTimer 是对 CFRunLoopTimerRef 的 ObjC 封装,底层完全一致。
// NSTimer 创建的三条路:
// 方式 1:加到 Default Mode(自动 run)
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *t) {}];
// 方式 2:手动选择 Mode
NSTimer *t = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *t) {}];
[[NSRunLoop mainRunLoop] addTimer:t forMode:NSRunLoopCommonModes];
// 方式 3:直接走 CFRunLoop
CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(
kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1.0, 1.0, 0, 0, ^(CFRunLoopTimerRef t) {}
);
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
CFRelease(timer);
# 6.2 Timer不准的真相
疑惑:为什么 NSTimer 经常不准,比如设 1 秒间隔,实际可能 1.1 秒才触发?
论证:三个原因叠加——
Mode 切换导致跳过(本篇主线案例):滑动时 RunLoop 在 Tracking Mode,Default Mode 的 Timer 不会被触发——直到切回去才一次性执行。
RunLoop 单次循环耗时过长:如果某个 Source0 回调(比如一个大 cell 的布局计算)耗了 50ms,那所有 Timer 就晚了 50ms。
mach_msg睡眠时的 timeout 参数由 "距离最近 Timer 的剩余时间" 决定,但如果 RunLoop 在处理 Source0 时耗时太长,根本来不及睡就被迫触发 Timer。Timer 不是实时定时器——它没有抢占能力,RunLoop 在当前循环没执行完之前,不会切换到 Timer 处理。
时间线(设 Timer 应在 T=1000ms 触发):
T=0 RunLoop 开始循环
T=20 处理 Source0(cell 布局,耗时 80ms)
T=100 继续处理 Source0...
T=950 处理 Blocks...
T=990 处理 Source0(又一个触摸事件,耗时 50ms)
T=1040 终于处理完 Source0
T=1040 发现 Timer 已经超时 40ms ⚠️ → 立刻执行(晚了 40ms)
精确计时方案:
// ❌ 不准:NSTimer
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{ ... }];
// ✅ 准:GCD Timer(不依赖 RunLoop 的 Mode)
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.01 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
// 高精度定时任务
});
dispatch_resume(timer);
| 对比项 | NSTimer | CADisplayLink | GCD Timer |
|---|---|---|---|
| 精度 | 低(受 RunLoop Mode + 循环耗时影响) | 高(跟着屏幕刷新率) | 最高 |
| 用途 | 低频定时(秒级) | UI 刷新相关(每帧) | 高精度计时 |
| RunLoop 依赖 | ✅ 完全依赖 | ✅ 加到 RunLoop | ❌ 独立于 RunLoop |
| Mode 影响 | ⚠️ 切换 Mode 会跳过 | 同 NSTimer | 不受影响 |
# 6.3 CADisplayLink原理
CADisplayLink 本质是一个绑定 V-Sync(屏幕刷新信号)的 Timer:
CoreAnimation 在 RunLoop 中注册了一个 Observer(kCFRunLoopBeforeWaiting)
│
▼
这个 Observer 检测到屏幕刷新信号 → 触发 CADisplayLink 回调
│
▼
回调里通常会执行 CALayer 的渲染准备(prepare 阶段)
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self
selector:@selector(frameUpdate:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (void)frameUpdate:(CADisplayLink *)link {
// link.duration = 每帧时长(60 FPS → 16.67ms;120 FPS → 8.33ms)
// link.timestamp = 当前帧的时间戳
// link.targetTimestamp = 下一帧的预期时间戳
}
为什么 CADisplayLink 在滑动时不会暂停? 因为它也是一个 RunLoop Timer,受 Mode 影响。如果用 NSDefaultRunLoopMode,滑动时照样暂停;必须加 NSRunLoopCommonModes。
# 6.4 GCD Timer vs NSTimer
| 维度 | NSTimer | GCD dispatch_source timer |
|---|---|---|
| 是否依赖 RunLoop | ✅ 绑定到特定 RunLoop + Mode | ❌ 独立运行 |
| 滑动时暂停? | 如果不在 Tracking/Common Mode 下会 | ❌ 不会 |
| 循环引用风险 | ✅(target 强引用) | ✅ Block 捕获 self(需 weak-strong dance) |
| 精度 | 低(受 RunLoop 影响) | 高(纳秒级) |
| 可暂停/恢复 | ❌(只能 invalidate 一次性销毁) | ✅ dispatch_suspend / dispatch_resume |
iOS 10+ block API | ✅ 支持 | ✅ Block 是原生方式 |
面试常见追问:既然 GCD Timer 更准,为什么 App 里还大量用 NSTimer? 答:因为很多场景不需要高精度(比如"30 秒后自动锁屏"),NSTimer 的 API 更简单。需要精确计时的场景(秒杀倒计时、动画帧、游戏主循环),必须用 GCD Timer 或 CADisplayLink。
# 7. Observer——观察者
# 7.1 六大活动状态
CFRunLoop 提供了 6 种活动状态(CFRunLoopActivity):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 进入 RunLoop(每个周期一次)
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source0
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中醒来
kCFRunLoopExit = (1UL << 7), // 退出 RunLoop
};
系统注册的 Observer 实例:
| Observer | 监听时机 | 做什么 |
|---|---|---|
| AutoreleasePool | Entry + BeforeWaiting + Exit | push / pop 自动释放池(第 14 篇详解) |
| CoreAnimation | BeforeWaiting + AfterWaiting | 提交 UI 渲染事务 |
| UIGestureRecognizer | AfterWaiting | 处理手势识别状态机 |
| GCD 主队列 | BeforeWaiting | 检查 dispatch main queue 有无任务 |
AutoreleasePool 与 RunLoop 的关系(简化版):
RunLoop Entry:
→ push AutoreleasePool(创建新池)
每次 RunLoop BeforeWaiting:
→ pop 旧池,释放本轮产生的临时对象
→ push 新池
RunLoop Exit:
→ pop 最后一个池
这就是为什么"方法里的临时对象不会无限累积"——每个 RunLoop 周期结束时 AutoReleasePool 都会释放它们。
# 7.2 卡顿监控方案
原理:卡顿 = 主线程 RunLoop 的一次循环耗时过长。利用 Observer 可以在 BeforeSources 和 AfterWaiting 之间测耗时。
业界经典方案——子线程 Ping 主线程:
@interface LagMonitor : NSObject
@property (nonatomic, assign) BOOL isMonitoring;
@end
@implementation LagMonitor
- (void)startMonitoring {
self.isMonitoring = YES;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (self.isMonitoring) {
__block BOOL mainThreadResponded = NO;
// 1. 往主线程丢一个任务
dispatch_async(dispatch_get_main_queue(), ^{
mainThreadResponded = YES;
});
// 2. 等待主线程响应
[NSThread sleepForTimeInterval:0.05]; // 50ms 超时阈值
// 3. 检查
if (!mainThreadResponded) {
// 💀 主线程 50ms 没有响应 → 发生了卡顿!
[self recordLagWithStack];
}
}
});
}
- (void)recordLagWithStack {
// 取主线程的 call stack
NSArray *stack = [NSThread callStackSymbols];
// 上报到 APM 平台
}
@end
RunLoop Observer 方案(更精确):
static void runLoopObserverCallback(CFRunLoopObserverRef observer,
CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopAfterWaiting: // 刚被唤醒
// 记录时间 T1
break;
case kCFRunLoopBeforeWaiting: // 即将休眠
// 记录时间 T2
// 如果 T2 - T1 > 50ms → 发生卡顿!
break;
default: break;
}
}
// 注册
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
kCFRunLoopAfterWaiting | kCFRunLoopBeforeSources,
YES, 0,
&runLoopObserverCallback, NULL
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
两种方案对比:
| Ping 主线程 | RunLoop Observer | |
|---|---|---|
| 精度 | 受后台线程调度影响 | 精确到 RunLoop 状态切换 |
| 开销 | 每 50ms 一次 dispatch | 每次 RunLoop 循环都回调 |
| 能监控卡顿时长 | ❌ 只能知道"卡了" | ✅ 能算精确耗时 |
| 能定位卡在哪个阶段 | ❌ | ✅(在 BeforeTimers 还是 BeforeSources 之间) |
# 7.3 启动耗时监控
App 启动分为 pre-main(dyld 加载)和 main 函数之后(到首屏渲染完成)。利用 RunLoop Observer 可以精确测量 "main 之后到首帧渲染" 的耗时:
// AppDelegate.m
__block CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopBeforeWaiting, // 监控「即将休眠」
YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 此时 RunLoop 完成了第一轮 UI 渲染,即将休眠
NSTimeInterval launchTime = [[NSDate date] timeIntervalSinceDate:appStartTime];
NSLog(@"🔥 App 启动完成,耗时: %.2f ms", launchTime * 1000);
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
这里的原理是:RunLoop 在"完成 UI 渲染提交 → 即将休眠"之间说明本轮事件全部处理完了,首帧已经渲染。
# 8. RunLoop与UI刷新
# 8.1 为什么滑动时NSTimer暂停
回到主线案例的第一个原理解答:
// 默认行为
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:YES block:^{
// 只在 NSDefaultRunLoopMode 下触发
}];
// 手指触摸屏幕时:
// 系统调用 CFRunLoopRunInMode(UITrackingRunLoopMode, ...)
// 此 Timer 在 Default Mode 下 → 本轮不处理 ❌
// 手指离开时:
// 系统切回 NSDefaultRunLoopMode
// Timer 被重新纳入处理 → 发现已超时 → 立即触发 ✅
//
// 这就是"滑动时暂停,松手后立即恢复"的完整解释
# 8.2 滑动性能优化三板斧
板斧 1:Timer 加 CommonModes
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 滑动时 Timer 也正常触发 ▶ 代价:Timer 回调会跟渲染抢 CPU
板斧 2:利用 Mode 切换延迟非紧急任务
// 滑动时不做,松手后做
if ([runLoop currentMode] == NSDefaultRunLoopMode) {
[self loadImageForVisibleCells]; // 只在空闲时加载
}
板斧 3:利用 Observer 实现空闲时执行
// 监听 kCFRunLoopBeforeWaiting → 说明主线程即将空闲
// 在这时做预加载、预计算
# 8.3 利用空闲时机预计算
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopBeforeWaiting, // ← 关键:RunLoop 即将休眠(主线程空闲)
YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 主线程即将空闲,做点不紧急的事
[self preloadNextBatchOfCells];
}
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
// ↑
// 只在 Default Mode:滑动时不触发,不抢渲染资源
经典案例——UITableView 的分批计算:
// 列表有 10000 条数据,每条需要做复杂排版计算
// 一次性算完会卡 3 秒 → 用户无法交互
// 方案:每次 RunLoop 空闲时算 10 条,1000 次空闲就把 10000 条算完
__block int index = 0;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopBeforeWaiting, YES, 0,
^(CFRunLoopObserverRef obs, CFRunLoopActivity act) {
int batchSize = 10;
for (int i = 0; i < batchSize && index < 10000; i++, index++) {
[self calculateLayoutForRow:index];
}
if (index >= 10000) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), obs, kCFRunLoopDefaultMode);
CFRelease(obs);
}
}
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
这个方案的本质是 "把一个大任务拆成若干个小任务,分摊到多个 RunLoop 空闲周期执行"——用户滑动时 RunLoop 不空闲,就不会挤占 UI 渲染时间。
# 9. 线程保活与常驻线程
# 9.1 为什么子线程的RunLoop默认关闭
疑惑:我 dispatch_async 到一个子线程,为什么 [NSTimer scheduledTimerWith...] 不会触发?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 此时子线程的 RunLoop 还没有 run!
NSTimer *t = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{
NSLog(@"这永远不会打印"); // ❌ Timer 加到空的 RunLoop 上
}];
// 线程马上结束了,RunLoop 被销毁 → Timer 跟着销毁
});
论证:子线程创建后,RunLoop 对象在你第一次 CFRunLoopGetCurrent() 时创建,但没有人调用 run —— RunLoop 处于"创建但未启动"状态。没有 RunLoop 循环,Timer 永远没机会被处理。
# 9.2 常驻线程的正确写法
方案 A:CFRunLoopRun() 永远不退出
- (void)startLiveThread {
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(threadEntry)
object:nil];
[thread start];
}
- (void)threadEntry {
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 必须至少添加一个 Source/Timer,否则 RunLoop 立刻退出!
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
CFRunLoopRun(); // ← 永远不会返回,除非调用 CFRunLoopStop()
}
}
// 在这个线程上执行任务
- (void)executeOnLiveThread:(void(^)(void))block {
[self performSelector:@selector(executeBlock:)
onThread:self.liveThread
withObject:block
waitUntilDone:NO];
}
// 销毁
- (void)stopLiveThread {
CFRunLoopStop(CFRunLoopGetCurrent()); // 退出 CFRunLoopRun()
}
关键:必须往 RunLoop 里添加至少一个 Source/Timer——空的 RunLoop 一 run 就立刻退出(因为"没有事件源,没有存在的意义")。
方案 B:runMode:beforeDate: 可控循环
@property (atomic, assign) BOOL shouldKeepRunning;
- (void)threadEntry {
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (self.shouldKeepRunning && [runLoop runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]]) {
// runMode:beforeDate: 在以下情况返回:
// 1. 处理了一个事件且 returnAfterSourceHandled = YES
// 2. 超时
// 3. 没有事件源(这种情况直接退出 loop)
}
}
}
# 9.3 AFNetworking 2.0的经典案例
AFNetworking 2.0 的 AFURLConnectionOperation 使用了常驻线程来处理网络回调:
// AFNetworking 简化版
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; // 保活
[runLoop run];
}
}
为什么 AFN 2.0 需要常驻线程? NSURLConnection 的回调是基于 RunLoop 的,如果把 delegate 回调切到后台线程,那个线程必须有活跃的 RunLoop 来接收回调。
📌 AFN 3.0+ 默认使用
NSURLSession,基于 GCD,不再需要常驻线程——这是 Apple 网络栈架构演进的一个体现:从 "RunLoop 驱动" 变成 "GCD 驱动"。
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到第 1 章的广告轮播暂停,七个疑问现在逐条作答:
| 疑问 | 答案 |
|---|---|
| ① RunLoop 大循环长什么样? | 第 3.1:BeforeTimers → Sources → BeforeWaiting → mach_msg 休眠 → AfterWaiting → 处理唤醒原因 |
| ② 什么是 Mode?为什么切 Mode? | 第 4 章:Mode 是 Source/Timer/Observer 的分组容器,切换 Mode 实现主线程 CPU 优先级调度 |
| ③ NSTimer 不准的根因? | 第 6.2:Mode 切换跳过、RunLoop 循环耗时长、Timer 无抢占能力 — 三重叠加 |
| ④ Source0/Source1 分别是什么? | 第 5 章:Source0 手动触发(触摸事件)、Source1 基于 Mach Port(跨线程通信) |
| ⑤ Observer 能观测哪些状态? | 第 7.1:Entry / BeforeTimers / BeforeSources / BeforeWaiting / AfterWaiting / Exit |
| ⑥ 子线程默认为什么没 RunLoop?怎么保活? | 第 9 章:首次 currentRunLoop 才创建但不自动 run;加 Source/Timer + CFRunLoopRun() 保活 |
| ⑦ 怎么用 RunLoop 做卡顿监控? | 第 7.2:Ping 主线程法 / Observer 测 BeforeSources→BeforeWaiting 耗时 |
四种修复方案(按推荐排序):
方案 A:Timer 加到 CommonModes(最简单)
NSTimer *t = [NSTimer timerWithTimeInterval:3.0 repeats:YES block:^(NSTimer *timer) {
[self scrollToNext];
}];
[[NSRunLoop mainRunLoop] addTimer:t forMode:NSRunLoopCommonModes];
代价:滑动时 Timer 也会触发(可能跟 UI 渲染抢 CPU),但因为广告轮播耗时极少,几乎无影响。
方案 B:用 GCD Timer 替代 NSTimer
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC, 0.01 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{ [self scrollToNext]; });
dispatch_resume(timer);
// 不依赖 RunLoop Mode,滑动时照样触发 ✅
代价:GCD Timer 不能被 invalidate 后重用,需要重新创建。
方案 C:利用 RunLoop Observer,空闲时执行非紧急任务
CFRunLoopObserverRef obs = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 主线程空闲时才做轮播 —— 不跟渲染抢资源
if ([[NSRunLoop currentRunLoop].currentMode isEqual:NSDefaultRunLoopMode]) {
[self scrollToNextIfNeeded];
}
}
);
代价:Observer 每次 RunLoop 休眠前都触发,频率比 3 秒一次的 Timer 高得多,轮播逻辑需要自己做时间间隔判断。
方案 D:利用 performSelector 递归调用(避免 Timer 对象)
- (void)scheduleScroll {
[self performSelector:@selector(scrollToNextAndSchedule)
withObject:nil
afterDelay:3.0
inModes:@[NSRunLoopCommonModes]];
}
- (void)scrollToNextAndSchedule {
[self scrollToNext];
[self scheduleScroll]; // 递归调度下一次
}
此方案本质是把 Timer 变成一个不断重设的 afterDelay 调用——省去了持有 Timer 对象,但每次都要重新加到 RunLoop。
# 10.2 一个RunLoop周期的一生
把主线程 RunLoop 一个完整周期串成知识树:
RunLoop 一个周期:
│
├─ Entry:通知 Observer(AutoreleasePool push)
│
├─ BeforeTimers:通知 Observer
│ └─ 此时可以检查哪些 Timer 即将到期
│
├─ BeforeSources:通知 Observer
│ └─ 准备好处理 Source0
│
├─ 处理 Source0:
│ ├─ 触摸事件 → _UIApplicationHandleEventQueue
│ ├─ performSelector:withObject:afterDelay:
│ └─ 其他 Source0 回调
│
├─ 处理 Blocks:__CFRunLoopDoBlocks
│ └─ CFRunLoopPerformBlock 注册的 block
│
├─ 检查 Source1(Mach Port)────────────────── 第 5 章
│ ├─ 有消息 → 立即处理 → 回到循环开头
│ └─ 无消息 ↓
│
├─ BeforeWaiting:通知 Observer ────────────── 第 7 章
│ ├─ AutoreleasePool pop → push
│ ├─ CoreAnimation 提交渲染事务
│ └─ 自定义 Observer(卡顿监控/空闲任务)
│
├─ 休眠:mach_msg(timeout = 距最近 Timer 的时间) ⏸️
│ ├─ 被 Timer 唤醒
│ ├─ 被 Source1(Mach Port)唤醒
│ ├─ 被 GCD 主队列任务唤醒
│ └─ 被 CFRunLoopWakeUp 手动唤醒
│
├─ AfterWaiting:通知 Observer ─────────────── 第 7 章
│ └─ 自定义 Observer 可以计算卡顿耗时
│
├─ 根据唤醒原因处理:────────────────────────── 第 3/5/6 章
│ ├─ Timer 唤醒 → __CFRunLoopDoTimers
│ ├─ Source1 唤醒 → __CFRunLoopDoSource1
│ └─ GCD 唤醒 → 执行 dispatch main queue block
│
└─ 循环回到 BeforeTimers
一个完整周期 = 1 RunLoop iteration。App 不退出 = 这个循环永不停止。
# 10.3 设计哲学回扣
整理本篇的四条跨篇适用的设计哲学:
哲学 1:事件驱动,不是轮询——mach_msg 休眠是 RunLoop 的灵魂
RunLoop 的"节能"来自 mach_msg 在内核态阻塞等待——没有事件时 CPU 真正停下来,被硬件中断唤醒才继续。这和 while(1){poll()} 的"耗时检查"有天壤之别。事件驱动 = 高响应 + 低功耗——这是 iOS 省电的根基之一。
哲学 2:优先级隔离——Mode 是对主线程 CPU 时间的调度器
为什么 iOS 滑动比 Android 流畅?不是因为硬件更强,而是因为 RunLoop 的 Mode 机制把"UI 渲染"和"业务定时任务"隔离开——滑动时业务 Timer 直接暂停,CPU 全部留给渲染。Mode = 不抢不拖的优先级调度。
哲学 3:组合优于继承——Observer 是 RunLoop 的"插件系统"
AutoreleasePool、CoreAnimation、手势识别——这些基石级功能都不是 RunLoop 内部硬编码的,而是通过 Observer 机制"插"进去的。Observer 让 RunLoop 保持简洁,同时允许任意系统模块在不改动 RunLoop 源码的前提下"监听"它的状态切换。这是 iOS 系统设计中最优雅的扩展点。
哲学 4:按需创建,用完即毁——RunLoop 的生命周期绑定线程
子线程的 RunLoop 默认不创建;创建了不加 Source/Timer 就立刻退出;线程消亡 RunLoop 跟着销毁。RunLoop 不是"常驻守护者"而是"用完即弃的工具人"。这种设计避免了无意义的内存占用——只有需要消息循环的线程才付出代价。
# 10.4 RunLoop速查表
| 概念 | 核心要点 | 关键 API |
|---|---|---|
| RunLoop 主循环 | BeforeTimers → Sources → BeforeWaiting → 休眠 → AfterWaiting | CFRunLoopRun() |
| Mode | Source/Timer/Observer 的分组容器,一次只运行一个 Mode | NSDefaultRunLoopMode / UITrackingRunLoopMode / NSRunLoopCommonModes |
| Source0 | 触摸事件、performSelector,手动触发 | CFRunLoopSourceSignal + CFRunLoopWakeUp |
| Source1 | Mach Port 消息,自动唤醒 RunLoop | 系统内部使用 / GCD dispatch main queue |
| Timer | 定时触发回调,精度受 RunLoop 影响 | NSTimer / CFRunLoopTimerRef |
| Observer | 监听 RunLoop 状态切换(6 种) | CFRunLoopObserverCreate |
| 线程保活 | 加 Source/Timer + 调用 run | CFRunLoopRun() / runMode:beforeDate: |
| 休眠机制 | mach_msg 在内核等待,节能 | 自动(无需手动) |
| AutoreleasePool | 在每个 RunLoop 周期开始时 push,休眠前 pop | 系统自动管理 |
60 秒诊断命令清单:
// 1. 看某个线程的 RunLoop 是否有 source
CFRunLoopCopyAllModes(CFRunLoopGetCurrent());
// 2. 看主线程 RunLoop 当前 Mode
NSLog(@"%@", [[NSRunLoop mainRunLoop] currentMode]);
// 3. 看某个 RunLoop 下有哪些 Timer
NSArray *timers = (__bridge NSArray *)CFRunLoopCopyAllTimers(rl, mode);
// 4. 打印 RunLoop 状态
CFRunLoopObserverCreateWithHandler(..., kCFRunLoopAllActivities, ..., ^(...){
NSLog(@"活动: %lu", activity);
});
// 5. Instruments → Time Profiler → 看主线程的 mach_msg_trap 占比
// mach_msg_trap 占比高 = RunLoop 大部分时间在休眠(App 空闲)
// mach_msg_trap 占比低 = App 一直在忙(可能卡顿)
// 6. lldb 看当前 RunLoop Mode
(lldb) po [[NSRunLoop mainRunLoop] currentMode]
NSTimer 选择速查:
需要精确计时(秒杀、倒计时)?
├─ 是 → GCD dispatch_source timer
└─ 否 → 需要跟着屏幕刷新(动画)?
├─ 是 → CADisplayLink
└─ 否 → NSTimer(方便)
└─ 是否需要在滑动时生效?
├─ 是 → addTimer:forMode:NSRunLoopCommonModes
└─ 否 → scheduledTimerWithTimeInterval:(default)
下一篇:你已经知道 RunLoop 如何在每个周期里调度触摸、Timer、动画和绘图,下一章进入 04.UIView生命周期与渲染 (opens new window)——把 "一次
setNeedsDisplay从调用到像素亮起来的完整链路" 从 CoreAnimation → Render Server → GPU 级别彻底剖开。