「OC」初识runloop

article/2025/6/7 4:57:17

「OC」初识runloop

简介

iOS中的RunLoop(运行循环)是事件处理的核心机制,负责管理线程的生命周期、事件调度及资源优化。其核心作用是通过循环处理输入事件、定时器任务和观察者回调,保持线程活跃且高效运行。

runloop的作用

RunLoop内部运行原理

RunLoop 是 iOS 中的一种机制,来保证你的 app 一直处于可以响应事件的状态,在有事情做的时候随时响应,然后没事做的时候休息,不占用 CPU。类似于使用一个do…while…函数

runloop和线程的关系

RunLoop 是和线程一一对应的,app 启动之后,程序进入了主线程,苹果帮我们在主线程启动了一个 RunLoop。如果是我们开辟的线程,就需要自己手动开启 RunLoop,而且,如果你不主动去获取 RunLoop,那么子线程的 RunLoop 是不会开启的,它是懒加载的形式。而且RunLoop 的销毁发生在线程结束的时候。

在看源码之前我们先了解一个概念:pthread_t NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np()[NSThread mainThread] 来获取主线程

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {if (pthread_equal(t, kNilPthreadT)) {t = pthread_main_thread_np();}__CFLock(&loopsLock);  // 加锁保证线程安全if (!__CFRunLoops) {   // 检查全局字典 __CFRunLoops 是否为空__CFUnlock(&loopsLock);  // 临时解锁,避免死锁// 创建临时字典存储线程与RunLoop映射CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);// 创建主线程 RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());// 将主线程与RunLoop存入字典:key=主线程, value=RunLoopCFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);// 原子操作:将 dict 赋值给全局字典 __CFRunLoopsif (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {CFRelease(dict);  // 若赋值失败,释放临时字典}CFRelease(mainLoop);  // 释放临时RunLoop(已存入字典,引用计数由字典管理)__CFLock(&loopsLock);  // 重新加锁}// 以线程 t 为 key,从全局字典 __CFRunLoops 中查找 RunLoopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFUnlock(&loopsLock);  // 解锁if (!loop) {  // 若字典中未找到 RunLoopCFRunLoopRef newLoop = __CFRunLoopCreate(t);  // 创建新 RunLoop__CFLock(&loopsLock);  // 加锁// 双重检查:避免其他线程已创建loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if (!loop) { // 将新 RunLoop 存入全局字典:key=线程 t, value=newLoopCFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);loop = newLoop;  // 赋值给返回值}__CFUnlock(&loopsLock);  // 解锁CFRelease(newLoop);  // 释放临时对象(字典已持有强引用)}if (pthread_equal(t, pthread_self())) {  // 检查 t 是否为当前线程_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);  // 存储到线程本地数据// 初始化 RunLoop 计数器,管理生命周期if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);}}return loop;  // 返回 RunLoop 对象
}

我们可以看到,我们的RunLoop 与线程的对应关系保存在一个全局的 Dictionary 中

runloop对外的接口

在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

结构大概如下

RunLoop_0

每一个runloop之中包含若干个Mode,每一个Mode包含若干个Source/Obverse/Timer。

Mode

每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。就如同平行世界意义,A Mode发生的事件和B Mode发生的无关。在苹果的架构之中,滚动和默认状态对应着两种不同状态的Mode,所以苹果可以在滚动时专心处理滚动时的事情。

Mode的结构

struct __CFRunLoopMode {CFStringRef _name;           // Mode 名称(如 "kCFRunLoopDefaultMode")CFMutableSetRef _sources0;   // 非基于 Port 的事件源(需手动触发)CFMutableSetRef _sources1;   // 基于 Port 的事件源(自动唤醒 RunLoop)CFMutableArrayRef _timers;   // 定时器事件CFMutableArrayRef _observers;// 状态观察者
};

苹果提供了几种Mode

Mode 名称适用场景特点
NSDefaultRunLoopMode默认模式,处理常规任务(Timer、网络回调等)主线程空闲时默认运行
UITrackingRunLoopMode界面跟踪模式,处理滚动、触摸等交互事件滚动时自动激活,优先保障流畅性
NSRunLoopCommonModes通用模式(非独立模式),包含 Default + Tracking 模式的事件集合解决 Timer 在滚动时失效的问题
UIInitializationRunLoopModeApp 启动初始化阶段使用启动完成后不再生效
GSEventReceiveRunLoopMode系统内部事件处理(如硬件事件)开发者无需主动使用

