当前位置: 首页 > news >正文

iOS Runloop 深度解析

iOS Runloop 面试深度解析

一句话定义:Runloop 是一个有事干就干、没事就睡觉的事件驱动循环,它让线程不退出,同时做到 CPU 零空转。


目录

  1. Runloop 是什么

  2. Runloop 与线程的关系

  3. 核心数据结构

  4. Mode 机制

  5. Source / Timer / Observer

  6. 完整运行流程

  7. 休眠原理(亮点)

  8. Runloop 与 AutoreleasePool

  9. Runloop 与 NSTimer

  10. Runloop 与 GCD

  11. Runloop 与事件响应

  12. 实战应用场景

  13. 高频面试题精解


1. Runloop 是什么

本质:有条件的 do-while

// 伪代码还原 Runloop 内核 int retVal = 0; do { // 1. 通知 Observer:即将处理 Timer/Source // 2. 处理到来的事件(Touch / Timer / Source) // 3. 没有事件 → 调用 mach_msg 陷入内核态睡眠 // 4. 被唤醒 → 处理唤醒原因 retVal = XCFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle); } while (retVal != kCFRunLoopRunStopped && retVal != kCFRunLoopRunFinished);

与普通 while(1) 的本质区别

普通死循环Runloop
CPU 占用100%(忙等)接近 0%(睡眠)
唤醒方式无,一直跑mach_msg 内核事件唤醒
适用场景不适合长时间运行线程保活、事件驱动

深度:Runloop 的"睡眠"不是sleep(),而是通过mach_msg_trap陷入内核态,CPU 真正分配给其他进程,这是它的核心价值所在。


2. Runloop 与线程的关系

一一对应,懒加载

线程 ←——对应——→ Runloop(存储在全局 CFMutableDictionaryRef 中,key=线程,value=Runloop)
  • 主线程:App 启动时由系统自动创建并运行 Runloop(UIApplicationMain内部调用)

  • 子线程:默认没有 Runloop,第一次调用[NSRunLoop currentRunLoop]时才懒加载创建

  • 子线程 Runloop 不会自动运行:需要手动调用run/runUntilDate:/runMode:beforeDate:

为什么主线程不会退出?

因为主线程的 Runloop 在UIApplicationMain里被启动,内部是一个永不退出的循环,App 的生命周期就等于这个循环的生命周期。


3. 核心数据结构

Runloop 相关结构全部定义在CoreFoundation框架(开源)中:

CFRunLoop ├── pthread_t thread // 对应线程 ├── CFMutableSetRef modes // 所有注册的 Mode ├── CFRunLoopModeRef currentMode // 当前运行的 Mode(只能跑一个) └── CFMutableSetRef commonModes // 标记为 Common 的 Mode 集合 │ └── CFRunLoopMode ├── CFMutableSetRef sources0 // Source0 集合 ├── CFMutableSetRef sources1 // Source1 集合 ├── CFMutableArrayRef timers // Timer 集合 └── CFMutableArrayRef observers // Observer 集合

关键:Runloop 在同一时刻只能运行在一个 Mode 下,切换 Mode 需要先退出当前 Mode,再重新进入新 Mode。这个设计是 NSTimer 在滚动时失效的根本原因。


4. Mode 机制

系统内置 Mode

Mode 名称常量触发场景
DefaultkCFRunLoopDefaultMode/NSDefaultRunLoopMode默认,App 空闲时
TrackingUITrackingRunLoopModeScrollView 滚动时
Initialization_kCFRunLoopInitializationModeApp 启动初始化
CommonkCFRunLoopCommonModes/NSRunLoopCommonModes伪 Mode,代表一组 Mode
EventReceiverGSEventReceiveRunLoopMode接收系统事件

CommonModes 不是一个真正的 Mode

kCFRunLoopCommonModes是一个标记集合,不是实际运行的 Mode。

当你把 Source/Timer 加入 CommonModes 时,实际上是把它加入到所有标记了 Common 的 Mode(Default + Tracking)中。

// 错误:Timer 在滚动时暂停 [NSTimer scheduledTimerWithTimeInterval:1.0 ...]; // 默认加入 Default Mode // 正确:滚动时也触发 [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

5. Source / Timer / Observer

