Skip to content

RunLoop ✓

面试点

概念,数据结构,事件循环机制, RunLoop 与 NSTimer, RunLoop 与多线程

概念

RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。

  • 没有消息需要处理时,休眠以避免资源占用。(用户态 -> 内核态)
  • 有消息需要处理时,立刻被唤醒。(用户态 <- 内核态)

RunLoop 工作流程

  1. 等待事件:RunLoop在等待事件时,通常会调用操作系统的内核态函数来挂起当前线程,等待某个事件发生(如输入事件或计时器触发)。此时,线程可能从用户态切换到内核态。

  2. 事件到达:当有事件到达时(如触摸事件、网络数据到达或计时器到期),操作系统会通过中断机制将控制权从内核态切换到用户态,通知RunLoop有事件需要处理。

  3. 处理事件:RunLoop从操作系统接收到事件后,会在用户态中执行相应的处理逻辑,例如调用事件处理函数或分发事件到对应的处理对象。

  4. 继续等待:处理完当前事件后,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 的,需要手动创建

如何实现一个常驻线程:

  1. 为当前线程开启一个 RunLoop
  2. 像该 RunLoop 中添加一个 Port/Source 等维持 RunLoop 的事件循环
  3. 启动 RunLoop

提问

ios 中的 runloop 概念?

在 iOS 中,RunLoop 是线程保持活跃并处理事件的核心机制。可以将它理解为一个“消息循环”,它不断地检查是否有需要处理的事件,例如触摸事件、定时器、网络请求或其他 I/O 操作。

主要概念

  • 线程与 RunLoop 的关系 每个线程(主线程或子线程)在创建后都有一个与之关联的 RunLoop。特别是主线程的 RunLoop 与 UI 事件密切相关,负责处理用户的交互事件以及更新界面。
  • 消息循环机制 RunLoop 持续运行,在内部采用循环机制:检查事件源、处理事件、休眠等待新事件到来。一旦有事件触发,RunLoop 会唤醒并处理这些事件。
  • 输入源和定时器 RunLoop 主要管理两类事件源:
  • 输入源(Input Sources):如用户触摸、手势、网络输入、Mach Port 等。
  • 定时器(Timers):用于周期性地触发某些任务或延迟执行代码。

工作流程

  1. 启动 当线程启动时,系统会为该线程初始化一个 RunLoop。对于主线程来说,系统会自动调用 RunLoop 开始循环运行。
  2. 等待与处理 RunLoop 进入循环状态,等待各种事件的发生。当事件到达时,RunLoop 会依次取出并分发给相应的处理方法,处理完成后继续等待下一个事件。
  3. 休眠 如果没有事件需要处理,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:

  1. 获取当前线程的 RunLoop 调用 [NSRunLoop currentRunLoop] 就能获取当前线程关联的 RunLoop。如果当前线程还没有 RunLoop,这个方法会自动创建一个。

  2. 启动 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 上,它实际上是一个事件处理循环,负责不断检查和分发各种事件。其核心结构可以概括为以下几个部分:

  1. RunLoop Modes(运行模式) RunLoop 按照不同的使用场景将事件分为多个模式。常见的模式有:
  2. NSDefaultRunLoopMode:默认模式,处理大部分普通事件。
  3. UITrackingRunLoopMode:用于处理 UI 交互过程中(如滚动)的事件,保证响应流畅。
  4. NSRunLoopCommonModes:一个集合,可以将多个模式归纳到一起,方便同时处理一些事件。
  5. 输入源(Input Sources) 输入源是 RunLoop 用来接收外部事件的主要途径,包括:
  6. 非端口输入源(Non-port based sources):例如自定义事件,这类输入源需要程序主动触发。
  7. 端口输入源(Port-based sources):例如 Mach Port、Socket 等系统级别的通信机制,当有数据或信号到达时会唤醒 RunLoop。
  8. 定时器(Timers) 定时器(例如 NSTimer)也被添加到 RunLoop 中,用于在指定时间间隔后触发某个任务。定时器会在对应的模式下被调度执行。
  9. 观察者(Observers) 观察者可以监听 RunLoop 的各个状态变化(例如即将进入休眠、刚刚被唤醒、即将退出循环等),从而让开发者有机会在这些时刻执行自定义代码。这在性能调试和特殊任务调度中非常有用。