当我们程序运行而画面静止,他处于kCFRunLoopDefaultMode的状态,如果进行滚动,他就会处于UITrackingRunLoopMode。我们如果想要让定时器在滚动和平时状态都能触发定时器的功能,我们就设置NSRunLoopCommonModes状态,我们在之前的项目之中用过

timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
[loop addTimer:self.timer forMode:NSRunLoopCommonModes];

Observer

观察者,如果说runloop是随叫随到的打工人的话,那么观察者就是观察,runloop的工作状态,什么时候工作休息。苹果公司用一个枚举来列举runloop的工作状态,以达成通过对应状态通知系统的流程。

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),           // 即将进入 LoopkCFRunLoopBeforeTimers = (1UL << 1),    // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2),   // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5),   // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6),    // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7),            // 即将退出 LoopkCFRunLoopAllActivities = 0x0FFFFFFFU   // 所有的状态
};

而Mode之中的timer和source就是runloop需要完成的任务

timer

从刚刚给出的结构图可以看到,存储timer的是一个数组,其实说白了timer就是计时器,将事件和调用的时间间隔全部注册到runloop之中。RunLoop 就会根据你设定的时间点,当时间点到时,去执行这个任务,如果它正在休眠,那么就会先唤醒 RunLoop,再去执行。

其实这个时间间隔也不是完全严格的,因为我们系统在处理任务时候也会有先后顺序,只能说在对应秒数间隔的时候,将需要完成的任务加入任务列表

source

source就是runloop需要完成的另一种任务,source是数据的抽象类,说白了就是一个协议,泛指遵循这个协议的类,我们也可以自定义source(不过一般用不上)。

源码之中定义了两种source

  • Source0:处理 App 内部事件,App 自己负责管理(触发),如 UIEventCFSocket

    typedef struct {CFIndex version;void *  info;const void *(*retain)(const void *info);void    (*release)(const void *info);CFStringRef (*copyDescription)(const void *info);Boolean (*equal)(const void *info1, const void *info2);CFHashCode  (*hash)(const void *info);void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);void    (*perform)(void *info);
    } CFRunLoopSourceContext;

    source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

  • Source1:由 RunLoop 内核管理,Mach port 驱动,如 CFMackPortCFMessagePort

    typedef struct {CFIndex version;void *  info;const void *(*retain)(const void *info);void    (*release)(const void *info);CFStringRef (*copyDescription)(const void *info);Boolean (*equal)(const void *info1, const void *info2);CFHashCode  (*hash)(const void *info);
    #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)mach_port_t (*getPort)(void *info);void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
    #elsevoid *  (*getPort)(void *info);void    (*perform)(void *info);
    #endif
    } CFRunLoopSourceContext1;

    Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。

特性Source0Source1
触发方式手动标记 + 唤醒自动唤醒(通过 Mach Port)
管理方应用层系统内核
事件类型应用内逻辑事件(如 UI 交互)系统事件/跨进程通信
唤醒能力❌ 无法主动唤醒 RunLoop✅ 可主动唤醒 RunLoop
典型代表performSelector:, 触摸事件硬件输入、CADisplayLink

RunLoop 的内部逻辑

我们可以看到源码