Source0 —— 非内核驱动的输入源

  • 不基于 mach port,不能主动唤醒 Runloop

  • 需要先调用CFRunLoopSourceSignal标记为待处理,再调用CFRunLoopWakeUp手动唤醒

  • 典型场景:

  • performSelector:onThread:

  • UIKit 的触摸事件分发(IOKit的原始事件经 Source1 接收后,转交给 Source0 分发)

  • CFSocket回调

Source1 —— 基于 mach port 的输入源

  • 基于 mach port,可以主动唤醒 Runloop(内核直接发消息)

  • 典型场景:

  • 硬件事件(触摸、锁屏等)原始接收

  • 线程间通信(performSelector:onThread:的底层唤醒部分)

  • 各类系统 Port 事件

亮点:触摸事件的完整链路是IOKit → SpringBoard(Source1 唤醒 App) → Source0 → UIApplication → UIWindow → hitTest → 响应链

Timer

  • NSTimer/CADisplayLink本质都是CFRunLoopTimer

  • 时间精度受 Runloop 迭代耗时影响,不精确

  • 必须添加到 Runloop 某个 Mode 才能触发

Observer

监听 Runloop 的状态变化,共 6 种状态:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = 1 << 0, // 即将进入 Loop kCFRunLoopBeforeTimers = 1 << 1, // 即将处理 Timer kCFRunLoopBeforeSources = 1 << 2, // 即将处理 Source kCFRunLoopBeforeWaiting = 1 << 5, // 即将休眠 kCFRunLoopAfterWaiting = 1 << 6, // 从休眠中被唤醒 kCFRunLoopExit = 1 << 7, // 即将退出 Loop kCFRunLoopAllActivities = 0x0FFFFFFFU };

