「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);
// 创建主线程 RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 将主线程与RunLoop存入字典:key=主线程, value=RunLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
// 原子操作:将 dict 赋值给全局字典 __CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict); // 若赋值失败,释放临时字典
}
CFRelease(mainLoop); // 释放临时RunLoop(已存入字典,引用计数由字典管理)
__CFLock(&loopsLock); // 重新加锁
}
// 以线程 t 为 key,从全局字典 __CFRunLoops 中查找 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock); // 解锁
if (!loop) { // 若字典中未找到 RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 创建新 RunLoop
__CFLock(&loopsLock); // 加锁
// 双重检查:避免其他线程已创建
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 将新 RunLoop 存入全局字典:key=线程 t, value=newLoop
CFDictionarySetValue(__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), // 即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 Loop
kCFRunLoopAllActivities = 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); #else void * (*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找到对应mode
CFRunLoopModeRef 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
{
// 设置标记为YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(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 wait
timeoutCount = 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观察者
@public
dispatch_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:3
target:self
selector:@selector(updateCPUInfo)
userInfo:nil
repeats: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的CommonModes
CFRunLoopAddObserver(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];
// 主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = image;
[cell setNeedsLayout];
});
}
});
});
}
@end
以上是我模拟SDWebImage的结构大致实现,提供了任务队列,缓存,以及安全线程。
其中创建安全线程的原因是,NSMutableArray
其实不是线程安全的,我们必须保证我们的添加和删除的操作是在同一个线程之中,这个内容其实就是使用了生产者——消费者模式。