/// 用DefaultMode启动
void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {/// 首先根据modeName找到对应modeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);/// 如果mode里没有source/timer/observer, 直接返回。if (__CFRunLoopModeIsEmpty(currentMode)) return;/// 1. 通知 Observers: RunLoop 即将进入 loop。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);/// 内部函数,进入loop__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO;int retVal = 0;do {/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);/// 执行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);/// 4. RunLoop 触发 Source0 (非port) 回调。sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);/// 执行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。if (__Source0DidDispatchPortLastTime) {Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)if (hasMsg) goto handle_msg;}/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。if (!sourceHandledThisLoop) {__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。/// • 一个基于 port 的Source 的事件。/// • 一个 Timer 到时间了/// • RunLoop 自身的超时时间到了/// • 被其他什么调用者手动唤醒__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg}/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);/// 收到消息,处理消息。handle_msg:/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。if (msg_is_timer) {__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())} /// 9.2 如果有dispatch到main_queue的block,执行block。else if (msg_is_dispatch) {__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);} /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);if (sourceHandledThisLoop) {mach_msg(reply, MACH_SEND_MSG, reply);}}/// 执行加入到Loop的block__CFRunLoopDoBlocks(runloop, currentMode);if (sourceHandledThisLoop && stopAfterHandle) {/// 进入loop时参数说处理完事件就返回。retVal = kCFRunLoopRunHandledSource;} else if (timeout) {/// 超出传入参数标记的超时时间了retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(runloop)) {/// 被外部调用者强制停止了retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {/// source/timer/observer一个都没有了retVal = kCFRunLoopRunFinished;}/// 如果没超时,mode里没空,loop也没被停止,那继续loop。} while (retVal == 0);}/// 10. 通知 Observers: RunLoop 即将退出。__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

RunLoop_1

  1. 通知 Observer 已经进入 RunLoop
  2. 通知 Observer 即将处理 Timer
  3. 通知 Observer 即将处理 Source0
  4. 处理 Source0
  5. 如果有 Source1,跳到第 9 步(处理 Source1)
  6. 通知 Observer 即将休眠
  7. 将线程置于休眠状态,直到发生以下事件之一
    • 有 Source0
    • Timer 到时间执行
    • 外部手动唤醒
    • 为 RunLoop 设定的时间超时
  8. 通知 Observer 线程刚被唤醒
  9. 处理待处理事件
    • 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到 2
    • 如果 Source1 触发,处理 Source1
    • 如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到 2
  10. 通知 Observer 即将退出 Loop

实际上 RunLoop 内部就是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,直到超时或手动停止,该函数才会返回。

默认的超时时间是一个巨大的数,可以理解为无穷大,也就是不会超时。

也可以看到,RunLoop 内部的事情也是有一个先后顺序的,当任务很繁重的时候,就可能会出现定时器不准的情况。

之前一直说 do-while,可能会有人担心如果一直是 do-while,那其实线程并没有停止下来,一直在等待。但其实 RunLoop 进入休眠所调用的函数是 mach_msg(),其内部会进行一个系统调用,然后内核会将线程置于等待状态,所以这是一个系统级别的休眠,不用担心 RunLoop 在休眠时会占用 CPU。

RunLoop 的应用

autoreleasePool

我们之前接触过ARC的相关内容,那么被推入到自动释放池的对象,在什么时候被销毁呢?

这个问题其实就和runloop相关了,苹果底层注册了两个Observer,第一个 Observer,监听一个事件,就是 Entry,即将进入 Loop 的时候,创建一个自动释放池,并且给了一个最高的优先级,保证自动释放池的创建发生在其他回调之前,这是为了保证能管理所有的引用计数。

第二个 Observer,监听两个事件,一个 BeforeWaiting,一个 ExitBeforeWaiting 的时候,干两件事,一个释放旧的池,然后创建一个新的池,所以这个时候,自动释放池就会有一次释放的操作,是在 RunLoop 即将进入休眠的时候。Exit 的时候,也释放自动释放池,这里也有一次释放的操作。

触控事件的响应

苹果在内容注册了一个注册了一个 Source1 来监听系统事件。我们在触摸事件流程之中学习到了

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 用 mach port 转发给需要的 App,注册的 Source1 触发回调,回调中将 IOHIDEvent 包装成 UIEvent 进行处理或分发。

刷新界面

当 UI 需要更新,先标记一个 dirty,然后提交到一个全局容器中去。然后,在 BeforeWaitingExit时,会遍历这个容器,执行实际的绘制和调整,并更新 UI 界面。

PerformSelector

当调用 performSelector:afterDelay: 时,其实内部会创建一个定时器,注册到当前线程的 RunLoop 中(如果当前线程没有 RunLoop,这个方法就会失效)。

有时候会看到 afterDelay:0,这样的作用是避免在当前的这个循环中执行,等下一次循环再执行。比方有时候会判断当前的 Mode 是否是 Tracking 或者 Default,为了避免判断错误,会使用 afterDelay:0 的方式将判断延迟到下一次 RunLoop 再执行。

实战演练

线程保活

情况一
- (void)viewDidLoad {[super viewDidLoad];self.thread = [[JCThread alloc] initWithTarget:self selector:@selector(run) object:nil];[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{NSLog(@"1");[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}// 子线程需要执行的任务
- (void)test
{NSLog(@"%s %@", __func__, [NSThread currentThread]);
}- (void)run {NSLog(@"%s %@", __func__, [NSThread currentThread]);NSLog(@"%s ----end----", __func__);
}

image-20250601221304811

我们可以看到我们触发touchBegan方法,但是发现并没有运行run方法,我们会发现我们在viewDidLoad方法之中就将线程销毁了,从我们刚刚学习的内容,我们知道如果Mode里没有任何的Source0/Source1/Timer/Observer, Runloop会立马退出。便引出我们的场景二

场景二

我们在线程启动之前,给他添加source

- (void)run {NSLog(@"%s %@", __func__, [NSThread currentThread]);// 往RunLoop里面添加Source\Timer\Observer[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer:[[NSTimer alloc]init] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop] run];NSLog(@"%s ----end----", __func__);
}

通过在run方法中加入上面代码,让线程一直不死,打印屏幕界面:

img

场景三
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//NSThread使用block的方法,消除循环引用__weak typeof(self) weakSelf = self;self.stopped = NO;self.thread = [[ZXYThread alloc] initWithBlock:^{NSLog(@"%@----begin----", [NSThread currentThread]);// 往RunLoop里面添加Source\Timer\Observer[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];while (weakSelf && !weakSelf.isStoped) {[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];//distantFuture表示无限大的事件}NSLog(@"%@----end----", [NSThread currentThread]);}];[self.thread start];
}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{if (!self.thread) return;[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}// 子线程需要执行的任务
- (void)test
{NSLog(@"%s %@", __func__, [NSThread currentThread]);
}- (void) stop {if (!self.thread) return;// 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}// 用于停止子线程的RunLoop
- (void)stopThread
{// 设置标记为YESself.stopped = YES;// 停止RunLoopCFRunLoopStop(CFRunLoopGetCurrent());NSLog(@"%s %@", __func__, [NSThread currentThread]);// 清空线程self.thread = nil;
}- (void)dealloc
{NSLog(@"%s", __func__);[self stop];
}@end

由于我们第二种方法,将NSPort添加到runloop之中,这样有一个问题,就是我们一旦创建了这个任务线程,runloop就永远不会被回收,那么我们能不能可以控制它的停止和运行呢?我们这里就使用了一个标志stopped来控制,结合循环调用 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];给runloop进行保活操作

情景四

当然情景三已经做的挺好的了,但显然这个内容没有完成封装操作,接下来给出一个分装之后的版本,这里我们的JCThread继承的是NSObject,而把NSThread写在类之中,这样可以有效避免用户自己调用NSTread之中的方法导致出现逻辑上的错误。

#import <Foundation/Foundation.h>
typedef void (^JCThreadTask)(void);
@interface JCPermenantThread : NSObject
/**在当前子线程执行一个任务*/
- (void)executeTask:(JCPermenantThreadTask)task;
/**结束线程*/
- (void)stop;@end#import "ZXYPermenantThread.h"/** ZXYThread **/
@interface JCThread : NSThread
@end
@implementation JCThread
- (void)dealloc{NSLog(@"%s", __func__);
}
@end/** ZXYPermenantThread **/
@interface JCPermenantThread()
@property (strong, nonatomic) JCThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end@implementation ZXYPermenantThread
#pragma mark - public methods
- (instancetype)init{if (self = [super init]) {self.stopped = NO;__weak typeof(self) weakSelf = self;self.innerThread = [[JCThread alloc] initWithBlock:^{[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];while (weakSelf && !weakSelf.isStopped) {[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];}}];[self.innerThread start];}return self;
}- (void)executeTask:(ZXYPermenantThreadTask)task{if (!self.innerThread || !task) return;[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}- (void)stop{if (!self.innerThread) return;[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}- (void)dealloc{NSLog(@"%s", __func__);[self stop];
}#pragma mark - private methods
- (void)__stop{self.stopped = YES;CFRunLoopStop(CFRunLoopGetCurrent());self.innerThread = nil;
}- (void)__executeTask:(JCPermenantThreadTask)task{task();
}@end

性能检测

创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。

一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。接下来,我们就可以 dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

开启一个子线程监控的代码如下:

//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{//子线程开启一个持续的 loop 用来进行监控while (YES) {long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));if (semaphoreWait != 0) {if (!runLoopObserver) {timeoutCount = 0;dispatchSemaphore = 0;runLoopActivity = 0;return;}//BeforeSources 和 AfterWaiting 这两个状态能够检测到是否卡顿if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {//将堆栈信息上报服务器的代码放到这里} //end activity}// end semaphore waittimeoutCount = 0;}// end while
});

为什么BeforeSourcesAfterWaiting 这两个状态能够检测卡顿呢

1. kCFRunLoopBeforeSources
  • 含义:RunLoop 即将处理 Source0 事件(如触摸事件、网络回调、自定义输入源)。
  • 为何关键
    • 此状态标志着主线程开执行用户触发的耗时任务(例如点击事件后的复杂计算、JSON 解析等)。
    • 若在此状态停留过久(如超过 3 秒),说明主线程正在阻塞处理事件,直接导致界面无响应。
2. kCFRunLoopAfterWaiting
  • 含义:RunLoop 刚被唤醒,即将处理 Timer、Source1(系统事件)或 GCD 派发到主线程的任务
  • 为何关键
    • 唤醒后需处理系统级事件(如硬件事件、跨线程通信),或执行 dispatch_async(dispatch_get_main_queue()) 提交的任务。
    • 此阶段耗时过长可能因 GPU 渲染压力、线程锁竞争或 I/O 阻塞引起卡顿。
3. 其他状态
RunLoop 状态行为描述忽略原因
kCFRunLoopEntryRunLoop 刚启动短暂过渡状态,几乎不耗时
kCFRunLoopBeforeTimers即将处理 Timer 事件Timer 回调通常轻量,且系统优化后执行极快
kCFRunLoopBeforeWaitingRunLoop 即将休眠(无任务需处理)空闲状态,线程正常休眠,非卡顿标志
kCFRunLoopExitRunLoop 退出线程结束时的瞬时状态
完整代码

我们用一个单例来完成,我们当app启动时进行创建监听,然后在结束时销毁,这里我直接借用的是Runloop-实际开发你想用的应用场景的代码

#import <Foundation/Foundation.h>// 卡顿监控器接口定义
@interface HCCMonitor : NSObject
+ (instancetype)shareInstance;  // 单例获取方法
- (void)beginMonitor;           // 开始监控(启动CPU和卡顿检测)
- (void)endMonitor;             // 停止监控(释放资源)
@end#import "HCCMonitor.h"
#import "HCCCallStack.h"        // 堆栈捕获工具
#import "HCCCPUMonitor.h"       // CPU监控工具(未直接使用)@interface HCCMonitor() {int timeoutCount;            // 连续超时计数(用于多次卡顿确认)CFRunLoopObserverRef runLoopObserver; // RunLoop观察者@publicdispatch_semaphore_t dispatchSemaphore; // 信号量(卡顿检测同步机制)CFRunLoopActivity runLoopActivity;      // 当前RunLoop状态
}
@property (nonatomic, strong) NSTimer *cpuMonitorTimer; // CPU监控定时器
@end@implementation HCCMonitor#pragma mark - 单例实现
+ (instancetype)shareInstance {static id instance = nil;static dispatch_once_t dispatchOnce;dispatch_once(&dispatchOnce, ^{instance = [[self alloc] init];});return instance;
}#pragma mark - 开始监控
- (void)beginMonitor {// ===================== CPU监控 =====================// 每3秒执行一次CPU检测(通过Mach API获取线程级CPU使用率)self.cpuMonitorTimer = [NSTimer scheduledTimerWithTimeInterval:3target:selfselector:@selector(updateCPUInfo)userInfo:nilrepeats:YES];// ===================== 卡顿监控 =====================if (runLoopObserver) return; // 避免重复创建// 创建初始值为0的信号量(用于同步RunLoop状态变化)dispatchSemaphore = dispatch_semaphore_create(0);// 配置RunLoop观察者上下文(传递self指针用于回调)CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};// 创建观察者(监听RunLoop所有活动状态)runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, // 监听所有状态变化YES,                    // 是否重复观察0,                      // 优先级&runLoopObserverCallBack, // 回调函数&context                // 上下文数据);// 将观察者添加到主线程RunLoop的CommonModesCFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);// ===================== 卡顿检测线程 =====================dispatch_async(dispatch_get_global_queue(0, 0), ^{// 常驻线程持续检测卡顿while (YES) {// 等待信号量(20ms超时阈值,对应60FPS的16.67ms/帧)long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));// 信号量等待超时(主线程未及时响应)if (semaphoreWait != 0) {if (!runLoopObserver) return; // 观察者被释放则退出/* 卡顿判定条件:- kCFRunLoopBeforeSources:处理事件源前(如点击/网络回调)- kCFRunLoopAfterWaiting:唤醒后(如结束休眠)这两个阶段长时间停留表明主线程阻塞[1](@ref)*/if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {// 连续超时判定(示例代码已注释,实际可启用)// if (++timeoutCount < 3) continue; NSLog(@"monitor trigger"); // 卡顿触发日志// 异步捕获堆栈信息(避免阻塞监控线程)dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{// [HCCCallStack callStackWithType:HCCCallStackTypeAll]; // 实际需启用});}}timeoutCount = 0; // 重置超时计数}});
}#pragma mark - 停止监控
- (void)endMonitor {[self.cpuMonitorTimer invalidate]; // 停止CPU定时器if (!runLoopObserver) return;// 移除RunLoop观察者并释放资源CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);CFRelease(runLoopObserver);runLoopObserver = NULL;
}#pragma mark - RunLoop状态回调
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {HCCMonitor *monitor = (__bridge HCCMonitor*)info;monitor->runLoopActivity = activity; // 记录当前RunLoop状态// 发送信号量(通知监控线程状态已更新)dispatch_semaphore_t semaphore = monitor->dispatchSemaphore;dispatch_semaphore_signal(semaphore);
}#pragma mark - CPU监控核心方法
- (void)updateCPUInfo {thread_act_array_t threads;         // 线程数组mach_msg_type_number_t threadCount = 0; // 线程数量// 获取当前任务的所有线程const task_t thisTask = mach_task_self();kern_return_t kr = task_threads(thisTask, &threads, &threadCount);if (kr != KERN_SUCCESS) return;// 遍历所有线程for (int i = 0; i < threadCount; i++) {thread_info_data_t threadInfo;thread_basic_info_t threadBaseInfo;mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;// 获取线程基础信息if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {threadBaseInfo = (thread_basic_info_t)threadInfo;// 过滤空闲线程if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {// CPU使用率换算(原始值/10 = 百分比)integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;// 高负载检测(阈值70%)if (cpuUsage > 70) {// 捕获线程堆栈NSString *reStr = HCCStackOfThread(threads[i]);NSLog(@"CPU overload thread stack:\n%@", reStr);}}}}
}@end

性能优化

我们在使用UITableView的时候,如果需要批量加载比较大的图片,那么对于程序来说,势必会造成卡顿,那其实我们可以根据runloop的周期,在每个runloop周期下载一个图片,我们把下载任务存储在数组之中,每次运行的时候取出一个进行运行

// 图片下载管理器
@interface ImageDownloadManager : NSObject
@property (nonatomic, strong) NSMutableArray *taskQueue; // 任务队列
@property (nonatomic, strong) NSCache *memoryCache;      // 内存缓存
@property (nonatomic, strong) dispatch_queue_t syncQueue; // 线程安全队列
@end@implementation ImageDownloadManager+ (instancetype)shared {static dispatch_once_t onceToken;static ImageDownloadManager *instance;dispatch_once(&onceToken, ^{instance = [[ImageDownloadManager alloc] init];// 初始化队列和缓存instance.taskQueue = [NSMutableArray array];instance.memoryCache = [[NSCache alloc] init];instance.syncQueue = dispatch_queue_create("com.imageDownload.sync", DISPATCH_QUEUE_SERIAL);// 添加RunLoop观察者CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,                         // 1. 内存分配器(通常用默认)kCFRunLoopBeforeWaiting,                     // 2. 监听的状态(即将休眠)YES,                                         // 3. 是否重复观察0,                                           // 4. 优先级(0最高)^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // 5. 回调Block[instance processNextTask];               // 6. 触发任务处理});CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 7. 添加到主线程});return instance;
}// 添加下载任务
- (void)addDownloadTask:(NSURL *)url forIndexPath:(NSIndexPath *)indexPath {dispatch_async(self.syncQueue, ^{// 避免重复添加相同任务for (NSDictionary *task in self.taskQueue) {if ([task[@"url"] isEqual:url]) return;}[self.taskQueue addObject:@{@"url": url,@"indexPath": indexPath}];});
}// 执行下一个任务
- (void)processNextTask {dispatch_async(self.syncQueue, ^{if (self.taskQueue.count == 0) return;NSDictionary *task = self.taskQueue.firstObject;[self.taskQueue removeObjectAtIndex:0];NSURL *url = task[@"url"];NSIndexPath *indexPath = task[@"indexPath"];// 后台线程执行下载dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{NSData *data = [NSData dataWithContentsOfURL:url];UIImage *image = [UIImage imageWithData:data];if (image) {// 缓存图片[self.memoryCache setObject:image forKey:url.absoluteString];// 主线程更新UIdispatch_async(dispatch_get_main_queue(), ^{UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];cell.imageView.image = image;[cell setNeedsLayout];});}});});
}
@end

以上是我模拟SDWebImage的结构大致实现,提供了任务队列,缓存,以及安全线程。

其中创建安全线程的原因是,NSMutableArray其实不是线程安全的,我们必须保证我们的添加和删除的操作是在同一个线程之中,这个内容其实就是使用了生产者——消费者模式。

参考文章

iOS概念攻坚之路(一):RunLoop

RunLoop - 同是天涯打工人

Runloop-实际开发你想用的应用场景


http://www.hkcw.cn/article/PZXnifoOyw.shtml

相关文章

python学习打卡day43

DAY 43 复习日 作业&#xff1a; kaggle找到一个图像数据集&#xff0c;用cnn网络进行训练并且用grad-cam做可视化 浙大疏锦行 数据集使用猫狗数据集&#xff0c;训练集中包含猫图像4000张、狗图像4005张。测试集包含猫图像1012张&#xff0c;狗图像1013张。以下是数据集的下…

【AI学习从零至壹】基于深度学习的⽂本分类任务

基于深度学习的⽂本分类任务 文本分类任务的实现思路⽂本预处理文本分词Jieba分词文本分词器SentencePiece训练步骤⽂本结构化转换 语料转换为训练样本 文本分类任务的实现思路 ⽂本分类就是⼀个将⽂本分配到预定义类别的⼀个过程 整体流程包括&#xff1a; ⽂本语料的获取和…

sourcetree中的mercurial有什么用

1、安装SourceTree的过程中&#xff0c;有一个选项就是mercurial&#xff0c;&#xff0c;一直没搞明白他是干什么用的&#xff0c;直到今天 2、ai登场 3、总结 此软件无用&#xff0c;不需要安装

【Linux】linux基础指令

目录 管理用户相关useraddpaaswduserdelLinux中的用户文件结构 ls-aLinux目录中的.和..是什么&#xff1f; -l-d-FLinux指令使用多个选项 pwdcd绝对路径与相对路径 touchmkdir-p rmdir-p rm-r-i-f mancpmvecho输出重定向和追加重定向 cat-b-n-s moreless-N-i headtail管道文件搭…

Linux中shell介绍

一、脚本实践 脚本示例1 -- 直接编辑并创建一个文件 vim bak.sh-- 写入下面这句话 # 获取ip地址信息 ifconfig ens33 | grep -w inet | awk {print $2} | xargs echo "IP: "-- 运行bak文件 bash bak.sh或者-- 添加可执行权限 chmod ax bak.sh./bak.sh或者source ba…

【智能制造】精读57页智慧工厂MES 项目解决方案【附全文阅读】

本文概述了智慧工厂MES项目解决方案在工业4.0背景下的整体框架与应用。智慧工厂以企业管理运营中心为核心&#xff0c;融合战略绩效、集团管控、决策分析及大数据分析平台&#xff0c;实现C2M&#xff08;Consumer to Manufacturer&#xff09;个性化订单处理。通过信息化系统平…

Stable Diffusion 技术原理解析与代码实践

1. 引言 Stable Diffusion 是由 Stability AI 开发的开源文本到图像生成模型,自 2022 年发布以来在创意产业和研究领域引起了广泛关注。它基于潜在扩散模型架构,能够根据文本描述生成高质量的图像内容,为艺术创作、设计和内容生成提供了强大工具。 2. 技术原理详解 2.1 扩…

Cursor + Claude 4:海外工具网站开发变现实战案例

项目背景 在全球数字化浪潮中&#xff0c;海外工具网站市场蕴含着巨大的商业机会。本文将详细介绍如何使用Cursor编辑器结合Claude 4 AI助手&#xff0c;开发一个面向海外用户的多功能工具网站"ToolBox Pro"&#xff0c;并通过多元化策略实现有效变现。该项目在6个月…

AI 赋能名片设计:告别模板化,创造独特视觉风格

在商务社交与个人品牌传播中&#xff0c;名片是传递信息的重要载体&#xff0c;但千篇一律的模板设计往往让印象大打折扣。智能设计工具的出现&#xff0c;正以智能排版、创意生成和高效迭代的优势&#xff0c;颠覆传统名片设计范式。本文将推荐创客贴、Canva、Fotor 懒设计等多…

MCP:让AI工具协作变得像聊天一样简单 [特殊字符]

想象一下,你正在处理一个项目,需要从A平台查看团队讨论,从B平台获取客户信息,还要在GitHub上检查代码进度。传统做法是什么?打开三个不同的网页,在各个平台间来回切换,复制粘贴数据,最后还可能因为信息分散而遗漏重要细节。 听起来很熟悉?这正是当前工作流程的痛点所…

h5的aliplayer-min.js 加密视频会走到debugger

h5的aliplayer-min.js 如果 https://g.alicdn.com/apsara-media-box/imp-web-player/2.19.0/aliplayer-min.js走加密视频的话会有debugger 更换aliplayer-min.js版本解决了 https://g.alicdn.com/apsara-media-box/imp-web-player/2.25.1/aliplayer-min.js 对应css&#xff1a…

AgenticSeek:您的本地AI智能大脑,免费且私密

还在为昂贵的AI智能体订阅费望而却步吗&#xff1f;还在担心将敏感数据交由第三方处理的隐私风险吗&#xff1f;现在&#xff0c;一款名为AgenticSeek的AI工具横空出世&#xff0c;它承诺提供一个完全免费、100%本地化运行、且功能强大的AI智能体解决方案&#xff0c;旨在成为付…

活动选择问题一文详解

活动选择问题一文详解 一、活动选择问题描述1.1 问题定义1.2 示例说明 二、贪心算法求解策略2.1 贪心思想2.2 策略证明2.3 算法步骤 三、代码实现3.1 Python 实现3.2 C 实现3.3 Java 实现 四、复杂度分析4.1 时间复杂度4.2 空间复杂度 五、应用拓展5.1 资源分配5.2 任务调度优化…

xmake的简易学习

文章目录 1. xmake是什么2. 一个可执行程序3. 一个库文件4. 遍历文件用法5. 第三方库3.1 系统安装库3.2 独立库 6. 后续 由于前一篇博客的最后说要做一些rknn的优化&#xff0c;其实这个工作很早就完成了&#xff0c;但是我是使用 xmake这个来做我的工程的构建的&#xff0c;不…

【网络安全 | 信息收集】灯塔(资产收集工具)安装教程

文章目录 简介安装教程1.创建文件2.执行命令3.运行程序简介 ARL(Asset Reconnaissance Lighthouse)资产侦察灯塔系统,旨在快速侦察与目标关联的互联网资产,构建基础资产信息库。 协助甲方安全团队或者渗透测试人员有效侦察和检索资产,发现存在的薄弱点和攻击面。 其特性如…

TCP小结

1. 核心特性 面向连接&#xff1a;通过三次握手建立连接&#xff0c;四次挥手终止连接&#xff0c;确保通信双方状态同步。 TCP连接建立的3次握手 抓包&#xff1a; client发出连接请求&#xff1b; server回应client请求&#xff0c;并且同步发送syn连接&#xff1b; clien…

Ansys Zemax | 手机镜头设计 - 第 3 部分:使用 STAR 模块和 ZOS-API 进行 STOP 分析

附件下载 联系工作人员获取附件 该系列文章将讨论智能手机镜头模组设计的挑战&#xff0c;从概念、设计到制造和结构变形的分析。本文是四部分系列的第三部分&#xff0c;它涵盖了使用 Ansys Zemax OpticStudio Enterprise 版本提供的 STAR 技术对智能手机镜头进行自动的结构…

【Redis】set 类型

set 一. set 类型介绍二. set 命令sadd、smembers、sismemberscard、spop、srandmembersmove、srem集合间操作交集&#xff1a;sinter、sinterstore并集&#xff1a;sunion、sunionstore差集&#xff1a;sdiff、sdiffstore 三. set 命令小结四. set 内部编码方式五. set 使用场…

006网上订餐系统技术解析:打造高效便捷的餐饮服务平台

网上订餐系统技术解析&#xff1a;打造高效便捷的餐饮服务平台 在数字化生活方式普及的当下&#xff0c;网上订餐系统成为连接餐饮商家与消费者的重要桥梁。该系统以菜品分类、订单管理等模块为核心&#xff0c;通过前台展示与后台录入的分工协作&#xff0c;为管理员和会员提…

Python趣学篇:Pygame重现经典打砖块游戏

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 专栏介绍&#xff1a;《Python星球日记》 目录 一、游戏背景与技术选型1. 打砖块游戏…