RunLoop初步了解

RunLoop初步了解

文章目录

RunLoop是线程的管理者,让线程在有任务时高效工作,没有任务的时候进入内核休息,需要时能被硬件中断或端口消息立即唤醒

UIApplication

UIApplication 是 iOS 应用程序的"根对象",它本质上是一个单例类,继承自 UIResponder,负责管理和控制整个 App 的生命周期、事件循环和核心状态

objc 复制代码
int UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);

这个方法是UIKit提供的一个C函数,他做了五件事

  1. 创建UIApplication单例:整个 App 的根对象,全局唯一,负责管理 App 生命周期,分发系统级事件,管理所有 UIWindow
  2. 创建AppDelegate为代理
  3. 加载主界面
  4. 启动主线程RunLoop,在这之后主线程才变成了永不退出的事件循环
  5. 处理事件循环,RunLoop 启动后,所有的事情都由 RunLoop 驱动,触摸事件,Timer 回调,GCD 主队列任务

子线程的RunLoop是调用 [NSRunLoop currentRunLoop]CFRunLoopGetCurrent() 时才会创建,如果从未调用过 CFRunLoopGetCurrent(),它执行完任务后立刻销毁,不会保活

RunLoop退出的条件:App退出,线程关闭,设置最大时间到期,ModeItem为空

Mode与Item

RunLoop 在 Core Foundation 层的结构如下

objc 复制代码
struct __CFRunLoop {
    pthread_t _pthread;                // 绑定的线程
    CFMutableSetRef _commonModes;      // 通用模式名单
    CFMutableSetRef _commonModeItems;  // 通用事件仓库
    CFRunLoopModeRef _currentMode;     // 当前运行的 Mode
    CFMutableSetRef _modes;            // 所有 Mode
};

struct __CFRunLoopMode {
    CFStringRef _name;                 // Mode 名字
    CFMutableSetRef _sources0;         // 手动事件源
    CFMutableSetRef _sources1;         // 内核事件源
    CFMutableArrayRef _observers;      // 观察者
    CFMutableArrayRef _timers;         // 定时器
    pthread_mutex_t _lock;             // 线程锁
};
Mode

RunLoop在同一时刻只能运行一种Mode,切换Mode只能退出RunLoop,Mode的意义就是隔离不同优先级的事件,只能添加不能删除,因为RunLoop同时指出里一个任务,所以遍历元素少效率高

类型

  1. kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 Mode 下运行

  2. UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响

  3. UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用

4: GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到

5: kCFRunLoopCommonModes: 占位mode,作为标记DefaultMode和CommonMode用

RunLoop 内部有一个非常重要的函数,叫 CFRunLoopRunSpecific(),它接受一个 Mode 参数。每一次 RunLoop 进入循环时,都会明确指定本次循环要跑哪个 Mode

当你滑动 UITableView 时,UIKit 会在底层调用 CFRunLoopRunInMode( UITrackingRunLoopMode, ...),强制 RunLoop 切换到 Tracking模式

开发者指定:你手动调用 [[NSRunLoop currentRunLoop] runMode:beforeDate:]时,你传什么 Mode,它就只跑什么 Mode

当然自定义Mode的情况极为少见,一般只用系统给的Mode

Item

作为Mode里的具体任务,每个Mode里都有四个集合存放具体的Item

Item 类型 作用 典型例子
Source0 手动事件源,不唤醒 RunLoop performSelector:、UI 事件分发
Source1 内核事件源,能主动唤醒 RunLoop 触摸事件、GCD 主队列回调
Timer 定时器 NSTimerperformSelector:afterDelay:
Observer 观察者,监听 RunLoop 状态 自动释放池释放、卡顿监控
objc 复制代码
// Source0:没有 port
CFRunLoopSource {order = ..., {callout = ...}}

// Source1:拥有 port
CFRunLoopSource {order = ..., {port = ..., callout = ...}}
  1. Source

