Day9-RunLoop 卡顿优化与监控实现

YVTU

iOS RunLoop 卡顿优化指南

一、理解 RunLoop 卡顿的本质

1. RunLoop 基本机制

RunLoop 本质上是一个事件循环机制,主要用于线程的任务调度。在主线程中,RunLoop 负责处理:

  • 触摸事件
  • 定时器事件(NSTimerCADisplayLink
  • UI 渲染
  • GCD 主队列任务
  • 系统事件(比如键盘弹出)

2. 卡顿现象原因

主线程卡顿的本质是 RunLoop 长时间没有返回到空闲状态,常见原因:

  • 同步耗时任务阻塞主线程(如文件 IO、网络请求、复杂计算)
  • UI 渲染过慢(大量绘制、图片解码)
  • 高频次定时器任务
  • 死锁或线程抢占资源

二、RunLoop 卡顿监控

1. 使用 RunLoop Observer 检测卡顿

1
2
3
4
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 记录进入和退出时间,判断耗时
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

2. 常用工具

  • FPS 监控工具
  • Time Profiler(定位卡顿调用栈)
  • Instruments 的 Core Animation(检测 UI 绘制)
  • 自行实现卡顿监控(可设定阈值,如超过 200ms)

三、RunLoop 卡顿优化策略

1. 减少主线程压力

  • 耗时操作异步处理:如网络、数据库、压缩、解码
  • 使用 GCD 或 NSOperation 将任务放在子线程
1
2
3
4
5
6
DispatchQueue.global().async {
// 耗时操作
DispatchQueue.main.async {
// 回主线程更新 UI
}
}

2. 延迟或分批执行任务

  • 使用 RunLoop idle 时机处理任务
  • 利用 CADisplayLink 拆分任务(典型如 tableView cell 异步绘制)
1
2
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(step)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

3. 图像加载优化

  • 图片解码、压缩处理放到子线程
  • 使用异步图片加载框架(如 SDWebImage、Kingfisher)

4. 优化 UI 绘制

  • 减少不必要的视图层级
  • 合理使用 rasterization, shouldRasterize,避免频繁重绘
  • 尽量使用 CALayer 做动画或绘制

5. 优化定时器

  • 避免使用过多高频率的 NSTimer
  • CADisplayLinkDispatchSourceTimer 精确控制
  • 注意释放定时器,防止循环引用或内存泄漏

四、进阶建议

使用 RunLoop 模型实现任务调度(如 YYKit 的 AsyncLayer)

核心思想:

  • 主线程每帧空闲阶段处理少量任务(每帧分批)
  • 避免阻塞主线程

利用信号量/ watchdog 检测严重卡顿

可用 dispatch_semaphore 配合子线程检测主线程长时间不响应,进行卡顿上报。

五、总结

优化手段 是否推荐 场景说明
耗时任务异步化 网络、图片、计算等
UI 绘制优化 多图页面、复杂视图
RunLoop 空闲任务调度 批量任务,降低瞬时压力
卡顿监控工具接入 提前发现问题
自定义 RunLoop 调度 高阶优化,如任务优先级

iOS 主线程卡顿监控实现方案

🔧 方案核心思路

  • 利用 CFRunLoopObserver 监听 RunLoop 各阶段;
  • 在子线程中开启定时器,检查主线程是否长时间停留在某个阶段;
  • 超过阈值(如 200ms)即判定为卡顿,并记录堆栈。

✅ 示例代码(Objective-C)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@interface MainThreadMonitor : NSObject
@end

@implementation MainThreadMonitor {
CFRunLoopObserverRef observer;
dispatch_semaphore_t semaphore;
CFRunLoopActivity activity;
BOOL isMonitoring;
}

- (instancetype)init {
if (self = [super init]) {
semaphore = dispatch_semaphore_create(0);
}
return self;
}

- (void)startMonitoring {
if (isMonitoring) return;
isMonitoring = YES;

// 1. 设置 RunLoop 观察者
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopCallback, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

// 2. 在子线程中检测主线程卡顿
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
while (self->isMonitoring) {
long result = dispatch_semaphore_wait(self->semaphore, dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC));
if (result != 0) {
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting) {
NSLog(@"⚠️ 发现卡顿");
[self logMainThreadStack];
}
}
}
});
}

