Runloop

【iOS】Runloop

文章目录

一、RunLoop简介

运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能

二、RunLoop基本作用

  1. 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就是保证了程序的持续运行
  2. 处理App的各种事件,比如:触摸事件,定时器事件,Selector事件等
  3. 节省CPU性能,提高性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CPU,现在没有事情做,我要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情

通过图片可以看出,RunLoop在跑圈过程中,当接收到Input sources或者Timer sources时就会交给对应的处理方处理,如果没有事件的话就会休眠

图片的解释

  1. **Port:**这里的Port可以理解为一种:线程之间传递事件/消息的通道,然后Thread调用handlePort:处理
  2. **Custom:**自定义一种事件来源,开发者可以自己设置发生某个事件,然后就唤醒RunLoop
  3. **performSelector:**让某个线程执行某个方法,比如主线程performSelector:onThread:子线程,然后子线程就执行mySelector:
  4. **Timer:**时间事件,也就是我们常用的定时器,然后线程执行timeFired

三、RunLoop的结构体

通过源码我们找到_CFRunLoop的结构体

objc 复制代码
struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

在这里我们主要看两个成员变量:CFRunLoopModeRef _currentMode;,CFMutableSetRef _modes;

``CFRunLoopModeRef _currentMode其实就是指向CFRunLoopMode`的一个指针,CFRunLoopMode源码查询如下

objc 复制代码
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

主要查看这四个成员变量

objc 复制代码
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

它们都是包含当前Mode下所有的Source1/Source0/Timers/Observer

Source1/Source0/Timers/Observer

  1. Source0:基于Port的线程间通信,是系统底层事件,可以直接唤醒RunLoop
  2. source0是非基于Port的输入源 ,一般包括触摸事件,PerformSelectors

但其实触摸事件是source1这个说法是不准确的,引用触摸事件的底层流程

当我们触发了事件(触摸/锁屏/摇晃等)后,由IOKit.framework生成一个 IOHIDEvent事件,而IOKit是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,并专门处理用户交互设备,由IOHIDServices和IOHIDDisplays两部分组成,其中IOHIDServices是专门处理用户交互的,它会将事件封装成IOHIDEvents对象,接着用mach port转发给需要的App进程,随后 Source1就会接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()内触发Source0,Source0 再触发 _UIApplicationHandleEventQueue()。所以触摸事件看到是在 Source0 内

因此应该这么总结:触摸事件的底层 Mach/Port 消息首先通过 Source1 进入 RunLoop;Source1 的回调把事件进一步封装/转交给 UIKit 的事件处理队列,随后由 Source0 触发 UIKit 对事件的实际处理。

  1. **Obeserver:**负责观察RunLoop的状态变化
  2. **Timer:**一个定时器,当达到fireDate,RunLoop处理Timer

四、详解RunLoop相关类及作用

我们目前对Runloop的内部结构有了大致了解,接下来详细分析RunLoop的相关类

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含Mode,每个Mode又包含Source、Timer、Observer 但每次RunLoop启动时只能指定其中一个Mode,也就是currentMode,要是想切换Mode,需要退出RunLoop,再重新制定一个Mode进入 主要是为了分隔各个Mode里不同的Source、Timer、Observer

注意:一种Mode里可以有多种Source、Timer、Observer,但至少有一个Source或者Timer,因为如果Mode为空,RunLoop就会立即退出

系统默认注册的五个Mode
Mode 作用
kCFRunLoopDefaultMode 默认模式,平时运行
UITrackingRunLoopMode 用户拖动、滚动 UIScrollView
UIInitializationRunLoopMode App 启动初始化阶段
GSEventReceiveRunLoopMode 接收系统事件相关
kCFRunLoopCommonModes 不是 Mode,而是 Mode 集合,一般包含tracking和default
Mode间的切换

我们在无限轮播图中就遇到过,一拖动scrollview,无限轮播图的定时器就停止了,但一停止滑动定时器就会重新恢复,这其实就涉及到了Mode切换的问题

objc 复制代码
@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSInteger count;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
    self.count = 0;
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.backgroundColor = UIColor.systemGrayColor;
    [self.view addSubview:scrollView];
    scrollView.frame = CGRectMake(20, 150,self.view.bounds.size.width - 40,300);
    scrollView.contentSize = CGSizeMake(scrollView.bounds.size.width, 1000);

}
- (void)timerAction:(NSTimer *)timer {
    self.count++;
    NSLog(@"Timer执行:%ld", self.count);
}

因为如果我们在主线程使用定时器,此时RunLoop的Mode为kCFRunLoopDefaultMode,即定时器属于kCFRunLoopDefaultMode,那么此时我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop的Mode切换回kCFRunLoopDefaultMode,所以NSTimer就又管用了。

那么如何timer在两个模式中都能运行呢?

在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer

使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签 0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"} ,2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"} , 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

objc 复制代码
	CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, 										kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        CFStringRef mode = CFRunLoopCopyCurrentMode(CFRunLoopGetMain());
        NSLog(@"Mode = %@ Activity = %lu",mode,activity);
        if (mode) {
            CFRelease(mode);
        }
    });
	CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
	CFRelease(observer);
创建监听者
objc 复制代码
	CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, 										kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity){
    ...
  }

第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配

第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态

第三个参数 Boolean repeats:YES:持续监听 NO:不持续

第四个参数 CFIndex order:优先级,一般填0即可

第五个参数 :回调 两个参数observer:监听者 activity:监听的事件:

Activity 常量 十进制(%lu) 十六进制 含义
kCFRunLoopEntry 1 0x1 即将进入 RunLoop
kCFRunLoopBeforeTimers 2 0x2 即将处理 Timer
kCFRunLoopBeforeSources 4 0x4 即将处理 Source(Port 事件)
kCFRunLoopBeforeWaiting 32 0x20 即将进入休眠(等待事件)
kCFRunLoopAfterWaiting 64 0x40 刚从休眠中唤醒
kCFRunLoopExit 128 0x80 即将退出 RunLoop
kCFRunLoopAllActivities 0x0FFFFFFF 0x0FFFFFFF 所有活动的组合(位掩码)
给RunLoop添加监听者
objc 复制代码
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);

第一个参数CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop

第二个参数CFRunLoopObserverRefobserver 监听者

第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态

将上面那个demo加入监听者后发现

objc 复制代码
Mode = UITrackingRunLoopMode Activity = 2
Mode = UITrackingRunLoopMode Activity = 4
Mode = UITrackingRunLoopMode Activity = 32
Mode = UITrackingRunLoopMode Activity = 64
Mode = UITrackingRunLoopMode Activity = 2
Mode = UITrackingRunLoopMode Activity = 4
Mode = UITrackingRunLoopMode Activity = 2
Mode = UITrackingRunLoopMode Activity = 4
Mode = UITrackingRunLoopMode Activity = 128
Mode = kCFRunLoopDefaultMode Activity = 1
Mode = kCFRunLoopDefaultMode Activity = 2
Mode = kCFRunLoopDefaultMode Activity = 4
Mode = kCFRunLoopDefaultMode Activity = 32
Mode = kCFRunLoopDefaultMode Activity = 64
Mode = kCFRunLoopDefaultMode Activity = 2
Mode = kCFRunLoopDefaultMode Activity = 4
Mode = kCFRunLoopDefaultMode Activity = 32
Mode = kCFRunLoopDefaultMode Activity = 64
Timer执行:5

发现确实是Runloop切换了timer就开始执行了

CF内存管理

ARC主要管理Objective-C对象,不会自动管理所有Core Foundation对象

最重要的判断方法:当创建对象的函数带有Create,Copy,Retain等字眼,创建出来的对象都需要在最后做一次release

五、RunLoop流程

源码解析

objc 复制代码
// 共外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    // 通知Observers : 进入Loop
    // __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函数
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 核心的Loop逻辑
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知Observers : 退出Loop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

// 精简后的 __CFRunLoopRun函数,保留了主要代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 通知Observers:即将处理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 
        
        // 通知Observers:即将处理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 处理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        // 如果有Sources1,就跳转到handle_msg标记处
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            goto handle_msg;
        }
        
        // 通知Observers:即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        
        // 进入休眠,等待其他消息唤醒
        __CFRunLoopSetSleeping(rl);
        __CFPortSetInsert(dispatchPort, waitSet);
        do {
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        
        // 醒来
        __CFPortSetRemove(dispatchPort, waitSet);
        __CFRunLoopUnsetSleeping(rl);
        
        // 通知Observers:已经唤醒
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
        if (被Timer唤醒的) {
            // 处理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
        else if (被GCD唤醒的) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { // 被Sources1唤醒的
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }
        
        // 执行Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        
    } while (0 == retVal);
    
    return retVal;
}

六、常驻线程

我们知道,当子线程中的任务执行完毕就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中始终存在,怎么才能让它不被销毁呢?这时就要给子线程开启一个RunLoop

objc 复制代码
//
//  ViewController.m
//  1
//
//  Created by MacBook Air on 2026/9/6.
//

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic, strong)NSThread* thread;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if (self.thread && !self.thread.isFinished) {
        NSLog(@"子线程已经存在");
        return;
    }
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(show) object:nil];
    [self.thread start];
    NSLog(@"子线程已经启动");
}
-(void)show{
    NSLog(@"show - 当前线程: %@", [NSThread currentThread]);
    NSLog(@"show - RunLoop开始创建");
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
    //添加一个Port保证RunLoop有Source不退出
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
    [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
    CFRunLoopObserverRef obeserver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要处理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要处理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒来了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), obeserver, kCFRunLoopDefaultMode);
    NSLog(@"show -RunLoop开始运行");
    [runLoop run];
    NSLog(@"show -RunLoop已退出");
    CFRelease(obeserver);
}
-(void)test {
    NSLog(@"test - 当前线程: %@", [NSThread currentThread]);
}
-(IBAction)btnClick:(id)sender {
    NSLog(@"按钮点击 - 当前线程:%@", [NSThread currentThread]);
    if (!self.thread || self.thread.isFinished) {
        NSLog(@"子线程还没启动");
        return;
    }
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
@end
相关推荐
秋雨梧桐叶落莳1 小时前
【iOS】GCD内容整理
macos·ios·cocoa
demo007x16 小时前
让大模型活在你的鼠标旁:我用 Tauri 2 + Rust 打造了一款“反直觉”的 AI 全局划词效率神器
macos·程序员·llm
那年窗外下的雪.1 天前
AIDC 学习日志|第 24 天|设备输出反推与 MAC Flapping 定位
网络协议·学习·tcp/ip·http·macos·tcpdump
RobinDevNotes1 天前
Palmier Pro:AI时代的Mac视频编辑器
人工智能·ceph·macos·ai·音视频·视频编辑·mcp
johnsong2 天前
AI前沿日报 2026-09-08
人工智能·macos
33三 三like2 天前
基于 MAC-RUNGUIDE 的完整启动流程
macos
用户2181697049303 天前
iOS 底层原理 runloop
objective-c
tedcloud1233 天前
Wand-Enhancer:如何搭建一套远程开发与测试环境
前端·人工智能·macos·开源·流程图
2501_915909063 天前
SwiftUI 和 UIKit 怎么选?两代 UI 框架的适用范围
vscode·ios·objective-c·个人开发·swift·敏捷流程