「OC」初识runloop
简介
iOS中的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之中包含若干个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 在滚动时失效的问题 |
UIInitializationRunLoopMode | App 启动初始化阶段使用 | 启动完成后不再生效 |
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 自己负责管理(触发),如
UIEvent
、CFSocket
。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 驱动,如
CFMackPort
、CFMessagePort
。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消息)。
特性 | Source0 | Source1 |
---|---|---|
触发方式 | 手动标记 + 唤醒 | 自动唤醒(通过 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);
}
- 通知 Observer 已经进入 RunLoop
- 通知 Observer 即将处理 Timer
- 通知 Observer 即将处理 Source0
- 处理 Source0
- 如果有 Source1,跳到第 9 步(处理 Source1)
- 通知 Observer 即将休眠
- 将线程置于休眠状态,直到发生以下事件之一
- 有 Source0
- Timer 到时间执行
- 外部手动唤醒
- 为 RunLoop 设定的时间超时
- 通知 Observer 线程刚被唤醒
- 处理待处理事件
- 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到 2
- 如果 Source1 触发,处理 Source1
- 如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到 2
- 通知 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
,一个 Exit
,BeforeWaiting
的时候,干两件事,一个释放旧的池,然后创建一个新的池,所以这个时候,自动释放池就会有一次释放的操作,是在 RunLoop 即将进入休眠的时候。Exit
的时候,也释放自动释放池,这里也有一次释放的操作。
触控事件的响应
苹果在内容注册了一个注册了一个 Source1 来监听系统事件。我们在触摸事件流程之中学习到了
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 用 mach port 转发给需要的 App,注册的 Source1 触发回调,回调中将 IOHIDEvent 包装成 UIEvent 进行处理或分发。
刷新界面
当 UI 需要更新,先标记一个 dirty,然后提交到一个全局容器中去。然后,在 BeforeWaiting
和 Exit
时,会遍历这个容器,执行实际的绘制和调整,并更新 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__);
}
我们可以看到我们触发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方法中加入上面代码,让线程一直不死,打印屏幕界面:
场景三
@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
});
为什么BeforeSources
和 AfterWaiting
这两个状态能够检测卡顿呢
1. kCFRunLoopBeforeSources
- 含义:RunLoop 即将处理 Source0 事件(如触摸事件、网络回调、自定义输入源)。
- 为何关键:
- 此状态标志着主线程开执行用户触发的耗时任务(例如点击事件后的复杂计算、JSON 解析等)。
- 若在此状态停留过久(如超过 3 秒),说明主线程正在阻塞处理事件,直接导致界面无响应。
2. kCFRunLoopAfterWaiting
- 含义:RunLoop 刚被唤醒,即将处理 Timer、Source1(系统事件)或 GCD 派发到主线程的任务。
- 为何关键:
- 唤醒后需处理系统级事件(如硬件事件、跨线程通信),或执行
dispatch_async(dispatch_get_main_queue())
提交的任务。 - 此阶段耗时过长可能因 GPU 渲染压力、线程锁竞争或 I/O 阻塞引起卡顿。
- 唤醒后需处理系统级事件(如硬件事件、跨线程通信),或执行
3. 其他状态
RunLoop 状态 | 行为描述 | 忽略原因 |
---|---|---|
kCFRunLoopEntry | RunLoop 刚启动 | 短暂过渡状态,几乎不耗时 |
kCFRunLoopBeforeTimers | 即将处理 Timer 事件 | Timer 回调通常轻量,且系统优化后执行极快 |
kCFRunLoopBeforeWaiting | RunLoop 即将休眠(无任务需处理) | 空闲状态,线程正常休眠,非卡顿标志 |
kCFRunLoopExit | RunLoop 退出 | 线程结束时的瞬时状态 |
完整代码
我们用一个单例来完成,我们当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-实际开发你想用的应用场景