当前位置: 首页 > news >正文

iOS八股文之 RunLoop

一、RunLoop是啥

  • 这个字面就是其含义 < loop > 简单说就是个循环;在很多系统和框架中都有这种类似作用的实现,可以称之为 Event loop;
  • 具体到 RunLoop 就是 iOS/macOS 系统中管理线程事件循环的核心机制,它让线程在 “有任务时工作,无任务时休眠”,避免线程空转浪费资源,同时保证事件(如用户交互、网络回调、定时器)能被及时处理。
  • RunLoop 是线程事件处理的 “调度中心”,其核心价值在于 “按需唤醒、隔离事件”。
  • 理解 RunLoop 是解决多线程、性能优化、卡顿监控等问题的关键。

二、RunLoop有啥东西

RunLoop 是包含事件监听、休眠唤醒、状态管理的复杂机制。先来5块钱的(先写5块,想到了再补 ( ̄▽ ̄)~* ):

1. 与线程的关系

  • 每个线程(包括主线程、子线程)都对应唯一一个 RunLoop 对象;线程与 RunLoop 是 “一一对应” 关系,通过全局字典(__CFRunLoopMode 内部维护)关联,线程销毁时 RunLoop 也会随之销毁;
  • 主线程的 RunLoop 由系统自动创建并启动,贯穿 App 生命周期;
  • 子线程的 RunLoop 若不手动启动,会在创建后立即销毁。子线程的 RunLoop 默认不创建,需手动触发(如调用 [NSRunLoop currentRunLoop])时才会初始化;

2. 了解结构:Mode 与 “事件源”

RunLoop 的运行依赖 Mode(模式)事件源(Sources、Timers、Observers)

//结构如下:
CFRunLoop {pthread_t _pthread;       // 关联的线程CFStringRef _currentMode; // 当前运行的ModeCFMutableSetRef _modes;   // 包含的所有Mode
}CFRunLoopMode {CFStringRef _name;        // Mode名称(如kCFRunLoopDefaultMode)CFMutableSetRef _sources0; // 非内核触发的事件源(如UI事件、performSelector:)CFMutableSetRef _sources1; // 内核触发的事件源(如端口通信、Mach消息)CFMutableArrayRef _timers; // 定时器(NSTimer、CADisplayLink)CFMutableArrayRef _observers; // 观察者(监控RunLoop状态变化)
}
  • Mode(关键):RunLoop 在某一时刻只能处于一个 Mode 中,只有当前 Mode 内的事件源才会被处理。目的是 “隔离不同场景的事件”(如滑动时只处理 UI 事件,忽略其他任务)。常用 Mode:
    • kCFRunLoopDefaultMode(默认模式):App 空闲时的默认模式,处理大部分事件(网络回调、定时器等)。
    • UITrackingRunLoopMode(UI 跟踪模式):ScrollView 滑动时进入的模式,优先处理滑动事件,避免卡顿。
    • kCFRunLoopCommonModes(通用模式):不是实际 Mode,是 “模式集合”(默认包含 DefaultMode 和 TrackingMode),事件源添加到这里会在多个 Mode 中生效。
  • 事件源:
    • Sources0:需手动触发的事件(如 UIButton 点击、performSelector:onThread:),处理时需调用 CFRunLoopSourceSignal 标记为待处理,再调用 CFRunLoopWakeUp 唤醒 RunLoop。
    • Sources1:由内核自动触发的事件(如 CFMachPort 端口通信、系统中断),无需手动唤醒。
    • Timers:定时器(NSTimer、CADisplayLink),当到达指定时间时触发,注意:Timer 依赖 RunLoop 运行,若 RunLoop 未启动或处于非当前 Mode,Timer 不会触发。
    • Observers:观察者,监控 RunLoop 状态变化(如进入、退出、处理事件前后),可用于卡顿监控、性能统计。

3. 如何转起来:“休眠 - 唤醒” 的循环

