runloop
RunLoop 本质是一个事件循环,让线程不执行完就退出;没有 RunLoop 的子线程,任务执行完毕后立刻销毁。
每个线程都可以拥有一个 RunLoop,但它不是"自带"的,而是"首次访问时才创建"的。主线程的 RunLoop 由系统自动创建并启动,子线程的 RunLoop 需要开发者手动触发创建,并且必须手动调用 run 方法才能让它开始工作。
获取runloop对象。
ini
// OC对象
NSRunLoop *mainLoop = [NSRunLoop mainRunLoop];
NSRunLoop *mainLoop1 = [NSRunLoop currentRunLoop];
// C语言的CoreFoundation
CFRunLoopRef loop2 = [mainLoop getCFRunLoop];
RunLoop 5 个核心对象(CFRunLoop 源码结构体)
比如默认的定时器,不添加runloop的UITrackingMode,在滑动UIScrollView的时候,就不会执行。因为UIScrollView滑动的时候,渲染的同时,又需要针对每个像素的移动进行回调。
因此为了页面不卡顿,提供了函数的优先级功能。默认优先级就是kCFRunLoopDefaultMode。而要任何时刻都不被影响,则UITrackingRunLoopMode。
-
CFRunLoopRef:RunLoop 本体,里面包含 mode 数组、当前 mode
-
CFRunLoopModeRef :运行模式,同一时刻只能处于一个 Mode。切换 Mode = 退出当前循环,重新进入新 Mode
-
常用 mode:
kCFRunLoopDefaultMode:默认模式,日常 App 运行(滑动 UIScrollView 时会被切走)UITrackingRunLoopMode:滚动模式,滚动时进入这个 mode,保证滑动流畅kCFRunLoopCommonModes:不是真实 Mode,是 mode 集合标记,把 default+tracking 标记为 common,添加到 common 的 timer/observer 会在两个 mode 都生效
-
-
CFRunLoopSourceRef:事件源(输入源)
- Source0:非内核事件,App 内部事件(UI 点击、performSelector:onThread,需要手动标记唤醒 RunLoop)
- Source1:内核端口事件,由内核触发,Mach 端口驱动,收到消息直接唤醒 RunLoop(端口通信、系统事件)
-
CFRunLoopTimerRef :基于 CFAbsoluteTime 的定时器,NSTimer 底层就是它,绑定 mode。如果 mode 切换,timer 不触发。
-
CFRunLoopObserverRef:观察者,监听 RunLoop 状态变化,可以在循环的各个节点回调。 7 个状态:
markdown1. kCFRunLoopEntry 进入runloop 2. kCFRunLoopBeforeTimers 处理Timer前 3. kCFRunLoopBeforeSources 处理Source前 4. kCFRunLoopBeforeWaiting 准备休眠前(最常用) 5. kCFRunLoopAfterWaiting 被唤醒,刚结束休眠 6. kCFRunLoopExit 退出runloop 7. kCFRunLoopAllActivities 全部状态
3. RunLoop 完整运行逻辑(源码简化流程)
就算没有开定时器,依然能监听kCFRunLoopBeforeTimers的回调。这个属于流程。
scss
// CFRunLoop.c 简化伪代码
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFRunLoopModeRef mode, ...)
{
// 1. 通知Observer:kCFRunLoopEntry 进入循环
__CFRunLoopDoObservers(rl, mode, kCFRunLoopEntry);
do {
// 2. Observer:kCFRunLoopBeforeTimers
__CFRunLoopDoObservers(rl, mode, kCFRunLoopBeforeTimers);
// 3. Observer:kCFRunLoopBeforeSources
__CFRunLoopDoObservers(rl, mode, kCFRunLoopBeforeSources);
// 4. 处理Source0(待执行block、点击事件等)
__CFRunLoopDoSources0(rl, mode, ...);
// 5. 如果有Source1事件,直接跳转处理,不进入休眠
if (__CFRunLoopHasSourcesPending(rl, mode)) {
continue;
}
// 6. Observer:kCFRunLoopBeforeWaiting,准备休眠
__CFRunLoopDoObservers(rl, mode, kCFRunLoopBeforeWaiting);
// 7. 【核心】调用mach_msg,线程休眠,等待内核消息
// 阻塞在这里,不占用CPU,直到收到mach端口消息
mach_msg(..., MACH_MSG_OPTION_RCV_TIMEOUT);
// 8. Observer:kCFRunLoopAfterWaiting,被唤醒
__CFRunLoopDoObservers(rl, mode, kCFRunLoopAfterWaiting);
// 9. 被唤醒后判断是什么事件唤醒:
if (timer触发) {
__CFRunLoopDoTimers();
} else if (source1收到消息) {
__CFRunLoopDoSource1();
} else if (被手动wakeup) {
// 只是唤醒标记,无事件
}
} while (满足继续循环条件);
// 10. Observer:kCFRunLoopExit,退出runloop
__CFRunLoopDoObservers(rl, mode, kCFRunLoopExit);
return result;
}
- 休眠依靠
mach_msg,是 Mach 内核的 IPC 通信,不是忙等 - Source0 不会主动唤醒 RunLoop,需要手动调用
CFRunLoopWakeUp唤醒 - Source1、Timer、外部 wakeup 可以唤醒 RunLoop
验证source0不会主动runloop
scss
- (void)viewDidLoad {
[self testSource0WakeUp];
}
void sourceo_print(void *info)
{
NSLog(@"this is a source0_print message %@", info);
}
- (void)testSource0WakeUp {
// 1. 注册一个 Observer,监听 RunLoop 的休眠和唤醒状态
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting,
NO, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeWaiting) {
NSLog(@"🛌 [Observer] RunLoop 即将休眠 (Sleep)");
} else if (activity == kCFRunLoopAfterWaiting) {
NSLog(@"⏰ [Observer] RunLoop 被唤醒 (WakeUp)");
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
// 2. 创建一个 Source0
CFRunLoopSourceContext context = {0};
context.info = @"我是传入的参数";
context.perform = sourceo_print;
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetMain(), source0, kCFRunLoopDefaultMode);
// 3. 测试场景:延迟 3 秒后触发 Source0
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
NSLog(@"--- 开始测试:只调用 CFRunLoopSourceSignal ---");
// 仅标记 Source0 待处理,不主动唤醒
// CFRunLoopSourceSignal(source0);
// 【关键验证点】:取消下面这行注释,观察控制台日志的区别
// CFRunLoopWakeUp(CFRunLoopGetMain());
});
}
另外一个线程标记了CFRunLoopSourceSignal(source0);,但是休眠后并不执行。除非执行CFRunLoopWakeUp(CFRunLoopGetMain());手动唤醒这个runloop。
对于定时器,每一次runloop循环都会取一个最近的定时器任务,将时间传递给底层的mach_port。这些定时器是用最小堆实现的。并不会查询所有的定时器,因为如果其中一个时间没有到,就没有必要取查找后续的任务了。
注意点
子线程 RunLoop 如果没有 source/timer/observer,run 会直接退出(空 RunLoop 自动退出)。
CFRunLoopStop 是立刻终止 RunLoop 循环吗?
不会立刻打断当前正在执行的事件,只会标记停止标记;本次循环执行完毕,下一轮循环才退出。
runloop的应用
- NSTimer / CADisplayLink(依赖 RunLoop Mode,滚动失效坑)
performSelector:onThread:afterDelay:底层就是往 RunLoop 添加 Source0- 子线程保活(常驻线程,开启 RunLoop,持续处理任务)
- 异步渲染、监控卡顿(Observer 监听
BeforeWaiting,检测长时间阻塞) - AutoReleasePool:RunLoop 每次循环的 beforeWaiting、afterWaiting 自动创建 / 释放自动释放池
NSTime/CADisplayLink
objectivec
- (void)viewDidLoad{
[super viewDidLoad];
// 自定添加到runloop
NSTimer *t = [NSTimer scheduledTimerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"scheduledTimerWithTimeInterval function");
}];
//
[[NSRunLoop currentRunLoop] addTimer:t forMode:UITrackingRunLoopMode];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 会自动的将所有的模式都停掉
// [t invalidate];
// 默认模式还能继续用
CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), (__bridge CFRunLoopTimerRef)t, (__bridge CFStringRef)UITrackingRunLoopMode);
});
NSTimer *timer = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"time function");
}];
// 这里default模式,滑动的时候失效
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
CADisplayLink *d = [CADisplayLink displayLinkWithTarget:self selector:@selector(display)];
[d addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//CADisplayLink比较特殊,不能从某个模式中删除
[d invalidate];
});
}
- (void)display{
NSLog(@"display");
}
performSelector:onThread:afterDelay:` 底层就是往 RunLoop 添加 Source0
这里虽然是source0。但是内部还有一个Timer,所以时间到了内核会自动激活,不需要手动wakeup。
子线程保活(常驻线程,开启 RunLoop,持续处理任务)
objectivec
@interface MyThread : NSObject
- (void)excuteTask:(void (^)(void))block;
- (void)stop;
@end
runloop和线程保活
为了避免频繁创建和销毁线程。可以让一个线程一直存在。
MyThread.h
@interface MyThread : NSObject
- (void)excuteTask:(void (^)(void))block;
- (void)stop;
@end
MyThread.m
@interface MyThread()
@property (nonatomic,strong) NSThread *thread;
@property (nonatomic, assign) CFRunLoopRef threadRunLoop; // 保存子线程runloop
@property (nonatomic, assign) BOOL isStop; // 保存子线程runloop
@end
@implementation MyThread
- (void)setupRunLoopObserver {
// 监听全部关键状态
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
NULL,
kCFRunLoopAllActivities,
YES, // repeats=YES,每一轮循环都触发
0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop: 进入循环");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop: 即将处理Timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop: 即将处理Source");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop: 准备进入休眠(mach_msg阻塞)");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop: 被唤醒,离开mach_msg");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop: RunLoop即将退出");
break;
default:
break;
}
}
);
// 添加到DefaultMode
CFRunLoopAddObserver(self.threadRunLoop, observer, kCFRunLoopDefaultMode);
CFRelease(observer); // create出来需要释放
}
- (instancetype)init
{
self = [super init];
if (self) {
self.isStop = false;
self.thread = [[NSThread alloc] initWithBlock:^{
self.threadRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
[self setupRunLoopObserver];
NSLog(@"runloop begin");
// run之前必须有任务。否则[[NSRunLoop currentRunLoop] run];直接退出,这种方式就算调用了stop也不会退出
// [self performSelector:@selector(keepAlive) onThread:self.thread withObject:nil waitUntilDone:NO];
// [[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (!self.isStop) {
/*
此时线程挂起,等待唤醒
- source0 事件:`performSelector:onThread`投递任务(执行 task)
- Source1 事件:NSPort 收到端口消息
- Timer 到点
- `CFRunLoopWakeUp(rl)` 手动唤醒 RunLoop
*/
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
}
NSLog(@"runloop end");
}];
self.thread.name = @"MyKeepAliveThread";
[self.thread start];
}
return self;
}
- (void)keepAlive{
NSLog(@"keepAlive");
}
- (void)excuteTask:(void (^)(void))block{
[self performSelector:@selector(runBlock:) onThread:self.thread withObject:block waitUntilDone:NO];
}
// 私有方法,真正在子线程执行block
- (void)runBlock:(void (^)(void))block {
if (block) {
block();
}
}
- (void)stop{
// 如果创建了当前对象init了,线程对象存在,但是子线程的代码还没有开始执行,这个runloop可能还是空的
self.isStop = true;
if (!self.threadRunLoop){
// 如果初始化刚完成调用stop.可能导致这个threadRunLoop还没有初始化完成
// 这里sleep,虽然会卡,但是收到的影响忽略不计。且大部分情况不会进来这个方法
[NSThread sleepForTimeInterval:0.001];
}
if (self.threadRunLoop){
NSLog(@"stop runloop");
// 这里只是标记要停止,如果当前没有任务,就算激活了runloop也不会停止。只有当还有任务的时候.
// 因为不调用这段代码,runloop循环的时候,啥也不干,又继续等待。
CFRunLoopStop(self.threadRunLoop);
// 这里激活,也有可能会被其它激活,这里保底写一个。
CFRunLoopWakeUp(self.threadRunLoop);
}
}
-(void)dealloc{
NSLog(@"MyThread dealloc");
}
ViewController.m
@interface ViewController()
@property (nonatomic, strong) MyThread *myThread;
@end
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.myThread = [[MyThread alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.myThread excuteTask:^{
// sleep(1);
// usleep(10) 微秒
NSLog(@"task1 %@ begin", [NSThread currentThread]);
[NSThread sleepForTimeInterval:3];
NSLog(@"task1 %@ finished", [NSThread currentThread]);
}];
});
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"touhbegin");
[self.myThread stop];
self.myThread = nil;
}
@end
runloop实现异步渲染、监控卡顿(Observer 监听BeforeWaiting,检测长时间阻塞)
RunLoop 在 iOS 性能优化中扮演着极其核心的角色。你提到的"异步渲染"和"卡顿监控"正是利用了 RunLoop 的两个关键特性:任务调度时机 和状态流转机制。
结合底层原理,我为你深度拆解这两个场景的实现机制:
1. RunLoop 如何实现异步渲染?
在 iOS 中,UI 渲染(如 drawRect:、文本排版、图片解码等)默认是在主线程同步执行的。如果渲染任务过重,就会阻塞主线程,导致掉帧卡顿。RunLoop 在这里提供了两个层面的优化:
① 利用 RunLoop 的"渲染提交时机"进行异步绘制
当你在主线程调用 setNeedsDisplay 时,系统并不会立刻绘制,而是给 Layer 打上"脏标记"。苹果在 RunLoop 中注册了一个优先级很晚的 Observer,专门监听 kCFRunLoopBeforeWaiting(即将休眠)和 kCFRunLoopExit(即将退出)事件58。
- 系统机制 :当 RunLoop 准备休眠时,Observer 被触发,系统会遍历所有带有脏标记的 Layer,调用
[CALayer display]将生成的 Bitmap 提交给 GPU18。 - 异步绘制切入点 :我们可以重写
displayLayer:方法接管这个流程。在kCFRunLoopBeforeWaiting阶段,把耗时的绘制任务(如使用 Core Graphics 绘制文字或图片)派发到子线程 执行。子线程生成 Bitmap 后,再回到主线程赋值给layer.contents110。这样就彻底避免了主线程在 RunLoop 休眠前被绘制任务阻塞。
② 利用 RunLoop 的"空闲时机"进行延迟加载
对于启动时不需要立即显示的大图或复杂视图,可以利用 performSelector:withObject:afterDelay:0 将任务挂载到下一次 RunLoop 循环中。这样既保证了不阻塞当前的启动流程,又能在主线程空闲时平滑地完成加载。
2. RunLoop 如何监控卡顿?
iOS 的卡顿本质上是主线程 RunLoop 在处理某个事件时耗时过长,导致无法及时进入休眠状态或处理下一帧的 VSync 信号4。利用 RunLoop 监控卡顿是目前线上最主流的方案(如微信的 Matrix 框架),其核心原理如下:
① 注册状态监听(Observer)
在主线程注册一个 CFRunLoopObserver,重点监听两个状态:
kCFRunLoopBeforeSources:RunLoop 即将开始处理事件(如触摸、网络回调等)4。此时记录下开始时间。kCFRunLoopBeforeWaiting:RunLoop 处理完所有事件,准备进入休眠。此时记录下结束时间。
② 旁路监控线程(独立判断)
如果在主线程自己判断是否超时,一旦主线程卡死,判断逻辑也会跟着卡死。因此,必须创建一个独立的子线程(监控线程) 来进行旁路检测:
- 监控线程会持续检查主线程记录的时间差(
当前时间 - 开始时间)2。 - 如果主线程长时间停留在
BeforeSources到BeforeWaiting之间,且时间差超过了预设的阈值(例如 200ms 或 2s,取决于业务对卡顿的定义),监控线程就会判定主线程发生了卡顿。
③ 抓取卡顿堆栈
一旦监控线程判定卡顿,它会立刻通过系统 API(如 backtrace)抓取主线程当前的函数调用栈,并将堆栈信息上报到服务器。这样开发者就能精准知道"主线程到底在干什么导致了卡顿"(比如是复杂的 JSON 解析,还是主线程的同步锁竞争)29。
总结
- 异步渲染 :利用 RunLoop 的
BeforeWaiting时机,将耗时的 CPU 绘制工作从主线程剥离到子线程,实现"主线程只负责发号施令,子线程负责画图,主线程再负责贴到屏幕上"的流水线作业。 - 卡顿监控:利用 RunLoop 的状态流转作为"心跳",通过旁路线程监控"心跳"是否超时,从而精准捕获主线程的阻塞行为。
objectivec
@interface ViewController ()
@property (nonatomic, assign) BOOL isRunLoopProcessing;
@property (nonatomic, assign) CFTimeInterval lastActivityTime;
@end
// 1. 模拟卡顿(在 ViewController 的 touchesBegan 或按钮点击事件中调用)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"🔥 主线程开始模拟卡顿(阻塞3秒)...");
// 制造一个 3 秒的耗时操作(比如死循环或复杂计算)
sleep(3);
NSLog(@"✅ 主线程卡顿结束,恢复响应");
}
// 2. 启动卡顿监控(在 viewDidLoad 中调用)
- (void)viewDidLoad {
[super viewDidLoad];
[self startANRMonitor];
}
- (void)startANRMonitor {
// 1. 注册主线程的 RunLoop Observer,监听状态变化
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
kCFRunLoopAllActivities, // 监听所有状态
YES, // 重复监听
0, // 优先级最高
&runLoopObserverCallback, // C 语言回调函数
&context
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
// 2. 开启一个独立的监控子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 监控线程:持续检查主线程的状态
while (YES) {
@autoreleasepool {
// 休眠 200 毫秒检查一次(可根据需要调整,通常 100~250ms 比较合理)
[NSThread sleepForTimeInterval:0.2];
// 如果主线程当前处于 "处理事件" 或 "处理 Source" 阶段,且持续时间超过阈值
// 这里我们简单用 1 秒作为卡顿判定标准(实际业务中通常是 200ms 掉帧)
if (self.isRunLoopProcessing && self.lastActivityTime > 0) {
CFTimeInterval duration = CFAbsoluteTimeGetCurrent() - self.lastActivityTime;
if (duration > 1.0) {
// 发现卡顿!抓取主线程堆栈
NSLog(@"🚨 警告:检测到主线程卡顿 %.2f 秒!正在抓取堆栈...", duration);
[self dumpMainThreadCallStack];
// 重置时间,避免重复报警
self.lastActivityTime = 0;
}
}
}
}
});
}
// 3. RunLoop 状态回调(C 函数)
static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
ViewController *vc = (__bridge ViewController *)info;
switch (activity) {
case kCFRunLoopBeforeSources:
case kCFRunLoopBeforeWaiting:
// 即将处理事件,记录开始时间
vc.lastActivityTime = CFAbsoluteTimeGetCurrent();
vc.isRunLoopProcessing = YES;
break;
case kCFRunLoopAfterWaiting:
case kCFRunLoopEntry:
case kCFRunLoopExit:
// 进入休眠或退出,说明当前事件处理完毕,重置标记
vc.isRunLoopProcessing = NO;
vc.lastActivityTime = 0;
break;
default:
break;
}
}
// 4. 抓取主线程堆栈
- (void)dumpMainThreadCallStack {
NSArray<NSInvocation *> *callStack = [NSThread callStackSymbols];
// 打印主线程的前 10 个堆栈帧,找出罪魁祸首
NSLog(@"📜 主线程卡顿堆栈:");
for (int i = 0; i < MIN(10, callStack.count); i++) {
NSLog(@"%@", callStack[i]);
}
}