Source0:没有 port,无法监听内核信号,只能由 App 内部手动触发(CFRunLoopSourceSignal + CFRunLoopWakeUp

Source1:拥有 mach_port_t 端口,可以监听内核消息,具备主动唤醒 RunLoop 的能力

一般来说就是Source1唤醒RunLoop,然后触发后面多个Source0,让它去完成任务

  1. Timer
objc 复制代码
struct __CFRunLoopTimer {
    uint64_t _fireTSR;          // 绝对触发时间(基于 mach_absolute_time 的纳秒数)
    CFTimeInterval _interval;   // 重复间隔,0 表示只触发一次
    CFIndex _order;             // 优先级
    CFRunLoopTimerCallBack _callout; // 回调函数指针
};

首先调用 addTimer:forMode:将你想定的时间点塞进该 Mode 的 _timers 数组,数组按 _fireTSR(触发时间)升序排列,最近的排最前面,RunLoop开始循环时,首先会告诉Observer要开始检查Timer了,但是不处理Timer,先处理的是Source0,知道任务完成,再看Timer,看自己能睡多久(mach_msg),差值就是现在的时间距离Timer的差,然后该Timer被移除

有Timer内核睡到 Timer 触发时间,超时返回,RunLoop 醒来执行 Timer 回调。

没有Timer就是RunLoop 无限期休眠,只能靠硬件中断(如触摸屏幕)或其他线程调用 CFRunLoopWakeUp*来唤醒

如果下一次Timer到了,上一个任务还没被完成,那么这个任务完成后就去完成被耽搁的任务,卡顿不可避免,RunLoop是单线程串行的,当前任务不执行完,RunLoop 根本没有机会回到循环顶部去检查 _timers 数组

  1. Observer
objc 复制代码
// Observer:order(优先级),activity(监听状态),callout(回调函数)
CFRunLoopObserver {order = ..., activities = ..., callout = ...}

Observer 监听 RunLoop 的 6 种状态变化:

状态常量 含义
kCFRunLoopEntry 即将进入 RunLoop
kCFRunLoopBeforeTimers 即将处理 Timer
kCFRunLoopBeforeSources 即将处理 Source
kCFRunLoopBeforeWaiting 即将休眠(自动释放池在这里释放)
kCFRunLoopAfterWaiting 刚被唤醒
kCFRunLoopExit 即将退出

Loop 机制

RunLoop 的 Loop 机制不是简单的 while(1),而是一个精密的任务处理 + 内核休眠的完整周期,每次循环都经历以下5个步骤:

  1. 准备(通知观察者)
c 复制代码
// 步骤 1:通知 Observer,即将进入 Loop
__CFRunLoopDoObservers(rl, kCFRunLoopEntry);
  1. 干活(处理所有待办任务)
c 复制代码
// 步骤 2:通知 Observer,即将处理 Timer
__CFRunLoopDoObservers(rl, kCFRunLoopBeforeTimers);

// 步骤 3:通知 Observer,即将处理 Source0
__CFRunLoopDoObservers(rl, kCFRunLoopBeforeSources);

// 步骤 4:执行被添加到 RunLoop 的 Blocks
__CFRunLoopDoBlocks(rl);

// 步骤 5:处理所有 Source0(手动触发的事件)
__CFRunLoopDoSources0(rl);

// 步骤 6:再次执行 Blocks
__CFRunLoopDoBlocks(rl);

// 步骤 7:检查是否有 Source1 就绪(有的话跳转到步骤 9)
  1. 睡觉(进入内核休眠)
c 复制代码
// 步骤 8:★ 核心休眠 ★
// 8.1 通知 Observer,即将休眠
__CFRunLoopDoObservers(rl, kCFRunLoopBeforeWaiting);

// 8.2 计算休眠时间:找出最近要触发的 Timer
CFTimeInterval timeout = 最近Timer的触发时间 - 当前时间;

// 8.3 调用 mach_msg,线程挂起
mach_msg(..., timeout);

线程从这里开始彻底休眠,CPU 占用率为 0

内核接管计时任务,靠硬件时钟中断或硬件输入中断来唤醒线程

如果没有 Timer,timeout 为无穷大,线程永远等待触摸事件

  1. 醒来(被唤醒)
c 复制代码
// 步骤 9:mach_msg 返回,线程被唤醒
// 9.1 通知 Observer,刚被唤醒
__CFRunLoopDoObservers(rl, kCFRunLoopAfterWaiting);

// 9.2 判断唤醒原因
if (超时了) {
    // 执行到点的 Timer 回调
    __CFRunLoopDoTimers(rl);
} else if (收到 Source1 消息) {
    // 处理端口消息(触摸事件、GCD 主队列回调)
    __CFRunLoopDoSource1(rl);
}
  1. 决策(是否继续循环)
c 复制代码
// 步骤 10:检查退出条件
if (超时 || 被停止 || Mode为空) {
    __CFRunLoopDoObservers(rl, kCFRunLoopExit);
    return; // 退出循环
} else {
    // 回到步骤 1,继续下一轮循环
}

mach_msg

RunLoop 的休眠不是 sleep(),而是通过 XNU 内核的 mach_msg() 系统调用实现的:

c 复制代码
mach_msg_return_t mach_msg(
    mach_msg_header_t *msg,
    mach_msg_option_t option,        // MACH_RCV_MSG(接收消息)
    mach_msg_size_t send_size,       // 0
    mach_msg_size_t rcv_size,
    mach_port_t rcv_name,            // 当前线程的端口
    mach_msg_timeout_t timeout,      // 最大等待时间
    mach_port_t notify
);

当 RunLoop 调用 mach_msg(..., timeout = 2.0 秒) 时:

  1. 触发 mach_msg_trap 系统调用,从用户态陷入内核态
  2. 内核将线程从"运行队列"移到"等待队列",保存上下文
  3. CPU 释放,该线程不再占用任何 CPU 时间片,CPU 占用率为 0%
  4. 等待唤醒:硬件中断(触摸)、超时(Timer 到点)或其他线程调用 CFRunLoopWakeUp
  5. mach_msg 返回,线程回到用户态,RunLoop 继续执行

AutoreleasePool

自动释放池也和RunLoop有关,在 ARC 时代,大部分内存管理由编译器自动完成,但对象的释放时机不是即时的

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    
    for (int i = 0; i < 100000; i++) {
        NSString *str = [NSString stringWithFormat:@"第 %d 条", i];
        // str 什么时候释放?
    }
}