工作机制

  • 运行循环过程: RunLoop 在当前模式下不断循环,先检查是否有输入源、定时器或其他事件需要处理。如果有,就处理相应的事件;如果没有,RunLoop 就会调用系统的阻塞等待函数(例如 mach_msg 或 select),使线程进入休眠状态以节省资源。
  • 状态切换: 当事件到来时,内核通过中断等机制唤醒线程,切换回用户态后,RunLoop 根据当前模式取出待处理事件并执行相应的回调,然后继续循环。

总结来说,iOS 中的 RunLoop 其实是一个集合了多种事件源(输入源、定时器、观察者等)的循环机制,通过不同的运行模式对事件进行分类管理,并依赖底层系统调用来实现高效的阻塞和唤醒。开发者通常不需要直接操作 RunLoop 内部的细节,而是通过 NSRunLoop 等高级 API 进行管理和配置。

source0 和 source1 啥区别,defaultmode 处理什么事件,trackingmode 处理什么事件

你的理解大致正确,下面详细解释一下:

  1. RunLoop 中的 Source0 和 Source1 的区别

  2. Source0

  3. 也称为非端口型输入源(non-port-based input source)。
  4. 这类输入源不依赖于内核提供的端口或其他系统通信机制。
  5. 需要应用程序主动调用信号(如调用 CFRunLoopSourceSignal)并手动唤醒 RunLoop。
  6. 通常用于自定义事件或内部状态改变,这种消息需要在合适的时候手动通知 RunLoop 去处理。
  7. Source1
  8. 也称为端口型输入源(port-based input source)。
  9. 这类输入源是基于内核提供的端口机制(例如 Mach port)实现的。
  10. 当有外部事件到来(比如来自系统的消息、网络数据等),内核会自动向这些端口发送信号,RunLoop 会收到通知并处理相应事件。
  11. 常用于系统级的事件通信,处理方式比较自动化。

  12. RunLoop Mode 中 Default Mode 与 Tracking Mode

  13. NSDefaultRunLoopMode(默认模式)

  14. 这是 RunLoop 的默认运行模式,主要用于处理常规事件,如定时器、非交互相关的输入源等。
  15. 在大多数情况下,如果没有用户交互的特殊需求,事件都在默认模式下被处理。
  16. UITrackingRunLoopMode(跟踪模式)
  17. 这个模式专门用于用户交互过程中,例如滚动或拖拽操作。
  18. 当用户开始滚动列表时,RunLoop 会自动切换到 UITrackingRunLoopMode,此时只会处理与 UI 跟踪相关的输入事件,避免其他任务(如默认模式下的定时器)干扰滚动的流畅性。
  19. 如果希望定时器在滚动时依然触发,可以将它添加到 NSRunLoopCommonModes 中,因为 NSRunLoopCommonModes 是一个包含了默认模式和 UITrackingRunLoopMode的集合。

  20. 模式之间的隔离性

  21. 相互隔离

  22. 在任一时刻,RunLoop 只处理当前模式下注册的事件、定时器和观察者。
  23. 这就意味着:如果一个定时器只添加到了 NSDefaultRunLoopMode,那么当 RunLoop 切换到 UITrackingRunLoopMode(例如在用户滚动时),该定时器就不会被触发,因为当前模式中不包含它。
  24. 这种机制实现了不同场景下的任务隔离,确保例如滚动时的触摸响应不会因其他定时任务而受到干扰。

  25. 模式切换的触发

  26. 自动模式切换

  27. 系统会根据用户的交互行为自动切换 RunLoop 的模式。
  28. 例如,当用户开始滚动列表时,UIKit 会让主线程 RunLoop 切换到 UITrackingRunLoopMode;当滚动结束后,再切回到 NSDefaultRunLoopMode。
  29. 这种切换主要是由系统内部的事件处理机制自动触发的,而不是由开发者直接调用系统接口实现。

