「OC」初识runloop

「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);
        // 创建主线程 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_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),           // 即将进入 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 自己负责管理(触发),如 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);
    #else
        void *  (*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找到对应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);
}

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
{
    // 设置标记为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
});

为什么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观察者
    @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其实不是线程安全的,我们必须保证我们的添加和删除的操作是在同一个线程之中,这个内容其实就是使用了生产者——消费者模式。

参考文章

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

RunLoop - 同是天涯打工人

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值