概述:
RunLoop 是一个核心的 iOS 机制,它是 事件循环机制 的实现,负责管理线程的执行和调度。RunLoop 能够持续监听输入事件(如用户触摸、定时器、网络请求等)并分发给相应的处理方法,从而保持应用程序持续运行。
核心功能:
- 处理异步事件:
RunLoop可以管理事件源,比如定时器、触摸事件、网络响应等,确保这些事件在合适的时机被触发。 - 控制线程执行: 通过控制线程是否处于休眠或运行状态,
RunLoop能够让线程处于活跃状态,并等待事件的发生。 - 保持线程活跃: 如果没有事件需要处理,
RunLoop会让线程进入休眠状态,节省系统资源。
1. 为什么需要 RunLoop?
背景:
在多线程编程中,主线程(UI线程)必须保持活跃,才能响应用户的操作和刷新界面。没有 RunLoop,即便你创建了一个线程,也不能有效地等待和处理外部事件。
- UI更新:当我们触发按钮点击等操作时,事件会通过
RunLoop传递给相应的处理方法,确保 UI 在需要的时候得到更新。 - 后台任务:在后台线程处理任务时,我们也需要
RunLoop来监听定时器、网络请求等异步任务。
没有 RunLoop,线程就像失去了生命,即使任务没完成,它也会直接退出。
2. RunLoop 的基本工作原理
基本流程:
- RunLoop 启动:线程启动后,会进入
RunLoop循环。 - 等待事件:
RunLoop会等待外部事件的发生,比如触摸事件、定时器触发、网络回调等。 - 事件处理:当事件发生时,
RunLoop会将事件分发到对应的处理方法(例如响应按钮点击事件、定时器回调等)。 - 继续运行:当事件处理完后,
RunLoop会继续等待新的事件,或者退出。
RunLoop 状态:
- 休眠状态:当没有事件需要处理时,
RunLoop处于休眠状态。休眠时,线程不会消耗过多的 CPU 资源。 - 活跃状态:当有事件发生时,
RunLoop被唤醒,进入活跃状态来处理这些事件。
3. 常见用法
(1) 主线程的 RunLoop
主线程的 RunLoop 是自动开启的,常用于UI事件处理和异步任务的等待。
例如,在处理定时器时,你可能希望在主线程中定期执行某些操作:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateUI) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- 定时器会定期触发
updateUI方法。 NSRunLoopCommonModes确保定时器在滚动、拖动等事件中也能继续触发。
(2) 自定义线程的 RunLoop
对于自定义线程(非主线程),你需要手动创建并启动 RunLoop:
- (void)startCustomThread {
NSThread *customThread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
[customThread start];
}
- (void)runThread {
@autoreleasepool {
// 创建并启动 RunLoop
[[NSRunLoop currentRunLoop] run]; // 必须显式调用,线程才能持续运行
}
}
[[NSRunLoop currentRunLoop] run] 启动线程的 RunLoop,使其持续运行,处理事件。
(3) 使用 RunLoop 处理定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(handleTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- 这样可以确保定时器的回调不会被滚动等事件阻塞。
NSRunLoopCommonModes模式确保在用户滚动屏幕时,定时器依然可以触发。
4. 解决实际问题:
(1) 主线程阻塞问题
RunLoop 可以有效避免主线程阻塞。很多时候我们需要执行耗时操作(比如网络请求),但不能让主线程被阻塞,否则界面无法响应。
- (void)fetchData {
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// 网络请求完成后,更新 UI
dispatch_async(dispatch_get_main_queue(), ^{
// 更新 UI
});
}];
[task resume];
}
- 通过
dispatch_async把 UI 更新放到主线程,避免主线程阻塞。 - 事件循环 让主线程保持活跃,直到请求完成,才处理回调。
(2) 后台任务与定时器的配合
当我们在后台线程执行定时任务时,必须确保线程运行不被中断。使用 RunLoop 能保证后台线程的活跃:
- (void)performBackgroundTask {
[NSThread detachNewThreadSelector:@selector(executeBackgroundTask) toTarget:self withObject:nil];
}
- (void)executeBackgroundTask {
@autoreleasepool {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(backgroundTask) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]; // 保持线程活跃
}
}
- (void)backgroundTask {
NSLog(@"Background task executed");
}
通过 RunLoop 保证定时器在后台线程中正常触发,否则后台线程执行完毕后会自动退出,定时器就会停止。
5. 注意事项与优缺点
优点:
- 节省系统资源:通过在没有事件时让线程进入休眠状态,
RunLoop可以有效减少不必要的 CPU 消耗。 - 异步任务管理:能够很方便地在多线程中管理事件源(如定时器、网络请求等),确保线程在等待事件时不会结束。
- 灵活的线程控制:
RunLoop提供了灵活的方式来控制线程,确保线程在处理事件时能一直活跃。
缺点:
- 不能主动退出:
RunLoop只会在有事件时才会退出,如果没有事件源,线程会一直保持休眠状态,因此我们需要手动管理退出条件。 - 性能问题:过多的事件源可能会增加线程的调度开销,影响应用的性能。
- 内存管理:
RunLoop本身并不负责对象的内存管理,所以在使用时要注意内存泄漏(例如通过定时器等引用)。
一、RunLoop 的核心知识
1. RunLoop 的五个主要阶段
-
kCFRunLoopEntry(进入)
-
kCFRunLoopBeforeTimers(处理定时器)
-
kCFRunLoopBeforeSources(处理输入事件、UI 事件、手势、触摸、逻辑等)
-
kCFRunLoopBeforeWaiting(准备睡眠,提交渲染)
-
kCFRunLoopAfterWaiting(被唤醒)
-
kCFRunLoopExit(退出)
2. RunLoop 的几种模式
|
模式名称 |
描述 |
|---|---|
|
kCFRunLoopDefaultMode |
主线程 UI 默认模式(动画、UI 事件等) |
|
UITrackingRunLoopMode |
ScrollView 滑动时使用,优先处理滑动事件 |
|
kCFRunLoopCommonModes |
一个集合,会自动包含 Default + Tracking |
主线程 RunLoop 通常运行在 kCFRunLoopDefaultMode
滑动时临时切到 UITrackingRunLoopMode。
二、页面更新、动画、渲染原理
1. CPU / GPU 的三阶段流水线
每一帧都分为三个行为:
帧 n:CPU 处理(布局、事件、动画计算)
帧 n:提交渲染指令
帧 n:GPU 渲染上一帧内容(帧 n-1)
帧 n:显示上一帧内容(帧 n-2)
所以 GPU 和 CPU 并不共享同一帧的时间,是“阶梯式并行”。
2. kCFRunLoopBeforeSources 能否导致卡顿?
能,主要因为:
-
UI 布局(AutoLayout)
-
文本绘制计算
-
大量对象分配、销毁
-
JSON 解析
-
手势响应逻辑
-
主线程阻塞(锁等待)
虽然渲染在 GPU,但 渲染之前的所有“准备”工作都由 CPU 做。如果这一阶段 CPU 超过帧预算(16.67ms),就会卡。
三、卡顿原理(CPU/GPU 双方向)
1. CPU 卡顿
-
如果某一帧 CPU 用了 20ms > 16.67ms
-
GPU 没法按时拿到渲染指令
-
直接掉帧
2. GPU 卡顿
-
如果某一帧 GPU 渲染很慢(如大量模糊、阴影、离屏渲染)
-
GPU 也无法按时完成
-
同样掉帧
更高帧率 = 时间预算更少,CPU 和 GPU 压力显著增大。
不过 iOS 会自动降帧(ProMotion),不一定每次都跑 120Hz。
五、CPU 是否会“休息”?
场景:CPU 本帧只用 5ms
-
剩下的 11.67ms 主线程会休眠,等待下一次 VSync 或事件唤醒。
CPU 本帧用了 20ms(大于 16ms)
-
不会等两帧才继续
-
处理完当帧后立即进入下一次事件循环
-
而不是按 VSYNC 时间排队执行
VSync 的队列机制
-
没有任务排队机制
-
VSync 只是一个“屏幕刷新信号”
-
CPU 超过帧预算 → 掉帧,而不是积压任务
六、Timer(NSTimer / CFRunLoopTimer)行为
1. kCFRunLoopBeforeTimers 阶段的主要耗时是?
是定时器的回调执行时间。
2. 卡顿会导致 Timer 不准吗?会!
Timer 不是基于硬件时钟,是基于 RunLoop 事件驱动:
-
RunLoop 忙 → Timer 延迟
-
Timer 回调会比实际时间明显滞后
3. Timer 延迟后的行为(重点)
会采用:回调一次 + “延迟执行”
不会补偿,也不会自动“计算时间差”来保持精度。
例子:需要 1 秒执行一次,如果 CPU 卡顿 2 秒,第二次触发时间会直接变成 3 秒,不会补发“2 次补偿回调”。
|
领域 |
核心结论 |
|---|---|
|
RunLoop 阶段 |
BeforeSources 执行逻辑和 UI 事件,是 CPU 卡顿重灾区 |
|
渲染流程 |
CPU → 提交指令 → GPU 渲染上一帧 |
|
卡顿原因 |
CPU 重逻辑 或 GPU 重渲染 |
|
120Hz |
帧预算减半,CPU/GPU 压力更高 |
|
CPU 是否休息 |
会休眠等待下一帧,如果耗时过长则掉帧 |
|
VSync 队列 |
没有任务排队机制 |
|
Timer 行为 |
不补偿,不自动对齐系统时间;卡顿会导致回调延迟 |
|
如何精确计时 |
需要自己用时间戳计算差值 |
iOS 卡顿的本质:主线程卡住了
当主线程 RunLoop 在某一阶段(大多是 BeforeSources)超过 一帧预算:
-
60 FPS → 16.67ms
-
120 FPS → 8.33ms
就会出现卡顿(掉帧)。
所以卡顿监控核心就是:
盯住主线程是否连续多帧没有进入下一次 RunLoop 切换
卡顿监控RunLoop Observer
监控 RunLoop 是否卡在某个阶段太久。
📌 原理
-
给主线程 RunLoop 添加 CFRunLoopObserver
-
监听阶段(尤其是 BeforeSources、BeforeWaiting)
-
如果两个阶段之间超过阈值(50ms、100ms)→ 卡顿
3427

被折叠的 条评论
为什么被折叠?



