RunLoop ✓¶
面试点¶
概念,数据结构,事件循环机制, RunLoop 与 NSTimer, RunLoop 与多线程
概念¶
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
- 没有消息需要处理时,休眠以避免资源占用。(用户态 -> 内核态)
- 有消息需要处理时,立刻被唤醒。(用户态 <- 内核态)
RunLoop 工作流程
-
等待事件:RunLoop在等待事件时,通常会调用操作系统的内核态函数来挂起当前线程,等待某个事件发生(如输入事件或计时器触发)。此时,线程可能从用户态切换到内核态。
-
事件到达:当有事件到达时(如触摸事件、网络数据到达或计时器到期),操作系统会通过中断机制将控制权从内核态切换到用户态,通知RunLoop有事件需要处理。
-
处理事件:RunLoop从操作系统接收到事件后,会在用户态中执行相应的处理逻辑,例如调用事件处理函数或分发事件到对应的处理对象。
-
继续等待:处理完当前事件后,RunLoop会再次进入等待状态,循环上述过程。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
main函数之所以不会退出是因为执行 main 函数时,调用 UIApplicationMain
函数,函数内部会启动主线程的 RunLoop,而 RunLoop 又是对事件循环的一种维护机制,有事做事,没事做从用户态到内核态的切换,避免资源占用。
数据结构¶
NSRunLoop是CFRunLoop的封装,提供了面向对象的API.
- CFRunLoop
- CFRunLoopMode
- Source/Timer/Observer
1. CFRunLoop¶
- pthread : 一一对应(Runloop 和线程关系)
- currentMode : CFRunLoopMode 结构
- modes : NSMutableSet
- commonModes : NSMutableSet
- commonModeItems : Observer, Timer, Source
2. CFRunLoopMode¶
- name -> NSDefaultRunLoopMode
- sources0
- sources1
- observers
- timers
3. CFRunLoopSource¶
- source0: 需要手动唤起线程
- source1: 具备唤醒线程的能力
4. CFRunLoopTimer¶
基于事件的定时器,和 NSTimer 可切换
5. CFRunLoopObserver¶
观测时间点:
- kCFRunLoopEntry
- kCFRunLoopBeforeTimers
- kCFRunLoopBeforeSources
- kCFRunLoopBeforeWaiting
- kCFRunLoopAftereWaiting
- kCFRunLoopExit
kCFRunLoopEntry:RunLoop即将进入其运行循环。这是RunLoop刚刚开始执行时的状态。
kCFRunLoopBeforeTimers:RunLoop即将在当前循环迭代中处理定时器。这发生在RunLoop即将处理所有定时器之前。
kCFRunLoopBeforeSources:RunLoop即将在当前循环迭代中处理输入源。这发生在RunLoop即将处理所有输入源之前。
kCFRunLoopBeforeWaiting:RunLoop即将进入等待状态,等待输入源或定时器触发。这表示RunLoop即将进入休眠状态,等待事件发生。
kCFRunLoopAfterWaiting:RunLoop刚刚从等待状态中唤醒。这表示RunLoop已经被唤醒,现在将开始处理触发的事件。
kCFRunLoopExit:RunLoop即将退出其运行循环。这表示RunLoop即将结束当前的运行循环。
可以通过添加观察者(Observer)来监听RunLoop的状态变化。以下是一个示例代码,展示如何使用CFRunLoopObserver来监听RunLoop的不同状态:
void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop is entering");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop is about to process timers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop is about to process sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop is about to wait");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop has just been woken up");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop is exiting");
break;
default:
break;
}
}
- (void)startRunLoopObserver {
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallback, &context);
if (observer) {
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
}
结构之间的关系
6. CommonMode¶
NSRunLoopCommonModes
- CommonMode 不是真实存在的一种 Mode。
- 是同步 Source/Timer/Observer 到 多个 Mode 中一种技术方案
CommonModes 是一个伪模式:它不是一个真正的单一模式,而是一个集合,可以包含多个实际的模式。
方便管理输入源和定时器:通过将输入源或定时器添加到 CommonModes,可以确保它们在多个模式下都能正常运行,而无需分别在每个模式中手动添加。
实际模式包含:CommonModes 通常包含 NSDefaultRunLoopMode 和其他模式,如 UI 追踪模式。在不同应用场景下,CommonModes 可以包含不同的模式,确保输入源和定时器在这些模式下都被处理。
事件循环机制¶
RunLoop 与 NSTimer¶
RunLoop 与多线程¶
线程和 Runloop 一一对应
自己创建的线程默认是没有 RunLoop 的,需要手动创建
如何实现一个常驻线程:
- 为当前线程开启一个 RunLoop
- 像该 RunLoop 中添加一个 Port/Source 等维持 RunLoop 的事件循环
- 启动 RunLoop
提问¶
ios 中的 runloop 概念?¶
在 iOS 中,RunLoop 是线程保持活跃并处理事件的核心机制。可以将它理解为一个“消息循环”,它不断地检查是否有需要处理的事件,例如触摸事件、定时器、网络请求或其他 I/O 操作。
主要概念
- 线程与 RunLoop 的关系 每个线程(主线程或子线程)在创建后都有一个与之关联的 RunLoop。特别是主线程的 RunLoop 与 UI 事件密切相关,负责处理用户的交互事件以及更新界面。
- 消息循环机制 RunLoop 持续运行,在内部采用循环机制:检查事件源、处理事件、休眠等待新事件到来。一旦有事件触发,RunLoop 会唤醒并处理这些事件。
- 输入源和定时器 RunLoop 主要管理两类事件源:
- 输入源(Input Sources):如用户触摸、手势、网络输入、Mach Port 等。
- 定时器(Timers):用于周期性地触发某些任务或延迟执行代码。
工作流程
- 启动 当线程启动时,系统会为该线程初始化一个 RunLoop。对于主线程来说,系统会自动调用 RunLoop 开始循环运行。
- 等待与处理 RunLoop 进入循环状态,等待各种事件的发生。当事件到达时,RunLoop 会依次取出并分发给相应的处理方法,处理完成后继续等待下一个事件。
- 休眠 如果没有事件需要处理,RunLoop 会进入休眠状态,从而减少 CPU 的消耗。一旦有新的事件,RunLoop 会被唤醒进行处理。
运行模式
RunLoop 支持多种运行模式(RunLoop Modes),常见的有:
- NSDefaultRunLoopMode:默认模式,用于处理常规事件。
- UITrackingRunLoopMode:专门用于 UI 交互过程中,如滑动、拖拽时使用,避免与其他模式的干扰。
- NSRunLoopCommonModes:一个集合,包含了多个常用的模式。添加到这个模式中的事件源会在所有这些模式下触发。
通过不同的运行模式,开发者可以更精细地控制在特定情景下 RunLoop 的行为,例如在滚动 UI 时确保流畅的响应而不被其他定时器或任务打断。
总结
- 保持线程活跃:RunLoop 确保线程不会在等待事件时提前退出,同时又能在没有任务时休眠,降低能耗。
- 事件分发中心:它是 iOS 应用中分发各种用户和系统事件的核心机制,保证了应用能够及时响应各种操作。
- 多模式管理:通过设置不同的运行模式,可以根据不同的需求调整事件处理的优先级和行为,提升应用性能和用户体验。
理解 iOS 的 RunLoop 机制有助于深入把握应用的事件处理和线程管理,在面试中讨论相关的性能优化和多线程问题时,也能更自信地回答问题。
ios 应用启动会有多少个 runloop?¶
在 iOS 应用启动过程中,系统会自动在主线程上创建并启动一个 RunLoop,这个 RunLoop 负责处理 UI 事件和各种输入源。简单来说:
- 主线程 RunLoop:默认情况下,应用启动后只有主线程的 RunLoop 被自动创建,它负责处理所有用户交互、定时器、网络回调等事件。
- 其他线程:如果你手动创建其他线程,并且在这些线程中调用了
[[NSRunLoop currentRunLoop] run]
或类似方法,那么每个线程可以有自己的 RunLoop。但这些线程的 RunLoop并非自动创建,而是需要你明确设置。
所以,通常我们说的 iOS 应用启动时,主要是有一个 RunLoop(即主线程的 RunLoop)。其他的 RunLoop只有在你创建额外线程时才会出现。
一个 runloop 可以有多少个 mode¶
一个 RunLoop 支持多个 Mode,从理论上来说,数量上没有严格的上限。你可以为 RunLoop 添加自定义的模式,而不仅仅局限于系统预定义的几个常用模式。实际应用中,大多数场景只会用到几个模式,例如:
- NSDefaultRunLoopMode:默认模式,处理常规事件。
- UITrackingRunLoopMode:用于处理 UI 跟踪事件,如滚动和拖拽。
- NSRunLoopCommonModes:一个集合模式,通常包含多个常用模式,便于同时处理一些任务。
虽然理论上可以添加任意多个模式,但通常实际使用时不会超过十个,因为设计上主要是为了解决不同情境下的事件处理问题,而不是用来存放大量模式。
如何手动创建一个 runloop¶
在 iOS 中,你无法直接用 alloc/init 的方式创建一个 RunLoop,因为每个线程都隐式地拥有一个 RunLoop。你只能通过以下方式“获取”并“启动”当前线程的 RunLoop:
-
获取当前线程的 RunLoop 调用
[NSRunLoop currentRunLoop]
就能获取当前线程关联的 RunLoop。如果当前线程还没有 RunLoop,这个方法会自动创建一个。 -
启动 RunLoop 如果你希望这个 RunLoop 开始运行,就需要调用它的
run
或其他相关方法。通常在子线程中会这样写:
```objc - (void)myThreadMain { // 获取当前线程的 RunLoop NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 为了保证 RunLoop 不会立即退出,你需要添加至少一个输入源或计时器
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 让 RunLoop 进入循环
[runLoop run];
} ```
这样,当你创建一个新的线程并在其主函数中调用 myThreadMain
,当前线程就会启动它的 RunLoop,从而等待事件、定时器或其他输入源的触发。
总结:
- 每个线程都有一个隐式的 RunLoop,不能直接创建。
- 通过
[NSRunLoop currentRunLoop]
获取当前线程的 RunLoop。 - 通过添加输入源(例如 Port、Timer 等)来保证 RunLoop 持续运行。
- 调用
run
方法启动 RunLoop 的循环。
runloop 的结构是啥?¶
在 iOS 中,RunLoop 的内部结构主要体现在 Core Foundation 层的 CFRunLoop 上,它实际上是一个事件处理循环,负责不断检查和分发各种事件。其核心结构可以概括为以下几个部分:
- RunLoop Modes(运行模式) RunLoop 按照不同的使用场景将事件分为多个模式。常见的模式有:
- NSDefaultRunLoopMode:默认模式,处理大部分普通事件。
- UITrackingRunLoopMode:用于处理 UI 交互过程中(如滚动)的事件,保证响应流畅。
- NSRunLoopCommonModes:一个集合,可以将多个模式归纳到一起,方便同时处理一些事件。
- 输入源(Input Sources) 输入源是 RunLoop 用来接收外部事件的主要途径,包括:
- 非端口输入源(Non-port based sources):例如自定义事件,这类输入源需要程序主动触发。
- 端口输入源(Port-based sources):例如 Mach Port、Socket 等系统级别的通信机制,当有数据或信号到达时会唤醒 RunLoop。
- 定时器(Timers) 定时器(例如 NSTimer)也被添加到 RunLoop 中,用于在指定时间间隔后触发某个任务。定时器会在对应的模式下被调度执行。
- 观察者(Observers) 观察者可以监听 RunLoop 的各个状态变化(例如即将进入休眠、刚刚被唤醒、即将退出循环等),从而让开发者有机会在这些时刻执行自定义代码。这在性能调试和特殊任务调度中非常有用。
工作机制
- 运行循环过程: RunLoop 在当前模式下不断循环,先检查是否有输入源、定时器或其他事件需要处理。如果有,就处理相应的事件;如果没有,RunLoop 就会调用系统的阻塞等待函数(例如 mach_msg 或 select),使线程进入休眠状态以节省资源。
- 状态切换: 当事件到来时,内核通过中断等机制唤醒线程,切换回用户态后,RunLoop 根据当前模式取出待处理事件并执行相应的回调,然后继续循环。
总结来说,iOS 中的 RunLoop 其实是一个集合了多种事件源(输入源、定时器、观察者等)的循环机制,通过不同的运行模式对事件进行分类管理,并依赖底层系统调用来实现高效的阻塞和唤醒。开发者通常不需要直接操作 RunLoop 内部的细节,而是通过 NSRunLoop 等高级 API 进行管理和配置。
source0 和 source1 啥区别,defaultmode 处理什么事件,trackingmode 处理什么事件¶
你的理解大致正确,下面详细解释一下:
-
RunLoop 中的 Source0 和 Source1 的区别
-
Source0
- 也称为非端口型输入源(non-port-based input source)。
- 这类输入源不依赖于内核提供的端口或其他系统通信机制。
- 需要应用程序主动调用信号(如调用 CFRunLoopSourceSignal)并手动唤醒 RunLoop。
- 通常用于自定义事件或内部状态改变,这种消息需要在合适的时候手动通知 RunLoop 去处理。
- Source1
- 也称为端口型输入源(port-based input source)。
- 这类输入源是基于内核提供的端口机制(例如 Mach port)实现的。
- 当有外部事件到来(比如来自系统的消息、网络数据等),内核会自动向这些端口发送信号,RunLoop 会收到通知并处理相应事件。
-
常用于系统级的事件通信,处理方式比较自动化。
-
RunLoop Mode 中 Default Mode 与 Tracking Mode
-
NSDefaultRunLoopMode(默认模式)
- 这是 RunLoop 的默认运行模式,主要用于处理常规事件,如定时器、非交互相关的输入源等。
- 在大多数情况下,如果没有用户交互的特殊需求,事件都在默认模式下被处理。
- UITrackingRunLoopMode(跟踪模式)
- 这个模式专门用于用户交互过程中,例如滚动或拖拽操作。
- 当用户开始滚动列表时,RunLoop 会自动切换到 UITrackingRunLoopMode,此时只会处理与 UI 跟踪相关的输入事件,避免其他任务(如默认模式下的定时器)干扰滚动的流畅性。
-
如果希望定时器在滚动时依然触发,可以将它添加到 NSRunLoopCommonModes 中,因为 NSRunLoopCommonModes 是一个包含了默认模式和 UITrackingRunLoopMode的集合。
-
模式之间的隔离性
-
相互隔离
- 在任一时刻,RunLoop 只处理当前模式下注册的事件、定时器和观察者。
- 这就意味着:如果一个定时器只添加到了 NSDefaultRunLoopMode,那么当 RunLoop 切换到 UITrackingRunLoopMode(例如在用户滚动时),该定时器就不会被触发,因为当前模式中不包含它。
-
这种机制实现了不同场景下的任务隔离,确保例如滚动时的触摸响应不会因其他定时任务而受到干扰。
-
模式切换的触发
-
自动模式切换
- 系统会根据用户的交互行为自动切换 RunLoop 的模式。
- 例如,当用户开始滚动列表时,UIKit 会让主线程 RunLoop 切换到 UITrackingRunLoopMode;当滚动结束后,再切回到 NSDefaultRunLoopMode。
- 这种切换主要是由系统内部的事件处理机制自动触发的,而不是由开发者直接调用系统接口实现。
总结一下:
- Source0 和 Source1:
- Source0 是非端口型输入源,需要应用主动信号;
- Source1 是基于端口的输入源,由内核自动触发。
- RunLoop Mode:
- 默认模式(NSDefaultRunLoopMode)用于处理常规事件;
- 跟踪模式(UITrackingRunLoopMode)专门用于用户交互(如滚动)时的事件;
- 模式之间是相互隔离的,同一时刻只处理当前模式下注册的事件,而模式切换则主要由系统根据用户操作自动进行。
主要用于处理常规事件,如定时器、非交互相关的输入源等, 再具体点么,啥是常规事件
什么是 RunLoop 中的“常规事件”?(NSDefaultRunLoopMode 处理的事件)¶
在 iOS 中,NSDefaultRunLoopMode 是 RunLoop 的默认模式,负责处理不涉及用户交互的普通事件。这里的“常规事件”主要包括:
-
定时器(Timers)
-
例如
NSTimer
,当你创建一个NSTimer
并添加到NSDefaultRunLoopMode
,它会在主线程的 RunLoop 运行时按照设定的时间间隔触发。 -
示例
:
objc NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
-
问题:如果用户滑动
UIScrollView
,RunLoop 会切换到UITrackingRunLoopMode
,此时NSTimer
会暂停,等到滚动结束后才会继续运行。 -
解决方法
:如果希望滑动时
NSTimer
也继续运行,就要把它添加到
NSRunLoopCommonModes
:
objc [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
输入源(Input Sources)
-
Source0(非端口输入源)
:
-
例如手动触发的
performSelector:onThread:
任务,或者某些手动唤醒的事件处理。 -
示例
:
objc [self performSelector:@selector(doTask) withObject:nil afterDelay:2.0];
-
-
Source1(端口输入源)
:
- 例如
CFMachPort
、CFSocket
这些基于系统端口(如 Mach 消息、网络事件)的事件。
- 例如
-
手势识别(Gesture Recognizers)
-
NSDefaultRunLoopMode
处理普通的手势事件(不包括 UI 追踪,比如滚动)。 -
例如,轻点(tap)、长按(long press)等手势的回调函数会在
NSDefaultRunLoopMode
下执行。 -
UI 事件(UI Updates)
-
大部分 UI 事件,如
UIView
的drawRect:
重绘、按钮点击(UIButton
click)、普通UIView
动画(非CADisplayLink
相关的)。 -
但 UIScrollView 滑动不属于这个 Mode,而是
UITrackingRunLoopMode
。 -
dispatch_main()
处理的任务 -
dispatch_async(dispatch_get_main_queue(), ^{ ... });
提交的任务也会被NSDefaultRunLoopMode
处理。 - 但如果 RunLoop 进入
UITrackingRunLoopMode
(比如用户正在滚动列表),这些任务会等到滚动结束后才执行。
UITrackingRunLoopMode
处理的事件
当 RunLoop 切换到 UITrackingRunLoopMode,主要用于处理用户的触摸交互,例如:
- UIScrollView 的滚动事件(拖拽、惯性滚动)
- 滑动手势(Pan Gesture)
- 滚动过程中的 UI 更新(比如
CADisplayLink
动画)
在这个模式下,普通的 NSTimer 事件不会被触发,因为它们默认是在 NSDefaultRunLoopMode
里,而 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
互斥。
总结
✅ NSDefaultRunLoopMode(常规模式)
处理 非用户交互 相关的任务,包括:
- NSTimer
- 普通 UI 事件(如 UIButton 点击、UIView 重新绘制)
- performSelector:afterDelay: 之类的延迟任务
- 网络请求回调
- 主线程
dispatch_async(dispatch_get_main_queue(), ^{})
任务 - 手势识别(如 Tap、Long Press)
- 普通的
UIView
动画
✅ UITrackingRunLoopMode(追踪模式)
处理 滚动和交互 相关的任务,包括:
- UIScrollView 滚动
- 滑动手势
- 惯性滚动
- 滚动过程中的 UI 刷新
所以,如果你发现一个 NSTimer
在滚动时暂停,那就是因为它只在 NSDefaultRunLoopMode
里运行,而 UITrackingRunLoopMode
处于活动状态。要解决这个问题,就要用 NSRunLoopCommonModes
。
runloop 检测卡顿 能具体知道 哪段代码吗¶
🔍 如何使用 RunLoop 检测卡顿并精准定位代码? 目标:不仅检测到卡顿,还能精确获取导致卡顿的代码调用栈。
⏳ 方案:结合 RunLoop
监听 + 信号量阻塞检测 + 主线程堆栈抓取
核心思路:
1. 通过 RunLoop
观察者 (CFRunLoopObserver
) 监听主线程运行状态。
2. 在 BeforeWaiting
和 AfterWaiting
之间,设置一个子线程等待信号量:
- 如果 RunLoop
执行完毕,会及时 signal
,子线程不会超时。
- 如果 RunLoop
长时间未执行 signal
(超过 200ms),说明主线程被阻塞,判定为卡顿。
3. 一旦检测到卡顿,就获取主线程的调用堆栈,帮助我们找到具体的代码位置。
📌 代码实现
#import <mach/mach.h>
#import <pthread.h>
@interface LagMonitor : NSObject
@property (nonatomic, assign) CFRunLoopObserverRef observer;
@property (nonatomic, assign) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) CFRunLoopActivity activity;
@end
@implementation LagMonitor
+ (instancetype)sharedInstance {
static LagMonitor *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[LagMonitor alloc] init];
});
return instance;
}
- (void)startMonitoring {
if (self.observer) return;
// 1. 创建信号量,初始值为 0
self.semaphore = dispatch_semaphore_create(0);
// 2. 创建 RunLoop 观察者
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
self.observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES, // 是否重复
0, // 优先级
&runLoopObserverCallback,
&context);
// 3. 添加到主线程 RunLoop
CFRunLoopAddObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);
// 4. 在子线程中监听主线程是否被阻塞
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
while (YES) {
// 等待信号量 200ms,如果超时,说明 RunLoop 被阻塞
long timeout = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC));
if (timeout != 0) { // 超时,表示主线程被卡住了
NSLog(@"⚠️ 发生卡顿!");
[self logMainThreadStack]; // 采样主线程调用堆栈
}
}
});
}
// 5. RunLoop 观察回调
static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
LagMonitor *monitor = (__bridge LagMonitor *)info;
monitor.activity = activity;
// 释放信号量,表示 RunLoop 继续执行
dispatch_semaphore_signal(monitor.semaphore);
}
// 6. 获取主线程的调用堆栈
- (void)logMainThreadStack {
thread_act_array_t threads;
mach_msg_type_number_t thread_count = 0;
const task_t this_task = mach_task_self();
// 获取所有线程
if (task_threads(this_task, &threads, &thread_count) != KERN_SUCCESS) {
NSLog(@"❌ 获取线程失败");
return;
}
// 遍历所有线程,找到主线程
for (int i = 0; i < thread_count; i++) {
thread_act_t thread = threads[i];
mach_msg_type_number_t state_count = THREAD_STATE_COUNT;
thread_state_data_t thread_state;
if (thread_get_state(thread, THREAD_STATE, (thread_state_t)thread_state, &state_count) != KERN_SUCCESS) {
continue;
}
// 找到主线程
if (pthread_main_np() == thread) {
NSLog(@"🔍 捕获到主线程卡顿堆栈:\n%@", [NSThread callStackSymbols]);
break;
}
}
// 释放线程数组
vm_deallocate(this_task, (vm_address_t)threads, thread_count * sizeof(thread_t));
}
@end
📌 代码解析
| 步骤 | 作用 |
|------|------|
| 1. 创建 CFRunLoopObserver
| 监听 RunLoop
运行状态 |
| 2. 创建信号量 | 在子线程等待 RunLoop
完成 |
| 3. RunLoop
正常执行时释放信号量 | 让子线程继续执行 |
| 4. 如果 200ms 内没有信号量 | 说明 RunLoop
被阻塞(卡顿) |
| 5. 获取主线程调用堆栈 | 找到导致卡顿的代码 |
🎯 关键问题解答 1️⃣ 这个方案能准确定位卡顿代码吗? ✅ 是的! - 这个方案直接 获取主线程的调用栈,能够精确看到哪段代码执行时间过长,导致主线程卡顿。
2️⃣ 这个方案对性能影响大吗?
✅ 影响极小
- RunLoop
监听是 轻量级操作,不会影响 UI 线程。
- 只在 卡顿时才会获取主线程堆栈,避免频繁操作。
3️⃣ 适用于哪些场景?
✅ 适用于 Debug 和线上监控
- Debug 阶段:帮助开发者精准定位卡顿代码。
- 线上监控:
- 可以结合 log
上传卡顿堆栈,分析用户体验问题。
📌 结论
1. RunLoop 监听:检测主线程卡顿(阻塞 RunLoop
)。
2. 信号量检测:如果主线程 200ms 没有 signal
,判定卡顿。
3. 获取调用栈:精确找到导致卡顿的代码位置。
🚀 这样,你就可以精准定位 iOS 应用中的卡顿问题了! 🎯