// RunLoop 回调
static void runLoopCallback(CFRunLoopObserverRef observer, CFRunLoopActivity act, void *info) {
MainThreadMonitor *monitor = (__bridge MainThreadMonitor *)info;
monitor->activity = act;
dispatch_semaphore_signal(monitor->semaphore);
}

// 打印主线程堆栈
- (void)logMainThreadStack {
thread_act_t thread = mach_thread_self();
NSString *stack = [self getStackForThread:mach_thread_self()];
NSLog(@"卡顿堆栈:
%@", stack);
}

// 获取堆栈(可用 PLCrashReporter 或自己解析)
- (NSString *)getStackForThread:(thread_t)thread {
void *callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
NSMutableString *stack = [NSMutableString string];
for (int i = 0; i < frames; i++) {
[stack appendFormat:@"%s
", strs[i]];
}
free(strs);
return stack;
}

- (void)stopMonitoring {
if (!isMonitoring) return;
isMonitoring = NO;
if (observer) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
observer = NULL;
}
}

@end

📌 使用方式

1
2
MainThreadMonitor *monitor = [[MainThreadMonitor alloc] init];
[monitor startMonitoring];

🚀 补充优化建议

  • 上传卡顿信息 到服务端分析(包括时间戳、堆栈、设备信息等);
  • 使用 PLCrashReporter 捕获完整堆栈;
  • 避免误报(可设定 N 次连续卡顿后才触发上报);
  • 添加 FPS 监控作为辅助参考。

iOS 完整堆栈捕获实现原理

🔍 栈帧捕获的原理概述

1. 栈帧结构(以 x86_64 为例)

每次函数调用时,系统会在栈中保存:

  • 返回地址(return address)
  • 上一个函数的栈帧指针(frame pointer)
  • 函数参数和局部变量

因此,通过 当前帧的 frame pointer(rbp,可以回溯前一个函数的帧,形成调用链。

1
2
3
4
5
当前栈帧:
| return address |
| previous RBP | <---- 当前 RBP 指向这里
| arguments |
| local vars |

不断遍历 RBP,就可以得到一连串的调用路径。


2. backtrace()backtrace_symbols()

苹果系统提供了 glibc 的接口,能获取当前线程的调用栈信息:

1
2
3
void *callstack[128];
int frames = backtrace(callstack, 128); // 获得 return address 数组
char **strs = backtrace_symbols(callstack, frames); // 转为符号化字符串
  • backtrace():返回指针数组,每个指针是一个返回地址(PC 值)
  • backtrace_symbols():将地址解析为函数名(带符号信息)

✅ 适用于当前线程
❌ 无法捕获其他线程的堆栈


🧠 捕获其他线程堆栈的高级做法

1. 使用 Mach 线程 API 获取上下文

macOS 和 iOS 的线程由 Mach 线程驱动,可使用如下方式获取任意线程的状态:

1
2
3
4
thread_act_t thread = mach_thread_self(); // 或主线程 mach_port
_ARM_THREAD_STATE64 state;
mach_msg_type_number_t count = _ARM_THREAD_STATE64_COUNT;
thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, &count);
  • 可拿到寄存器信息(如 pc, lr, fp
  • fp(frame pointer)开始,手动解析内存获取调用链

2. 使用第三方库封装(推荐)

它们在崩溃或卡顿时:

  • 挂起线程
  • 读取线程上下文寄存器(如 pcspfp
  • 手动回溯调用栈
  • 进行符号化输出

⚠️ 注意事项

  • 手动读取线程寄存器有风险(需挂起线程,防止栈变化)
  • 对于优化或内联函数,可能无法获取完整信息
  • 系统函数符号化依赖 dSYM 或符号表

✅ 各方案对比

方法 优势 局限
backtrace 简单易用,当前线程调用栈 仅当前线程,信息不完整
Mach API + 寄存器解析 可捕获任意线程堆栈 实现复杂,需挂起线程
PLCrashReporter 稳定封装,支持崩溃/卡顿/符号化 增加依赖包,大小略大

📌 推荐实践

  • 异常或卡顿发生时暂停主线程 → 获取寄存器状态 → 回溯调用链
  • 使用第三方库如 PLCrashReporter 进行安全封装
  • 搭配符号表(.dSYM)进行符号化,以便远程分析