系统注册了哪些 Observer?

  • kCFRunLoopEntry:初始化 AutoreleasePool(push)

  • kCFRunLoopBeforeWaiting:释放旧的 AutoreleasePool(pop)并创建新的(push)

  • kCFRunLoopExit:释放 AutoreleasePool(pop)

  • kCFRunLoopBeforeWaiting:Core Animation 提交待渲染的图层树(CA::Transaction::commit


6. 完整运行流程

进入 Runloop │ ▼ ① 通知 Observer: kCFRunLoopEntry │ ▼ ② 通知 Observer: kCFRunLoopBeforeTimers │ ▼ ③ 通知 Observer: kCFRunLoopBeforeSources │ ▼ ④ 处理 Source0(非端口)事件 │ ▼ ⑤ 如果有 Source1 就绪 → 跳到第 ⑨ 步处理 │ ▼ ⑥ 通知 Observer: kCFRunLoopBeforeWaiting │ ▼ ⑦ 调用 mach_msg → 线程进入内核态休眠 💤 │ 等待唤醒(Timer 到期 / Source1 / 外部调用 CFRunLoopWakeUp / 超时) │ ▼ ⑧ 通知 Observer: kCFRunLoopAfterWaiting │ ▼ ⑨ 处理唤醒原因: ├── Timer 到期 → 触发 Timer 回调 ├── Source1 就绪 → 处理 Source1 └── 外部唤醒 → 处理其他来源 │ ▼ ⑩ 通知 Observer: kCFRunLoopExit(如果要退出) │ ▼ 循环回 ②,或退出

7. 休眠原理(亮点)

这是面试中拉开差距的关键点。

mach_msg_trap:从用户态到内核态

// CFRunLoop 源码简化 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); // 内部调用: mach_msg(msg, MACH_RCV_MSG, 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
  1. 调用mach_msg系统调用,CPU 从用户态切换到内核态

  2. 内核将线程挂起,放入等待队列

  3. 当指定 port 收到消息(Timer 触发 / 系统事件 / 主动唤醒),内核将线程从等待队列取出

  4. CPU 切回用户态,Runloop 继续处理

整个过程中线程真正休眠,CPU 不做任何工作,这是 Runloop 能让主线程同时保活且不耗电的根本机制。

与 sleep() 的区别

sleep()是用户态计时的忙等待变种;mach_msg是内核级挂起,CPU 直接调度到其他线程/进程,唤醒精度更高,功耗更低。


8. Runloop 与 AutoreleasePool

主线程 Runloop 注册了两个与 AutoreleasePool 相关的 Observer:

Observer 优先级最高(priority = 2147483647) kCFRunLoopEntry → _objc_autoreleasePoolPush() // 创建自动释放池 kCFRunLoopBeforeWaiting → _objc_autoreleasePoolPop() // 销毁旧池 + _objc_autoreleasePoolPush() // 创建新池 kCFRunLoopExit → _objc_autoreleasePoolPop() // 最终销毁

实际意义:

  • 每次 Runloop 即将休眠时,当前循环内所有autorelease对象被释放

  • 这就是为什么局部autorelease对象的生命周期能安全地撑过一次完整事件处理

  • 子线程没有自动的 AutoreleasePool,如果子线程中大量创建 OC 对象,需要手动创建@autoreleasepool {}


9. Runloop 与 NSTimer

NSTimer 不精确的原因

  1. Timer 依赖 Runloop,Runloop 每次循环耗时不固定

  2. 当前 Mode 下有耗时任务时,Timer 回调会延迟

经典问题:Timer 在 ScrollView 滚动时失效

// 原因:scheduledTimerWithTimeInterval 默认加入 NSDefaultRunLoopMode // 滚动时 Runloop 切换到 UITrackingRunLoopMode,Default Mode 中的 Timer 暂停 // 解法: NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // 同时加入 Default + Tracking

CADisplayLink

  • 本质也是CFRunLoopTimer,但绑定屏幕刷新(60/120Hz)

  • 同样受 Mode 影响,滚动时需要加入 CommonModes

子线程 Timer

// 子线程使用 Timer 必须手动跑 Runloop dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] run]; // 必须启动,否则 Timer 不触发 });

10. Runloop 与 GCD

GCD 和 Runloop 是独立的两套机制,但在主线程上有交集:

GCD dispatch_async(main_queue, block) → libdispatch 向主线程的 mach port 发送消息 → 唤醒主线程 Runloop(Source0 处理,但由 Source1 唤醒) → Runloop 在下一次循环处理这个 block

所以dispatch_async(main_queue)的 block 是在 Runloop 的一次循环中执行的,而不是"立即"执行。

Runloop 被 GCD 唤醒,处理的是 Source0(已标记为待处理),触发唤醒的是 libdispatch 的 port(Source1 范畴)。


11. Runloop 与事件响应

触摸事件完整链路

硬件触摸 → IOKit.framework 封装为 IOHIDEvent → SpringBoard(系统进程)通过 mach port 转发给 App → App 主线程 Source1(mach port)被唤醒 → Source1 回调将 IOHIDEvent 包装为 UIEvent,标记 Source0 为待处理 → Source0 回调调用 UIApplication.sendEvent: → UIWindow → hitTest:withEvent: → 响应链

面试亮点:触摸事件经过了两次 Source 转换(Source1 接收 → Source0 分发),这个细节能体现对 Runloop 原理的深度理解。

performSelector 与 Runloop

// 延迟执行,依赖 Runloop(子线程中若无 Runloop 则不执行) [self performSelector:@selector(foo) withObject:nil afterDelay:1.0]; // 跨线程执行,Runloop 不存在时无效 [self performSelector:@selector(foo) onThread:thread withObject:nil waitUntilDone:NO];

12. 实战应用场景

场景一:常驻线程(保活线程)

// AFNetworking 经典实现 + (void)networkRequestThreadEntryPoint:(id)object { @autoreleasepool { [[NSThread currentThread] setName:@"com.alamofire.networking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; // 添加一个永不触发的 Source,防止 Runloop 因无事件而退出 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; // 启动 } }

为什么需要加 Port? Runloop 退出的条件之一是:当前 Mode 下没有任何 Source/Timer/Observer。加一个空 Port(Source1)让 Runloop 认为有待处理事件,从而不退出。

场景二:TableView 滚动时延迟加载图片

// 图片加载放到 Default Mode,滚动时不占资源 [imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

场景三:卡顿检测

通过 Observer 监控主线程 Runloop 的状态时长:

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler( kCFAllocatorDefault, kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // 记录状态开始时间 // 在子线程定时检查:如果某状态持续超过阈值(如 16ms),判定为卡顿 } ); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

原理:卡顿 = 主线程 Runloop 某次循环耗时过长(即在BeforeSourcesAfterWaiting状态停留太久),子线程定时采样 Runloop 状态,触发卡顿上报。

场景四:界面流畅优化

Core Animation 的渲染提交时机在kCFRunLoopBeforeWaitingObserver 回调中(CA::Transaction::commit)。因此:

  • 同一个 Runloop 循环内的多次 UI 修改会被合并成一次渲染提交,不会每次修改都触发重绘

  • 手动调用setNeedsDisplay/setNeedsLayout只是标记,真正的渲染在 Runloop 即将休眠时批量处理


13. 高频面试题精解

Q1:Runloop 和线程的关系?

  • 一一对应,保存在全局字典中

  • 主线程 Runloop 由系统自动创建并启动

  • 子线程 Runloop 懒加载,首次调用currentRunLoop时创建,但不会自动启动

  • 线程销毁时 Runloop 也销毁


Q2:为什么说 Runloop 和 AutoreleasePool 有关系?

主线程 Runloop 注册了 Observer:在每次循环开始时 push 一个新池,即将休眠时 pop 旧池再 push 新池,退出时 pop 最后的池。因此主线程上普通 autorelease 对象最晚在当次 Runloop 循环结束(休眠前)被释放,而不需要等到整个 App 退出。


Q3:NSTimer 为什么在滚动时停止,如何解决?

原因:scheduledTimerWithTimeInterval:默认将 Timer 加入NSDefaultRunLoopMode,ScrollView 滚动时 Runloop 切换到UITrackingRunLoopMode,Default Mode 下的 Timer 暂停。

解决:将 Timer 加入NSRunLoopCommonModes(等价于同时加入 Default + Tracking)。

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

Q4:如何实现一个常驻子线程?

NSThread *thread = [[NSThread alloc] initWithBlock:^{ // 添加 Port 防止 Runloop 因空 Mode 退出 [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }]; [thread start];

关键点:必须在子线程内部启动 Runloop,且至少有一个 Source/Timer/Observer,否则 Runloop 立刻退出。


Q5:Source0 和 Source1 有什么区别?

Source0Source1
驱动方式非端口,需手动唤醒基于 mach port,内核主动唤醒
典型场景触摸事件分发、performSelector系统事件接收、线程间端口通信
唤醒 Runloop不能(需先 Signal + WakeUp)能(内核直接发消息)

Q6:Runloop 的休眠和 sleep 有什么本质区别?

sleep()是用户态的等待,CPU 仍然调度到当前线程检查时间;Runloop 通过mach_msg_trap陷入内核态,线程被挂起,CPU 完全分配给其他任务,直到指定 port 有消息时才被内核唤醒。后者是真正意义上的零 CPU 占用休眠。


Q7:performSelector:afterDelay: 在子线程不执行是为什么?

performSelector:afterDelay:内部使用 NSTimer,NSTimer 需要添加到 Runloop 才能触发。子线程默认没有 Runloop,Timer 无法注册,因此方法永远不会执行。解决方法:在子线程手动启动 Runloop。


Q8:GCD 的 dispatch_async 到主队列与 Runloop 有什么关系?

dispatch_async(main_queue, block)并不直接执行 block,而是向主线程的 dispatch_main_queue port 发送消息,唤醒主线程 Runloop,Runloop 在下一次循环中处理这个 block。因此,如果主线程 Runloop 被长时间阻塞,主队列的任务也会延迟。


Q9:界面更新(setNeedsLayout / setNeedsDisplay)的时机?

调用这些方法只是标记需要更新,并不立刻重绘。真正的渲染提交发生在 Runloop 即将休眠时,由 Core Animation 注册的kCFRunLoopBeforeWaitingObserver 回调触发(CA::Transaction::commit)。这就是为什么同一次循环内的多次 UI 修改不会导致多次重绘。


Q10:如何用 Runloop 实现卡顿监控?

核心思路:子线程信号量定时采样 + 主线程 Runloop Observer 状态记录。

  1. 主线程注册 Observer 监听BeforeSourcesAfterWaiting状态,记录进入时间戳

  2. 子线程每隔 16ms(一帧)通过信号量检查主线程 Runloop 状态

  3. 若主线程在BeforeSourcesAfterWaiting状态停留超过阈值(如 50ms),判定卡顿

  4. 通过PLCrashReporter等采集当前主线程调用栈上报


总结:一张图串联全部知识点

App 启动 └── UIApplicationMain → 启动主线程 Runloop 主线程 Runloop(Default / Tracking / Common) │ ├── Source1(mach port)← 接收硬件事件 / 系统消息 / GCD 主队列 │ │ 转交 │ ▼ ├── Source0 ← 处理 UIEvent 事件分发 / performSelector │ ├── Timer ← NSTimer / CADisplayLink(注意 Mode!) │ ├── Observer │ ├── Entry/Exit → AutoreleasePool push/pop │ ├── BeforeWaiting → AutoreleasePool pop+push │ │ → CATransaction commit(UI 提交渲染) │ └── BeforeSources/AfterWaiting → 卡顿监控采样点 │ └── mach_msg 休眠 💤(真正零 CPU,内核级挂起)

参考:CFRunLoop 开源实现(swift.org/open-source/corelibs-foundation)、Apple 官方 Threading Programming Guide

http://www.jsqmd.com/news/651706/

相关文章:

  • AWD Watchbird:PHP Web应用防火墙终极防护指南
  • 官方认证|2026年青岛七大正规豆包优化公司排名,余音智能综合实力遥遥领先 - 十大品牌榜
  • 多商户电商系统接入LINE Pay实战:从沙盒申请到退款流程的完整避坑指南
  • C语言第四节 字符和字符串和ASCII编码串
  • SAP FI 实战:从零到一构建企业核心科目表(COA)
  • #官方认证|2026年国内六大正规测厚仪公司排名,广东佛山等地覆盖,巢目科技技术实力遥遥领先 - 十大品牌榜
  • 融智天合同管理系统与预算管理融合体验 - 业财科技
  • 做一物一码要花多少钱才能做:先算清成本,再看长期回报
  • 官方认证|2026年青岛七大正规GEO优化公司排名,余音智能综合实力遥遥领先 - 十大品牌榜
  • 如何用AlwaysOnTop实现终极窗口置顶:免费效率提升完整指南
  • #官方认证|2026年国内六大正规X射线测厚仪公司排名,广东佛山等地巢目科技技术实力遥遥领先 - 十大品牌榜
  • 你的AI助手偷偷在学什么?这个浏览器仪表盘扒光了AI的脑子
  • 别再让图片变形了!Qt中QLabel显示图片的三种自适应方案实战(附完整代码)
  • 2026.4.15:超详细无人值守Ubuntu-Server安装保姆级教程
  • Abaqus子程序调试:如何在Visual Studio中高效单步追踪变量变化(2024最新版)
  • CSS如何通过Emotion管理样式加载顺序_处理组件优先级问题
  • C#怎么实现EF Core迁移 C#如何用Entity Framework Core进行数据库迁移和更新表结构【数据库】
  • 内网服务HTTPS化实战:除了mkcert,我们还需要注意什么?(含Nginx/IIS配置与客户端证书分发避坑指南)
  • SITS2026 AI面试模拟器深度拆解(训练数据/反馈闭环/岗位适配度三重验证)
  • 英雄联盟玩家必备的智能工具箱:5个核心功能提升你的游戏效率
  • 突破百度网盘限速壁垒:baidu-wangpan-parse工具实战指南与生态整合
  • WebLogic 10.3.6高危漏洞(CVE-2020-14750)实战修复指南:从补丁获取到验证的全流程解析
  • 让 AI 帮我读代码:一次 Nexent 编程助手实践
  • 告别卡顿与臃肿:Dell G15散热控制终极解决方案tcc-g15深度评测
  • UUV Simulator终极指南:构建高保真水下机器人仿真平台
  • 2025届必备的十大降重复率平台横评
  • 前端API设计进阶:从REST到GraphQL的演进
  • 3步解锁QQ音乐加密文件:qmcdump让你的音乐自由播放
  • 护照阅读器在各行业的应用场景
  • 如何用Python高效爬取B站数据:bilibili-api-python实战指南