编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

    • README
    • ObjC编程入门

    • Swift编程入门

    • 专栏博客

      • iOS系统架构概览
      • Runtime消息转发机制
      • RunLoop运行循环揭秘
        • 1. 案例引入
          • 1.1 一段诡异的卡顿
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 RunLoop不是While(1)
          • 2.2 为什么切出五层结构
        • 3. CFRunLoop源码剖析
          • 3.1 __CFRunLoopRun核心循环
          • 3.2 五大核心结构体
          • 3.3 线程与RunLoop绑定
        • 4. Mode机制详解
          • 4.1 Mode的隔离原理
          • 4.2 NSDefaultRunLoopMode
          • 4.3 UITrackingRunLoopMode陷阱
          • 4.4 CommonModes的设计意图
        • 5. Source——事件源
          • 5.1 Source0触摸事件
          • 5.2 Source1基于Mach Port
          • 5.3 自定义Source实战
        • 6. Timer——定时器
          • 6.1 NSTimer与CFRunLoopTimerRef
          • 6.2 Timer不准的真相
          • 6.3 CADisplayLink原理
          • 6.4 GCD Timer vs NSTimer
        • 7. Observer——观察者
          • 7.1 六大活动状态
          • 7.2 卡顿监控方案
          • 7.3 启动耗时监控
        • 8. RunLoop与UI刷新
          • 8.1 为什么滑动时NSTimer暂停
          • 8.2 滑动性能优化三板斧
          • 8.3 利用空闲时机预计算
        • 9. 线程保活与常驻线程
          • 9.1 为什么子线程的RunLoop默认关闭
          • 9.2 常驻线程的正确写法
          • 9.3 AFNetworking 2.0的经典案例
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一个RunLoop周期的一生
          • 10.3 设计哲学回扣
          • 10.4 RunLoop速查表
      • UIView生命周期与渲染
      • 事件响应与手势识别
      • AutoLayout布局系统
      • 自定义View绘制实践
      • CoreAnimation动画实战
      • ViewController容器管理
      • 转场动画与导航设计
      • 网络层架构设计
      • 数据持久化方案选型
      • 多线程与GCD实践
      • 内存管理与ARC原理
      • Block底层原理与应用
      • KVO与KVC实现机制
      • 性能优化与启动加速
      • 组件化与模块设计
      • 安全加密与逆向防护
      • 跨平台与混编方案
  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • iOS开发和进阶
  • 专栏博客
杨充
2026-06-20
目录

RunLoop运行循环揭秘

# RunLoop运行循环揭秘

深入篇 | CFRunLoop源码结构、Mode与Source/Timer/Observer、线程保活、卡顿监控、滑动优化实战。

# 目录介绍

  • 1. 案例引入
    • 1.1 一段诡异的卡顿
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 RunLoop不是While(1)
    • 2.2 为什么切出五层结构
  • 3. CFRunLoop源码剖析
    • 3.1 __CFRunLoopRun核心循环
    • 3.2 五大核心结构体
    • 3.3 线程与RunLoop绑定
  • 4. Mode机制详解
    • 4.1 Mode的隔离原理
    • 4.2 NSDefaultRunLoopMode
    • 4.3 UITrackingRunLoopMode陷阱
    • 4.4 CommonModes的设计意图
  • 5. Source——事件源
    • 5.1 Source0触摸事件
    • 5.2 Source1基于Mach Port
    • 5.3 自定义Source实战
  • 6. Timer——定时器
    • 6.1 NSTimer与CFRunLoopTimerRef
    • 6.2 Timer不准的真相
    • 6.3 CADisplayLink原理
    • 6.4 GCD Timer vs NSTimer
  • 7. Observer——观察者
    • 7.1 六大活动状态
    • 7.2 卡顿监控方案
    • 7.3 启动耗时监控
  • 8. RunLoop与UI刷新
    • 8.1 为什么滑动时NSTimer暂停
    • 8.2 滑动性能优化三板斧
    • 8.3 利用空闲时机预计算
  • 9. 线程保活与常驻线程
    • 9.1 为什么子线程的RunLoop默认关闭
    • 9.2 常驻线程的正确写法
    • 9.3 AFNetworking 2.0的经典案例
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一个RunLoop周期的一生
    • 10.3 设计哲学回扣
    • 10.4 RunLoop速查表

# 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 这个大循环工作,每个循环体里:

  1. 通知 Observer:即将处理 Timer
  2. 通知 Observer:即将处理 Source0
  3. 处理 Source0
  4. 如果有 Source1,跳转到"处理 Source1"分支
  5. 通知 Observer:即将休眠
  6. 休眠(mach_msg 等待 Mach Port 消息)
  7. 被唤醒后,根据唤醒原因走不同分支

关键在第 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)平铺放不行吗?

论证:

  1. 事件优先级隔离的需要——当用户在快速滑动列表时(手指在屏幕上),系统切换到 UITrackingRunLoopMode。这个 Mode 里只注册了触摸相关的 Source 和定时器。那些"非紧急"的 Timer(比如轮播广告、网络状态检测)在 NSDefaultRunLoopMode 里,滑动期间不会被触发——CPU 资源全部留给 UI 渲染。滑完后切回 NSDefaultRunLoopMode,非紧急 Timer 再照常运行。

  2. 没有 Mode 的后果:假设平铺——列表滑动时,每 0.1 秒有一个网络状态检测 Timer 也要执行——UI 线程就得在"渲染 cell"和"检测网络"之间频繁切换,帧率从 60 FPS 掉到 30 FPS。Mode 是对主线程 CPU 时间的优先级调度器。

  3. CommonModes 是"特权通道"——如果你确实希望某个 Timer 无论在什么 Mode 下都执行(比如倒计时),把它标记为 NSRunLoopCommonModes ——它会同时注册到 Default 和 Tracking 两个 Mode 下。这相当于"VIP 通行证"。

  4. 反向验证: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 秒才触发?

论证:三个原因叠加——

  1. Mode 切换导致跳过(本篇主线案例):滑动时 RunLoop 在 Tracking Mode,Default Mode 的 Timer 不会被触发——直到切回去才一次性执行。

  2. RunLoop 单次循环耗时过长:如果某个 Source0 回调(比如一个大 cell 的布局计算)耗了 50ms,那所有 Timer 就晚了 50ms。mach_msg 睡眠时的 timeout 参数由 "距离最近 Timer 的剩余时间" 决定,但如果 RunLoop 在处理 Source0 时耗时太长,根本来不及睡就被迫触发 Timer。

  3. 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 级别彻底剖开。

上次更新: 2026/06/28, 17:55:19
Runtime消息转发机制
UIView生命周期与渲染

← Runtime消息转发机制 UIView生命周期与渲染→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式