总结一下:

  • Source0 和 Source1
  • Source0 是非端口型输入源,需要应用主动信号;
  • Source1 是基于端口的输入源,由内核自动触发。
  • RunLoop Mode
  • 默认模式(NSDefaultRunLoopMode)用于处理常规事件;
  • 跟踪模式(UITrackingRunLoopMode)专门用于用户交互(如滚动)时的事件;
  • 模式之间是相互隔离的,同一时刻只处理当前模式下注册的事件,而模式切换则主要由系统根据用户操作自动进行。

主要用于处理常规事件,如定时器、非交互相关的输入源等, 再具体点么,啥是常规事件

什么是 RunLoop 中的“常规事件”?(NSDefaultRunLoopMode 处理的事件)

在 iOS 中,NSDefaultRunLoopMode 是 RunLoop 的默认模式,负责处理不涉及用户交互的普通事件。这里的“常规事件”主要包括:

  1. 定时器(Timers)

  2. 例如 NSTimer,当你创建一个 NSTimer 并添加到 NSDefaultRunLoopMode,它会在主线程的 RunLoop 运行时按照设定的时间间隔触发。

  3. 示例

    objc NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];

  4. 问题:如果用户滑动 UIScrollView,RunLoop 会切换到 UITrackingRunLoopMode,此时 NSTimer 会暂停,等到滚动结束后才会继续运行。

  5. 解决方法

    :如果希望滑动时

    NSTimer

    也继续运行,就要把它添加到

    NSRunLoopCommonModes

    objc [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  6. 输入源(Input Sources)

  7. Source0(非端口输入源)

    • 例如手动触发的 performSelector:onThread: 任务,或者某些手动唤醒的事件处理。

    • 示例

    objc [self performSelector:@selector(doTask) withObject:nil afterDelay:2.0];

  8. Source1(端口输入源)

    • 例如 CFMachPortCFSocket 这些基于系统端口(如 Mach 消息、网络事件)的事件。
  9. 手势识别(Gesture Recognizers)

  10. NSDefaultRunLoopMode 处理普通的手势事件(不包括 UI 追踪,比如滚动)。

  11. 例如,轻点(tap)、长按(long press)等手势的回调函数会在 NSDefaultRunLoopMode 下执行。

  12. UI 事件(UI Updates)

  13. 大部分 UI 事件,如 UIViewdrawRect: 重绘、按钮点击(UIButton click)、普通 UIView 动画(非 CADisplayLink 相关的)。

  14. UIScrollView 滑动不属于这个 Mode,而是 UITrackingRunLoopMode

  15. dispatch_main() 处理的任务

  16. dispatch_async(dispatch_get_main_queue(), ^{ ... }); 提交的任务也会被 NSDefaultRunLoopMode 处理。

  17. 但如果 RunLoop 进入 UITrackingRunLoopMode(比如用户正在滚动列表),这些任务会等到滚动结束后才执行。

UITrackingRunLoopMode 处理的事件

当 RunLoop 切换到 UITrackingRunLoopMode,主要用于处理用户的触摸交互,例如:

  1. UIScrollView 的滚动事件(拖拽、惯性滚动)
  2. 滑动手势(Pan Gesture)
  3. 滚动过程中的 UI 更新(比如 CADisplayLink 动画)

在这个模式下,普通的 NSTimer 事件不会被触发,因为它们默认是在 NSDefaultRunLoopMode 里,而 NSDefaultRunLoopModeUITrackingRunLoopMode 互斥。

总结

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. BeforeWaitingAfterWaiting 之间,设置一个子线程等待信号量: - 如果 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 应用中的卡顿问题了! 🎯