如果每创建一个临时对象就立即释放,会带来两个问题:频繁释放导致性能开销大,且有些对象需要在当前作用域结束后才释放。AutoreleasePool 的作用就是将对象的释放延迟到池子销毁时统一进行

复制代码
@autoreleasepool {
    NSString *str = [[NSString alloc] initWithFormat:@"Hello"];
    // str 被加入当前的 AutoreleasePool
}
// 池子销毁,str 的引用计数 -1,如果为 0 则释放

当调用 [obj autorelease] 时,会把对象加入当前线程栈顶的 Pool,Pool 销毁时,遍历列表,对每个对象发送 release

与RunLoop的关系

AutoreleasePool 的释放完全依赖 RunLoop 的 Observer 机制,主线程 RunLoop 中注册了两个 Observer,第一个 Observer 监听的是 kCFRunLoopEntry(RunLoop 刚启动),第二个监听 kCFRunLoopBeforeWaiting(即将休眠)和kCFRunLoopExit(退出 RunLoop)

objc 复制代码
Observer 回调 (kCFRunLoopBeforeWaiting) {
    // 1. 释放当前的 AutoreleasePool
    _objc_autoreleasePoolPop(当前池);
    // 2. 创建新的 AutoreleasePool
    _objc_autoreleasePoolPush();
}

主线程中不需要手动写 @autoreleasepool,因为 RunLoop 通过 Observer 帮你自动管理了,每次 RunLoop 循环结束(即将休眠)时,系统会自动释放当前池并创建新池,确保临时对象不会无限堆积

相关推荐
代码的小搬运工1 小时前
Runloop
macos·objective-c·cocoa
秋雨梧桐叶落莳2 小时前
【iOS】GCD内容整理
macos·ios·cocoa
demo007x16 小时前
让大模型活在你的鼠标旁:我用 Tauri 2 + Rust 打造了一款“反直觉”的 AI 全局划词效率神器
macos·程序员·llm
那年窗外下的雪.1 天前
AIDC 学习日志|第 24 天|设备输出反推与 MAC Flapping 定位
网络协议·学习·tcp/ip·http·macos·tcpdump
RobinDevNotes1 天前
Palmier Pro:AI时代的Mac视频编辑器
人工智能·ceph·macos·ai·音视频·视频编辑·mcp
johnsong2 天前
AI前沿日报 2026-09-08
人工智能·macos
33三 三like2 天前
基于 MAC-RUNGUIDE 的完整启动流程
macos
用户2181697049303 天前
iOS 底层原理 runloop
objective-c
tedcloud1233 天前
Wand-Enhancer:如何搭建一套远程开发与测试环境
前端·人工智能·macos·开源·流程图