简单说RunLoop 的核心逻辑是:一个 “处理事件→休眠→被唤醒→再处理事件” 的循环

  • 1. 进入循环:调用 CFRunLoopRun() 或 [NSRunLoop run],RunLoop 进入 kCFRunLoopEntry 状态,通知所有 Observers。
  • 2. 准备处理事件:进入 kCFRunLoopBeforeTimers 和 kCFRunLoopBeforeSources 状态,通知 Observers。
  • 3. 处理事件
    • 先处理 Sources0:若有待处理的 Sources0,执行其回调;若回调中触发了 Sources1,会优先处理。
    • 处理 Timers:检查所有 Timer,若到达触发时间,执行回调。
    • 处理 Sources1:若有内核触发的 Sources1,执行回调。
  • 4. 休眠:若没有事件需要处理,RunLoop 进入休眠(通过 mach_msg 调用陷入内核态),释放 CPU 资源。
  • 5. 唤醒:当有新事件(Sources0 被标记、Sources1 触发、Timer 到期、外部线程唤醒)时,内核唤醒 RunLoop,回到步骤 3 处理新事件。
  • 6. 退出循环:当调用 CFRunLoopStop() 或所有事件源失效时,RunLoop 进入 kCFRunLoopExit 状态,通知 Observers 后退出。

4. 与 AutoreleasePool 的关系

RunLoop 是 AutoreleasePool 自动释放的 “幕后推手”:
主线程的 RunLoop 每次进入循环时(kCFRunLoopEntry),会创建一个新的 AutoreleasePool。
在处理完事件即将休眠前(kCFRunLoopBeforeWaiting),会释放当前 Pool 中的对象,并创建新的 Pool。
退出 RunLoop 时(kCFRunLoopExit),最终释放所有 Pool。
这就是 “UI 操作中自动释放的对象会在当前 RunLoop 循环结束后释放” 的原因。

5. 与 GCD 的协作

GCD 的 dispatch_async(dispatch_get_main_queue(), ^{…}) 能在主线程执行,本质是通过 RunLoop 实现:
主线程的 RunLoop 注册了一个 Sources1(基于 Mach 端口 com.apple.main-thread)。
当 GCD 向主队列提交任务时,会通过该端口发送消息,唤醒主线程的 RunLoop,RunLoop 处理完消息后执行提交的 Block。

三、RunLoop 有啥用(应用举例)

1. 解决NSTimer在滑动时失效的问题

场景:NSTimer 默认添加到 kCFRunLoopDefaultMode,当 UIScrollView 滑动时,主线程 RunLoop 切换到 UITrackingRunLoopMode,Timer 会暂停。
实现:将 Timer 添加到 kCFRunLoopCommonModes(在 DefaultMode 和 TrackingMode 中都生效):

//举例代码手撸难免出错,复制需谨慎
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"定时执行");
}];
// 关键:添加到CommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2. 子线程保活(长期执行任务)

场景:子线程默认执行完任务后会立即退出,若需要子线程长期处理事件(如下载回调、消息推送),需让其 RunLoop 保持运行。
实现:给子线程的 RunLoop 添加一个 “永久事件源”(如空的 NSPort),再启动 RunLoop:

//举例代码手撸难免出错,复制需谨慎
@interface RPThreadKeeper : NSObject
@property (nonatomic, strong) NSThread *thread;
@end@implementation RPThreadKeeper
- (instancetype)init {if (self = [super init]) {self.thread = [[NSThread alloc] initWithBlock:^{// 添加一个空的Port作为事件源(避免RunLoop启动后立即退出)NSPort *port = [NSPort port];[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];// 启动RunLoop[[NSRunLoop currentRunLoop] run];}];[self.thread start];}return self;
}// 在保活的子线程执行任务
- (void)performTask:(dispatch_block_t)task {[self performSelector:@selector(executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}- (void)executeTask:(dispatch_block_t)task {if (task) task();
}// 停止RunLoop,释放线程
- (void)stop {[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}- (void)stopRunLoop {[[NSRunLoop currentRunLoop] stop];self.thread = nil;
}
@end

3. 性能优化:滑动时暂停非必要任务

场景:列表滑动时,若同时执行大量计算或图片解码,会导致卡顿。可利用 RunLoop 的 Mode 切换,在滑动时暂停这些任务。
实现:监听 RunLoop 进入 UITrackingRunLoopMode 时暂停任务,退出时恢复:

//举例代码手撸难免出错,复制需谨慎
// 定义一个全局的桥接函数(符合 C 函数指针类型)
static void runLoopObserverBridge(CFRunLoopObserverRef observer,CFRunLoopActivity activity,void *info) {// info 参数中存放 Block,此处转换并调用void (^observerBlock)(CFRunLoopObserverRef, CFRunLoopActivity) = (__bridge void(^)(CFRunLoopObserverRef, CFRunLoopActivity))info;if (observerBlock) {observerBlock(observer, activity);}
}
- (void)setupRPRunLoopObserver {void (^observerBlock)(CFRunLoopObserverRef, CFRunLoopActivity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {NSLog(@"RunLoop 活动: %lu", activity);if (activity == kCFRunLoopEntry) {// 进入滑动模式,暂停任务} else if (activity == kCFRunLoopExit) {// 退出滑动模式,恢复任务}};//CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopEntry | kCFRunLoopExit, // 监控进入和退出状态YES, // 重复执行0, // 优先级runLoopObserverBridge,  // 使用桥接函数作为函数指针(__bridge_retained void *)observerBlock  // 将 Block 作为上下文传递);// 添加到主线程RunLoopCFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);CFRelease(observer);
}

4. 监控应用卡顿(ANR 监控)

原理:主线程卡顿本质是 RunLoop 处理某个事件的耗时过长。通过 Observer 监控 RunLoop 从 kCFRunLoopBeforeSources 到 kCFRunLoopAfterWaiting 的耗时,然后根据业务自定义设置阈值,超过则判定为卡顿。

//举例代码手撸难免出错,复制需谨慎
- (void)setupRPBlockMonitor {static CFAbsoluteTime lastActivityTime;CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {lastActivityTime = CFAbsoluteTimeGetCurrent();if (activity == kCFRunLoopBeforeSources) {// 启动一个子线程定时检查dispatch_async(dispatch_get_global_queue(0, 0), ^{while (YES) {CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent();if (currentTime - lastActivityTime > 0.2) { //暂定超过200ms未响应为卡顿NSLog(@"卡顿!记录调用栈");// 记录主线程调用栈(通过backtrace获取)[self logCallStack];break;}[NSThread sleepForTimeInterval:0.1];}});}},NULL);CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);CFRelease(observer);
}

四、RunLoop在实践中常见问题(持续收集中)

RunLoop 的 “模式隔离” 和 “线程绑定” 特性,容易因使用不当导致隐性问题:

1. 子线程保活导致的内存泄漏

Bug 原因:
用了上面方法让子线程保活时,若未正确停止 RunLoop,线程会一直存活,且线程持有的对象(如工具类、Port)无法释放,会导致内存泄漏,一定看看是不是没stop掉。
解决方案:
check “停止” 接口,调用 CFRunLoopStop(runLoop) 终止 RunLoop,使线程退出。
另外用 __weak 引用避免线程对对象的强持有(如 ThreadKeeper 中线程 Block 不直接引用 self)。

2. RunLoop 嵌套导致的死锁

Bug 原因:
在 RunLoop 的事件回调(如 Timer、Source)中再次调用 [runLoop run] 或 CFRunLoopRun(),会导致 RunLoop 嵌套,新的 RunLoop 永远无法退出(因为外层 RunLoop 被阻塞),最终引发死锁。
解决方案:
这种问题常见于水平一般还玩花活的哥们拉的屎山代码里,赶紧check下是不是在 RunLoop 事件回调中某些条件触发了又启动新的 RunLoop;若需等待任务完成,改用 dispatch_semaphore 或 NSCondition。

3. 主线程任务在非 DefaultMode 中执行导致的卡顿

Bug 原因:
若在 UITrackingRunLoopMode(滑动时)执行耗时任务(如大量计算、同步网络请求),会阻塞滑动事件处理,导致界面卡顿。
解决方案:
简单点就把耗时任务放到子线程执行;但非要在主线程也不是不可以,用 performSelector:withObject:afterDelay:mode: 指定在 kCFRunLoopDefaultMode 执行,避免影响滑动:

//但是吧,建议根据实际业务需要处理,用子线程管理好深浅力度更灵活
[self performSelector:@selector(heavyTask) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

4. Observer 野指针崩溃

Bug 原因:
RunLoop 的 Observer 被释放后,RunLoop 仍可能回调该 Observer(因为 RunLoop 对 Observer 是弱引用),导致野指针崩溃。
解决方案:
当持有 Observer 的对象销毁时,需提前调用 CFRunLoopRemoveObserver 移除 Observer。

ps:总之,大部分bug是RunLoop了解不深时没用好导致,所以建议新手先掌握深浅,再大力用;

五、RunLoop 的一些其他相关(持续补充中)

1. 与 CADisplayLink 的关系

CADisplayLink 是基于屏幕刷新率(60fps多少年了抠搜苹果不给高刷,今年才给)的定时器,其触发依赖 RunLoop:
若 RunLoop 处理事件耗时超过刷新率(举例16.7ms(1/60s)),CADisplayLink 会掉帧,导致动画卡顿。
与 NSTimer 类似,需添加到 NSRunLoopCommonModes 避免滑动时掉帧。

2. 性能影响

过多的事件源(Sources/Timers)或 Observer 会增加 RunLoop 的处理负担,导致每次循环耗时变长,甚至引发卡顿。建议:
移除无用的 Timer 和 Observer。
高频任务(如每秒多次的 Timer)尽量合并或放到子线程。

3. 与后台保活的关系

iOS 后台模式(如音频、定位)能让 App 在后台运行,本质是系统让主线程的 RunLoop 继续运行(即使 App 进入后台),以处理特定事件(如音频回调)。

4. NSRunLoop 和 CFRunLoopRef

OSX/iOS 系统中,关于RunLoop有两个对象:NSRunLoop 和 CFRunLoopRef,得注意区分。

  • CFRunLoopRef 在 CoreFoundation中,它提供了纯C 的 API,它的 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封装,它提供了面向对象的 API,但需注意是它的 API 不是线程安全的。

这么长一坨能看到这已经是很有耐心的了,但是以上只是RunLoop的简述,有些详细的点建议大家多看大佬们的文章,集思广益 ( ̄▽ ̄)~*

http://www.dtcms.com/a/490557.html

相关文章:

  • zibbix
  • Macbook数据恢复 Disk Drill
  • 公司招聘一个网站建设来做推广制作表情包的软件
  • WebSocket实时通信:Socket.io
  • xml方式bean的配置---实例化bean的方式
  • 212. Java 函数式编程风格 - Java 编程风格转换:命令式 vs 函数式(以循环为例)
  • Ubuntu 24.04 修改 ssh 监听端口
  • 1千元以下做网站的公司wordpress sso插件开发
  • Pytorch神经网络工具箱
  • PyTorch DataLoader 高级用法
  • 怎么做一个网站app吗金华网站建设价格
  • 芷江建设局网站石家庄网站建设公司黄页
  • Excel表----VLOOKUP函数实现两表的姓名、身份证号码、银行卡号核对
  • XMLHttpRequest.responseType:前端获取后端数据的一把“格式钥匙”
  • office便捷办公06:根据相似度去掉excel中的重复行
  • Vue+mockjs+Axios 案例实践
  • http的发展历程
  • Python中使用HTTP 206状态码实现大文件下载的完整指南
  • AngularJS下 $http 上传文件
  • 如何弄死一个网站锡林郭勒盟建设工程造价管理网站
  • 【Node.js】为什么擅长处理 I/O 密集型应用?
  • 基于SpringBoot的无人机飞行管理系统
  • STM32的HardFault错误处理技巧
  • Tekever-固定翼无人机系统:模块化垂直起降、远程海上无人机、战术 ISR 无人机
  • Kafka Queue: 如何严格控制消息数量
  • 大兴建设网站wordpress 托管主机
  • 国外html响应式网站网站开发高级证
  • 苍穹外卖--04--Redis 缓存菜品信息、购物车
  • 大淘客网站如何做seowordpress o2o主题
  • 机器学习催化剂设计专题学习