RunLoop 深度解析
RunLoop 深度解析
目录
- RunLoop 基础概念
- RunLoop 底层原理
- RunLoop 源码分析
- RunLoop 的实际应用
- 常见应用场景
- 最佳实践
- 实际代码示例
1. RunLoop 基础概念
1.1 什么是 RunLoop
RunLoop(运行循环)是 iOS/macOS 开发中的一个核心概念,它是一个事件处理循环,用于管理和调度线程上的任务。
核心作用:
- 保持线程存活,避免线程执行完任务后立即退出
- 处理各种事件(触摸事件、定时器、网络请求等)
- 在没有事件时让线程休眠,节省 CPU 资源
- 在有事件时唤醒线程,及时处理
1.2 RunLoop 与线程的关系
重要特性:
- 一对一关系:每个线程都有且仅有一个 RunLoop
- 懒加载:RunLoop 不会自动创建,需要时才会创建
- 主线程 RunLoop:应用启动时自动创建并运行
- 子线程 RunLoop:默认不创建,需要手动获取或创建
// 获取当前线程的 RunLoop(如果不存在会自动创建)
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];// 获取主线程的 RunLoop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
1.3 RunLoop 的基本结构
RunLoop 包含以下几个核心组件:
1.3.1 Mode(模式)
RunLoop 运行在特定的 Mode 下,不同 Mode 包含不同的 Source、Timer 和 Observer。
常见的 Mode:
NSDefaultRunLoopMode:默认模式,大多数操作在此模式下UITrackingRunLoopMode:UI 追踪模式,ScrollView 滑动时切换到此模式NSRunLoopCommonModes:占位模式,包含 Default 和 Tracking 模式
Mode 切换的意义:
- 当 ScrollView 滑动时,RunLoop 切换到
UITrackingRunLoopMode - 此时
NSDefaultRunLoopMode下的 Timer 会暂停 - 只有添加到
NSRunLoopCommonModes的 Timer 才会继续执行
1.3.2 Source(事件源)
Source 是 RunLoop 要处理的事件源,分为两类:
Source0:
- 需要手动标记为待处理(signal)
- 处理 App 内部事件,如触摸事件、performSelector 等
- 不能主动唤醒 RunLoop
Source1:
- 基于 mach_port,可以主动唤醒 RunLoop
- 处理系统级事件,如端口通信、系统信号等
1.3.3 Timer(定时器)
Timer 是基于时间的触发器,必须添加到 RunLoop 才能正常工作。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 添加到 RunLoop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
1.3.4 Observer(观察者)
Observer 用于监听 RunLoop 的状态变化,可以监控以下时机:
- RunLoop 即将进入
- RunLoop 即将处理 Timer
- RunLoop 即将处理 Source
- RunLoop 即将进入休眠
- RunLoop 被唤醒
- RunLoop 即将退出
1.4 RunLoop 的生命周期
启动 → 进入循环 → 处理事件 → 休眠 → 唤醒 → 处理事件 → ... → 退出
关键点:
- RunLoop 在没有事件时会进入休眠状态
- 有事件时会自动唤醒
- 可以通过
CFRunLoopStop()主动停止 RunLoop
2. RunLoop 底层原理
2.1 RunLoop 的运行机制
RunLoop 的核心是一个 do-while 循环,不断检查是否有事件需要处理。
伪代码表示:
void CFRunLoopRun() {int32_t result;do {result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, returnAfterSourceHandled);} while (result != kCFRunLoopRunStopped);
}
运行流程:
- 通知 Observer:即将进入 RunLoop
- 通知 Observer:即将处理 Timer
- 通知 Observer:即将处理 Source0
- 处理 Source0
- 如果有 Source1 就绪,立即处理
- 通知 Observer:线程即将休眠
- 休眠,等待被唤醒
- 端口有消息(Source1)
- Timer 到时间
- RunLoop 超时
- 被手动唤醒
- 通知 Observer:线程被唤醒
- 处理唤醒时收到的事件
- 如果是 Timer,处理 Timer 回调
- 如果是 dispatch 到 main queue 的 block,执行 block
- 如果是 Source1,处理 Source1
- 根据结果决定是否继续循环
- 如果处理了事件,回到步骤 2
- 如果超时,回到步骤 2
- 如果被停止,退出循环
2.2 Mode 切换机制
RunLoop 在同一时间只能运行在一个 Mode 下,切换 Mode 需要退出当前循环,重新以新 Mode 进入。
Mode 切换的触发时机:
- ScrollView 开始滑动:切换到
UITrackingRunLoopMode - ScrollView 停止滑动:切换回
NSDefaultRunLoopMode - 系统事件:可能切换到其他 Mode
为什么需要 Mode 切换?
- 隔离不同场景的事件处理
- 避免滑动时 Timer 影响滚动性能
- 提供更精细的事件控制
2.3 Source 的分类和处理
Source0(非基于 Port)
特点:
- 需要手动标记为待处理状态
- 不能主动唤醒 RunLoop
- 处理 App 内部事件
处理流程:
- 事件发生(如触摸事件)
- 系统标记 Source0 为待处理
- RunLoop 被其他方式唤醒(如 Source1)
- RunLoop 检查并处理所有待处理的 Source0
- 调用 Source0 的回调函数
Source1(基于 Port)
特点:
- 基于 mach_port(进程间通信端口)
- 可以主动唤醒 RunLoop
- 处理系统级事件
处理流程:
- 系统事件发生(如端口收到消息)
- 内核通过 mach_port 发送消息
- Source1 接收到消息,唤醒 RunLoop
- RunLoop 处理 Source1 事件
- 调用 Source1 的回调函数
2.4 Timer 的实现原理
Timer 在底层是基于 mach_absolute_time() 实现的,精度很高。
Timer 的工作机制:
- 创建 Timer:设置时间间隔和回调
- 添加到 RunLoop:Timer 被添加到当前 Mode 的 Timer 列表
- RunLoop 检查:每次循环检查 Timer 是否到期
- 触发回调:如果到期,调用 Timer 的回调方法
- 重复执行:如果是重复 Timer,重新计算下次触发时间
Timer 的精度问题:
// Timer 不是实时的,会受到 RunLoop 的影响
// 如果 RunLoop 正在处理耗时操作,Timer 可能延迟触发
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 添加到 CommonModes 可以避免滑动时暂停
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
2.5 Observer 的监控机制
Observer 使用回调函数监听 RunLoop 的状态变化。
可监控的状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoopkCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2), // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoopkCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};
Observer 的应用:
- 性能监控:检测主线程卡顿
- 自动释放池:在合适的时机释放对象
- 日志记录:跟踪 RunLoop 的运行状态
2.6 RunLoop 的休眠机制
RunLoop 的休眠不是简单的 sleep(),而是基于 mach_msg() 的休眠。
休眠原理:
- RunLoop 调用
mach_msg()等待消息 - 如果没有消息,线程进入内核态休眠
- 有消息时,内核唤醒线程
- 线程回到用户态,继续执行
优势:
- 真正的休眠,不占用 CPU 资源
- 可以被精确唤醒
- 响应速度快
3. RunLoop 源码分析
3.1 CFRunLoopRef 结构体解析
CFRunLoop 在底层是一个结构体,包含以下关键成员:
struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock; // 锁,保证线程安全__CFPort _wakeUpPort; // 用于唤醒 RunLoop 的端口Boolean _stopped; // 是否已停止Boolean _ignoreWakeUps; // 是否忽略唤醒CFMutableSetRef _commonModes; // 存储所有 common modesCFMutableSetRef _commonModeItems; // 存储所有 common mode itemsCFRunLoopModeRef _currentMode; // 当前运行的 modeCFMutableSetRef _modes; // 存储所有 modesstruct _block_item *_blocks_head; // 待执行的 block 链表头struct _block_item *_blocks_tail; // 待执行的 block 链表尾CFAbsoluteTime _runTime; // RunLoop 开始运行的时间CFAbsoluteTime _sleepTime; // RunLoop 进入休眠的时间CFTypeRef _counterpart; // 对应的 NSRunLoop
};
关键字段说明:
_currentMode:当前运行的 Mode,RunLoop 一次只能运行在一个 Mode 下_modes:所有注册的 Mode 集合_commonModes:所有标记为 common 的 Mode 名称集合_commonModeItems:所有添加到 common modes 的 items(Source、Timer、Observer)
3.2 CFRunLoopMode 结构体解析
每个 Mode 包含该 Mode 下的所有 Source、Timer 和 Observer:
struct __CFRunLoopMode {CFRuntimeBase _base;pthread_mutex_t _lock;CFStringRef _name; // Mode 名称,如 "kCFRunLoopDefaultMode"Boolean _stopped;CFMutableSetRef _sources0; // Source0 集合CFMutableSetRef _sources1; // Source1 集合CFMutableArrayRef _observers; // Observer 数组CFMutableArrayRef _timers; // Timer 数组CFMutableDictionaryRef _portToV1SourceMap; // port 到 Source1 的映射__CFPortSet _portSet; // 所有 port 的集合CFIndex _observerMask; // Observer 的掩码uint64_t _timerSoftDeadline; // Timer 软截止时间uint64_t _timerHardDeadline; // Timer 硬截止时间
};
关键字段说明:
_sources0和_sources1:分别存储两种类型的 Source_timers:存储该 Mode 下的所有 Timer_observers:存储该 Mode 下的所有 Observer_portToV1SourceMap:用于快速查找 port 对应的 Source1
3.3 Source/Timer/Observer 的数据结构
CFRunLoopSource
struct __CFRunLoopSource {CFRuntimeBase _base;uint32_t _bits; // 标志位pthread_mutex_t _lock;CFIndex _order; // 优先级CFMutableBagRef _runLoops; // 该 Source 被添加到哪些 RunLoopunion {CFRunLoopSourceContext version0; // Source0 的上下文CFRunLoopSourceContext1 version1; // Source1 的上下文} _context;
};
Source0 和 Source1 的区别:
- Source0:
_context.version0包含回调函数,需要手动标记为待处理 - Source1:
_context.version1包含 mach_port,可以主动唤醒 RunLoop
CFRunLoopTimer
struct __CFRunLoopTimer {CFRuntimeBase _base;uint16_t _bits; // 标志位pthread_mutex_t _lock;CFRunLoopRef _runLoop; // 所属的 RunLoopCFMutableSetRef _rlModes; // 添加到哪些 modesCFAbsoluteTime _nextFireDate; // 下次触发时间CFTimeInterval _interval; // 时间间隔CFTimeInterval _tolerance; // 容差uint64_t _fireTSR; // 触发时间戳CFIndex _order; // 优先级CFRunLoopTimerCallBack _callout; // 回调函数CFRunLoopTimerContext _context; // 上下文
};
CFRunLoopObserver
struct __CFRunLoopObserver {CFRuntimeBase _base;uint32_t _bits; // 标志位pthread_mutex_t _lock;CFRunLoopRef _runLoop; // 所属的 RunLoopCFMutableSetRef _rlModes; // 添加到哪些 modesCFOptionFlags _activities; // 监听哪些活动CFIndex _order; // 优先级CFRunLoopObserverCallBack _callout; // 回调函数CFRunLoopObserverContext _context; // 上下文
};
3.4 关键函数实现分析
CFRunLoopRun
void CFRunLoopRun(void) {int32_t result;do {result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, // 超时时间(几乎无限)true); // returnAfterSourceHandled} while (result != kCFRunLoopRunStopped);
}
说明:
- 不断调用
CFRunLoopRunInMode,直到 RunLoop 被停止 - 使用
kCFRunLoopDefaultMode作为默认 Mode - 超时时间设置为很大,几乎不会超时
CFRunLoopRunInMode
这是 RunLoop 的核心函数,主要逻辑:
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {// 1. 获取或创建 RunLoopCFRunLoopRef rl = CFRunLoopGetCurrent();// 2. 根据 modeName 查找对应的 ModeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);// 3. 如果 Mode 不存在,返回错误if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {return kCFRunLoopRunFinished;}// 4. 设置当前 ModeCFRunLoopModeRef previousMode = rl->_currentMode;rl->_currentMode = currentMode;// 5. 通知 Observer:即将进入 RunLoop__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);// 6. 进入主循环result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);// 7. 恢复之前的 Moderl->_currentMode = previousMode;// 8. 通知 Observer:即将退出 RunLoop__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);return result;
}
__CFRunLoopRun(核心循环)
这是 RunLoop 真正执行循环的地方:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {int32_t retVal = 0;do {// 1. 通知 Observer:即将处理 Timer__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);// 2. 通知 Observer:即将处理 Source__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);// 3. 处理 Source0__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);// 4. 检查是否有 Source1 就绪Boolean hasSource1 = __CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm);if (hasSource1) {// 5. 处理 Source1__CFRunLoopDoSource1(rl, rlm, msg, msg->msgh_size, &reply);}// 6. 处理主队列的 block__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);// 7. 通知 Observer:即将进入休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);// 8. 进入休眠,等待消息__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm);// 9. 通知 Observer:被唤醒__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);// 10. 处理唤醒时收到的事件if (MACH_PORT_NULL != livePort) {// 处理 Source1__CFRunLoopDoSource1(rl, rlm, msg, msg->msgh_size, &reply);} else if (timers_all_fired) {// 处理 Timer__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());}// 11. 处理主队列的 block__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);// 12. 检查是否需要退出if (__CFRunLoopIsStopped(rl)) {retVal = kCFRunLoopRunStopped;} else if (rlm->_stopped) {rlm->_stopped = false;retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {retVal = kCFRunLoopRunFinished;}} while (0 == retVal);return retVal;
}
3.5 Common Modes 的实现机制
Common Modes 是一个特殊的概念,它不是一个真正的 Mode,而是一个占位符。
实现原理:
NSRunLoopCommonModes是一个字符串数组,包含多个 Mode 名称- 当向 Common Modes 添加 item 时,实际上会添加到所有 common modes 中
- 当切换 Mode 时,common mode items 会自动跟随切换
源码中的处理:
// 添加 item 到 common modes
void CFRunLoopAddItemToCommonModes(CFRunLoopRef rl, CFRunLoopModeItemRef item) {// 遍历所有 common modesCFSetApplyFunction(rl->_commonModes, ^(const void *value, void *context) {CFStringRef modeName = (CFStringRef)value;CFRunLoopModeRef mode = __CFRunLoopFindMode(rl, modeName, false);if (mode) {// 将 item 添加到该 mode__CFRunLoopModeAddItem(mode, item);}}, NULL);
}
3.6 自动释放池与 RunLoop
自动释放池的创建和释放与 RunLoop 密切相关:
时机:
- 在
kCFRunLoopEntry时创建自动释放池 - 在
kCFRunLoopBeforeWaiting时释放旧的池并创建新池 - 在
kCFRunLoopExit时释放自动释放池
源码实现:
// 在 RunLoop 进入时
if (kCFRunLoopEntry == activity) {_objc_autoreleasePoolPush();
}// 在 RunLoop 即将休眠时
if (kCFRunLoopBeforeWaiting == activity) {_objc_autoreleasePoolPop();_objc_autoreleasePoolPush();
}// 在 RunLoop 退出时
if (kCFRunLoopExit == activity) {_objc_autoreleasePoolPop();
}
这样设计的好处是:
- 在一次 RunLoop 循环中创建的对象,在循环结束时统一释放
- 避免内存峰值过高
- 及时释放不需要的对象
4. RunLoop 的实际应用
4.1 主线程 RunLoop 与 UI 渲染
主线程的 RunLoop 是 iOS 应用的核心,负责处理所有 UI 相关的事件。
主线程 RunLoop 的工作流程:
-
触摸事件处理
- 用户触摸屏幕 → Source1 接收事件 → 唤醒 RunLoop
- RunLoop 处理触摸事件 → 调用 UIResponder 的方法
- 事件沿着响应链传递
-
UI 更新
- 修改 UI 属性(如 frame、backgroundColor)不会立即生效
- 修改会在当前 RunLoop 循环结束时统一渲染
- 通过
setNeedsDisplay标记需要重绘的视图
-
布局更新
setNeedsLayout标记需要重新布局- 在 RunLoop 的
kCFRunLoopBeforeWaiting时执行布局计算 - 然后执行绘制操作
代码示例:
// 修改视图属性
self.view.backgroundColor = [UIColor redColor];
// 此时视图不会立即更新,而是等到 RunLoop 休眠前统一更新// 强制立即更新(不推荐,会阻塞主线程)
[self.view layoutIfNeeded];
4.2 子线程中创建 RunLoop
默认情况下,子线程没有 RunLoop,线程执行完任务后就会退出。如果需要让子线程保持存活并处理任务,需要手动创建并运行 RunLoop。
创建常驻线程的步骤:
- 创建线程
- 在线程中获取 RunLoop(会自动创建)
- 添加 Source 或 Timer 到 RunLoop(否则 RunLoop 会立即退出)
- 运行 RunLoop
代码示例:
// 创建常驻线程
@property (nonatomic, strong) NSThread *backgroundThread;- (void)createBackgroundThread {self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];[self.backgroundThread start];
}- (void)threadEntry {@autoreleasepool {// 获取当前线程的 RunLoop(会自动创建)NSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 添加一个 Port 作为 Source,防止 RunLoop 退出NSPort *port = [NSPort port];[runLoop addPort:port forMode:NSDefaultRunLoopMode];// 运行 RunLoop(会一直运行,直到线程退出)[runLoop run];}
}// 在常驻线程上执行任务
- (void)performTaskOnBackgroundThread:(void (^)(void))task {[self performSelector:@selector(executeTask:) onThread:self.backgroundThread withObject:task waitUntilDone:NO];
}- (void)executeTask:(void (^)(void))task {if (task) {task();}
}
4.3 使用 RunLoop 实现常驻线程
常驻线程常用于后台任务处理,如网络请求、文件操作等。
应用场景:
- 后台下载任务
- 定时任务执行
- 数据同步
- 日志记录
注意事项:
- 必须添加 Source 或 Timer,否则 RunLoop 会立即退出
- 使用 Port 是最简单的方式
- 线程退出前要停止 RunLoop
4.4 NSTimer 与 RunLoop 的关系
NSTimer 必须添加到 RunLoop 才能正常工作,这是很多开发者容易忽略的点。
常见问题:
-
Timer 不触发
- 原因:没有添加到 RunLoop 或添加到了错误的 Mode
- 解决:确保添加到正确的 RunLoop 和 Mode
-
滑动时 Timer 暂停
- 原因:Timer 添加到了
NSDefaultRunLoopMode,滑动时切换到UITrackingRunLoopMode - 解决:添加到
NSRunLoopCommonModes
- 原因:Timer 添加到了
代码示例:
// 方式1:使用 scheduledTimerWithTimeInterval(自动添加到当前 RunLoop 的 DefaultMode)
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];// 方式2:手动创建并添加到 RunLoop
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 添加到 DefaultMode(滑动时会暂停)
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];// 添加到 CommonModes(滑动时不会暂停)
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSRunLoopCommonModes];// 方式3:使用 GCD Timer(不依赖 RunLoop,更精确)
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.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{// 定时任务
});
dispatch_resume(timer);
4.5 网络请求与 RunLoop
NSURLConnection 的代理方法默认在创建连接的线程的 RunLoop 中回调。
问题场景:
- 在子线程中创建 NSURLConnection
- 子线程没有运行 RunLoop
- 代理方法不会被调用
解决方案:
// 方式1:在主线程创建连接
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];// 方式2:在子线程运行 RunLoop
dispatch_async(dispatch_get_global_queue(0, 0), ^{NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[connection scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];[connection start];[runLoop run]; // 运行 RunLoop,等待回调
});// 方式3:使用 NSURLSession(推荐,不依赖 RunLoop)
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {// 处理响应
}];
[task resume];
4.6 事件响应链与 RunLoop
触摸事件的处理流程与 RunLoop 密切相关:
- 事件产生:用户触摸屏幕
- 系统捕获:IOKit 捕获触摸事件
- 事件传递:通过 Source1 传递给主线程 RunLoop
- RunLoop 唤醒:RunLoop 被唤醒
- 事件分发:RunLoop 处理 Source1,调用 UIApplication 的
sendEvent: - 响应链传递:事件沿着响应链传递(UIApplication → UIWindow → ViewController → View)
- 处理事件:找到合适的响应者处理事件
代码示例:
// 重写 hitTest:withEvent: 自定义事件响应
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {// 检查是否在视图范围内if (![self pointInside:point withEvent:event]) {return nil;}// 检查子视图for (UIView *subview in [self.subviews reverseObjectEnumerator]) {CGPoint convertedPoint = [self convertPoint:point toView:subview];UIView *hitView = [subview hitTest:convertedPoint withEvent:event];if (hitView) {return hitView;}}return self;
}// 重写 touchesBegan:withEvent: 处理触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// 处理触摸开始
}
4.7 performSelector 与 RunLoop
performSelector:onThread:withObject:waitUntilDone: 依赖于目标线程的 RunLoop。
工作原理:
- 在目标线程的 RunLoop 中添加一个 Source0
- 当 RunLoop 运行时,会处理这个 Source0
- 调用指定的 selector
注意事项:
- 目标线程必须有运行中的 RunLoop
waitUntilDone:YES会阻塞当前线程waitUntilDone:NO不会阻塞,但需要目标线程的 RunLoop 运行
代码示例:
// 在主线程执行(waitUntilDone:NO,不阻塞)
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];// 在指定线程执行
[self performSelector:@selector(doWork) onThread:self.backgroundThread withObject:nil waitUntilDone:NO];// 延迟执行(依赖于当前线程的 RunLoop)
[self performSelector:@selector(delayedAction) withObject:nil afterDelay:2.0];// 取消延迟执行
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayedAction) object:nil];
5. 常见应用场景
5.1 性能监控(卡顿检测)
通过监控 RunLoop 的状态,可以检测主线程的卡顿情况。
原理:
- 添加 Observer 监听 RunLoop 的状态
- 记录
kCFRunLoopBeforeWaiting和kCFRunLoopAfterWaiting的时间间隔 - 如果间隔过长,说明主线程被阻塞
实现代码:
@interface RunLoopMonitor : NSObject
@property (nonatomic, assign) BOOL isMonitoring;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) CFRunLoopActivity activity;
@end@implementation RunLoopMonitor+ (instancetype)sharedInstance {static RunLoopMonitor *instance = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[RunLoopMonitor alloc] init];});return instance;
}- (void)startMonitoring {if (self.isMonitoring) {return;}self.isMonitoring = YES;self.semaphore = dispatch_semaphore_create(0);// 创建 ObserverCFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);// 添加到主线程 RunLoopCFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);// 在子线程中监控dispatch_async(dispatch_get_global_queue(0, 0), ^{while (self.isMonitoring) {// 等待 50ms,如果超时说明主线程可能卡顿long st = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));if (st != 0) {if (!self.semaphore) {self.isMonitoring = NO;self.activity = 0;break;}// 检查 RunLoop 状态if (self.activity == kCFRunLoopBeforeSources || self.activity == kCFRunLoopAfterWaiting) {// 可能发生卡顿,记录堆栈信息[self logStackInfo];}}}});
}static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {RunLoopMonitor *monitor = (__bridge RunLoopMonitor *)info;monitor.activity = activity;dispatch_semaphore_signal(monitor.semaphore);
}- (void)logStackInfo {NSArray *callStack = [NSThread callStackSymbols];NSLog(@"主线程可能卡顿,堆栈信息:\n%@", callStack);
}- (void)stopMonitoring {self.isMonitoring = NO;self.semaphore = nil;
}@end
5.2 自动释放池的时机
了解 RunLoop 与自动释放池的关系,有助于优化内存管理。
自动释放池的创建和释放时机:
kCFRunLoopEntry:创建自动释放池kCFRunLoopBeforeWaiting:释放旧池,创建新池kCFRunLoopExit:释放自动释放池
优化建议:
- 在 RunLoop 循环中创建大量临时对象时,考虑手动创建
@autoreleasepool - 避免在单次 RunLoop 循环中创建过多对象
代码示例:
// 在循环中处理大量数据时,使用 @autoreleasepool
for (int i = 0; i < 10000; i++) {@autoreleasepool {// 创建临时对象NSArray *tempArray = [self processData:i];// 使用 tempArray}// tempArray 在这里已经被释放
}
5.3 图片加载优化
利用 RunLoop 的特性,可以在合适的时机加载图片,避免阻塞主线程。
优化策略:
- 在
kCFRunLoopBeforeWaiting时加载图片 - 避免在用户交互时加载图片
- 使用 Observer 监控 RunLoop 状态
代码示例:
@interface ImageLoader : NSObject
@property (nonatomic, strong) NSMutableArray *imageLoadTasks;
@end@implementation ImageLoader- (void)setupRunLoopObserver {CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopBeforeWaiting | kCFRunLoopExit,YES,0,^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {// 在 RunLoop 即将休眠时加载图片[self loadImagesIfNeeded];});CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);CFRelease(observer);
}- (void)loadImagesIfNeeded {if (self.imageLoadTasks.count == 0) {return;}// 每次只加载一张图片,避免阻塞ImageLoadTask *task = [self.imageLoadTasks firstObject];[self.imageLoadTasks removeObjectAtIndex:0];// 加载图片[self loadImage:task];
}@end
5.4 网络请求优化
虽然现在推荐使用 NSURLSession,但了解 RunLoop 与网络请求的关系仍然有用。
优化要点:
- 确保网络请求的回调线程有运行中的 RunLoop
- 使用 NSURLSession 可以避免 RunLoop 相关问题
- 在后台线程处理网络请求时,注意 RunLoop 的生命周期
5.5 后台任务处理
使用 RunLoop 实现后台任务的持续处理。
应用场景:
- 后台数据同步
- 日志上传
- 缓存清理
代码示例:
@interface BackgroundTaskManager : NSObject
@property (nonatomic, strong) NSThread *backgroundThread;
@property (nonatomic, strong) NSMutableArray *taskQueue;
@end@implementation BackgroundTaskManager- (void)startBackgroundThread {self.taskQueue = [NSMutableArray array];self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];[self.backgroundThread start];
}- (void)threadEntry {@autoreleasepool {NSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 添加 Port,保持 RunLoop 运行NSPort *port = [NSPort port];[runLoop addPort:port forMode:NSDefaultRunLoopMode];// 添加 Timer,定期检查任务队列NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(processTasks) userInfo:nil repeats:YES];[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];[runLoop run];}
}- (void)processTasks {if (self.taskQueue.count > 0) {Task *task = [self.taskQueue firstObject];[self.taskQueue removeObjectAtIndex:0];[self executeTask:task];}
}- (void)addTask:(Task *)task {[self performSelector:@selector(addTaskToQueue:) onThread:self.backgroundThread withObject:task waitUntilDone:NO];
}- (void)addTaskToQueue:(Task *)task {[self.taskQueue addObject:task];
}@end
5.6 延迟执行优化
利用 RunLoop 的特性,实现更精确的延迟执行。
场景:
- 需要在用户停止操作后执行任务
- 避免频繁执行某些操作
- 实现防抖(debounce)功能
代码示例:
@interface Debouncer : NSObject
@property (nonatomic, strong) NSTimer *timer;
@end@implementation Debouncer- (void)debounce:(void (^)(void))block delay:(NSTimeInterval)delay {// 取消之前的 Timer[self.timer invalidate];// 创建新的 Timerself.timer = [NSTimer scheduledTimerWithTimeInterval:delay target:self selector:@selector(executeBlock:) userInfo:block repeats:NO];// 添加到 CommonModes,确保滑动时也能执行[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}- (void)executeBlock:(NSTimer *)timer {void (^block)(void) = timer.userInfo;if (block) {block();}[self.timer invalidate];self.timer = nil;
}@end// 使用示例
Debouncer *debouncer = [[Debouncer alloc] init];
[debouncer debounce:^{// 用户停止操作 0.5 秒后执行NSLog(@"执行搜索");
} delay:0.5];
6. 最佳实践
6.1 何时需要手动管理 RunLoop
需要手动管理的情况:
- 创建常驻后台线程
- 在子线程中使用 NSURLConnection(旧 API)
- 需要在特定时机执行任务
- 实现自定义的事件处理机制
不需要手动管理的情况:
- 主线程(系统自动管理)
- 使用 GCD 和 NSOperationQueue(不依赖 RunLoop)
- 使用 NSURLSession(不依赖 RunLoop)
- 大多数日常开发场景
6.2 RunLoop 的常见陷阱和注意事项
陷阱1:Timer 在滑动时暂停
问题:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 滑动 ScrollView 时,Timer 会暂停
解决:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
陷阱2:子线程 RunLoop 立即退出
问题:
dispatch_async(dispatch_get_global_queue(0, 0), ^{NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[runLoop run]; // 立即退出,因为没有 Source 或 Timer
});
解决:
dispatch_async(dispatch_get_global_queue(0, 0), ^{NSRunLoop *runLoop = [NSRunLoop currentRunLoop];NSPort *port = [NSPort port];[runLoop addPort:port forMode:NSDefaultRunLoopMode];[runLoop run]; // 现在会持续运行
});
陷阱3:performSelector 在子线程不执行
问题:
dispatch_async(dispatch_get_global_queue(0, 0), ^{[self performSelector:@selector(doWork) withObject:nil afterDelay:1.0];// doWork 不会执行,因为子线程的 RunLoop 没有运行
});
解决:
dispatch_async(dispatch_get_global_queue(0, 0), ^{NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[self performSelector:@selector(doWork) withObject:nil afterDelay:1.0];[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
});
陷阱4:RunLoop 无法停止
问题:
// 错误的方式
CFRunLoopStop(CFRunLoopGetCurrent()); // 可能无法立即停止
解决:
// 正确的方式:使用标志位
self.shouldStop = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
6.3 性能优化建议
-
避免在主线程 RunLoop 中执行耗时操作
- 耗时操作会阻塞主线程,导致 UI 卡顿
- 使用后台线程处理耗时任务
-
合理使用 CommonModes
- 只有需要在滑动时也执行的任务才添加到 CommonModes
- 避免添加过多任务到 CommonModes,影响滑动性能
-
及时移除不需要的 Observer、Timer、Source
- 避免内存泄漏
- 减少 RunLoop 的负担
-
使用 GCD 替代 RunLoop(如果可能)
- GCD 更简单,性能更好
- 不依赖 RunLoop,更可靠
6.4 调试技巧
- 打印 RunLoop 信息
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSLog(@"Current RunLoop: %@", runLoop);
NSLog(@"Current Mode: %@", CFRunLoopCopyCurrentMode(runLoop));
- 监控 RunLoop 状态
// 添加 Observer 监控所有状态变化
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {switch (activity) {case kCFRunLoopEntry:NSLog(@"RunLoop 进入");break;case kCFRunLoopBeforeTimers:NSLog(@"即将处理 Timer");break;case kCFRunLoopBeforeSources:NSLog(@"即将处理 Source");break;case kCFRunLoopBeforeWaiting:NSLog(@"即将进入休眠");break;case kCFRunLoopAfterWaiting:NSLog(@"被唤醒");break;case kCFRunLoopExit:NSLog(@"RunLoop 退出");break;}});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
- 检查 Timer 是否正确添加
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"RunLoop modes: %@", [runLoop currentMode]);
7. 实际代码示例
7.1 创建常驻线程的完整示例
// .h 文件
@interface PersistentThread : NSObject
- (void)start;
- (void)stop;
- (void)performTask:(void (^)(void))task;
@end// .m 文件
@interface PersistentThread ()
@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, strong) NSPort *port;
@property (nonatomic, assign) BOOL shouldStop;
@end@implementation PersistentThread- (void)start {if (self.thread && self.thread.isExecuting) {return;}self.shouldStop = NO;self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];[self.thread start];
}- (void)threadMain {@autoreleasepool {NSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 添加 Port,保持 RunLoop 运行self.port = [NSPort port];[runLoop addPort:self.port forMode:NSDefaultRunLoopMode];// 运行 RunLoopwhile (!self.shouldStop) {@autoreleasepool {[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];}}// 清理[runLoop removePort:self.port forMode:NSDefaultRunLoopMode];self.port = nil;}
}- (void)stop {self.shouldStop = YES;if (self.port) {// 通过 Port 发送消息唤醒 RunLoop[self.port sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];}
}- (void)performTask:(void (^)(void))task {if (!task || !self.thread || !self.thread.isExecuting) {return;}[self performSelector:@selector(executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}- (void)executeTask:(void (^)(void))task {if (task) {task();}
}@end
7.2 使用 RunLoop Observer 监控性能
@interface PerformanceMonitor : NSObject
+ (instancetype)sharedInstance;
- (void)startMonitoring;
- (void)stopMonitoring;
@end@interface PerformanceMonitor ()
@property (nonatomic, assign) BOOL isMonitoring;
@property (nonatomic, strong) CFRunLoopObserverRef observer;
@property (nonatomic, assign) NSTimeInterval lastActivityTime;
@end@implementation PerformanceMonitor+ (instancetype)sharedInstance {static PerformanceMonitor *instance = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[PerformanceMonitor alloc] init];});return instance;
}- (void)startMonitoring {if (self.isMonitoring) {return;}self.isMonitoring = YES;self.lastActivityTime = CACurrentMediaTime();// 创建 ObserverCFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};self.observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallback,&context);// 添加到主线程 RunLoopCFRunLoopAddObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);
}static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {PerformanceMonitor *monitor = (__bridge PerformanceMonitor *)info;NSTimeInterval currentTime = CACurrentMediaTime();NSTimeInterval interval = currentTime - monitor.lastActivityTime;if (interval > 0.016) { // 超过一帧的时间(60fps = 16.67ms)NSLog(@"⚠️ 主线程可能卡顿,间隔: %.3f 秒", interval);}monitor.lastActivityTime = currentTime;
}- (void)stopMonitoring {if (!self.isMonitoring) {return;}self.isMonitoring = NO;if (self.observer) {CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);CFRelease(self.observer);self.observer = NULL;}
}- (void)dealloc {[self stopMonitoring];
}@end
7.3 自定义 Source 的实现
@interface CustomSource : NSObject
- (void)fire;
@end@interface CustomSource ()
{CFRunLoopSourceRef _source;CFRunLoopRef _runLoop;
}
@end@implementation CustomSource- (instancetype)init {self = [super init];if (self) {[self setupSource];}return self;
}- (void)setupSource {// 创建 Source0 的上下文CFRunLoopSourceContext context = {0};context.info = (__bridge void *)self;context.perform = &sourcePerform;context.schedule = &sourceSchedule;context.cancel = &sourceCancel;// 创建 Source_source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
}static void sourcePerform(void *info) {CustomSource *source = (__bridge CustomSource *)info;NSLog(@"CustomSource 被触发");// 执行自定义逻辑
}static void sourceSchedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {CustomSource *source = (__bridge CustomSource *)info;source->_runLoop = rl;NSLog(@"CustomSource 被调度到 RunLoop");
}static void sourceCancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {CustomSource *source = (__bridge CustomSource *)info;source->_runLoop = NULL;NSLog(@"CustomSource 被取消");
}- (void)addToRunLoop:(NSRunLoop *)runLoop forMode:(NSString *)mode {CFRunLoopAddSource([runLoop getCFRunLoop], _source, (__bridge CFStringRef)mode);
}- (void)removeFromRunLoop:(NSRunLoop *)runLoop forMode:(NSString *)mode {CFRunLoopRemoveSource([runLoop getCFRunLoop], _source, (__bridge CFStringRef)mode);
}- (void)fire {// 标记 Source 为待处理CFRunLoopSourceSignal(_source);// 如果有 RunLoop,唤醒它if (_runLoop) {CFRunLoopWakeUp(_runLoop);}
}- (void)dealloc {if (_source) {CFRelease(_source);}
}@end
7.4 常见问题的解决方案
问题1:Timer 在后台不工作
// 解决方案:使用后台任务和本地通知
- (void)setupBackgroundTimer {// 注册后台任务[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{// 任务过期处理}];// 使用 NSTimer(在后台有限时间内有效)NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:60.0 target:self selector:@selector(backgroundTask) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
问题2:网络请求在子线程回调失败
// 解决方案:确保子线程的 RunLoop 运行
- (void)networkRequestInBackground {dispatch_async(dispatch_get_global_queue(0, 0), ^{NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://api.example.com"]];// 使用 NSURLConnection(需要 RunLoop)NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[connection scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];[connection start];// 运行 RunLoop,等待回调[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:30.0]];});
}// 更好的方案:使用 NSURLSession(不依赖 RunLoop)
- (void)networkRequestWithSession {NSURLSession *session = [NSURLSession sharedSession];NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://api.example.com"]completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {// 处理响应}];[task resume];
}
问题3:performSelector 延迟执行不准确
// 问题:performSelector:withObject:afterDelay: 依赖于 RunLoop,可能不准确
[self performSelector:@selector(doSomething) withObject:nil afterDelay:1.0];// 解决方案1:使用 GCD(更准确)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{[self doSomething];
});// 解决方案2:使用 NSTimer(添加到 CommonModes)
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
参考资料
- Apple Developer Documentation - Run Loops
- CFRunLoop 源码
