iOS开发基础39-RunLoop

RunLoop 深度解析:运行循环、Mode、Source/Timer/Observer 与应用场景

本文系统梳理 iOS RunLoop 机制:RunLoop 的作用与本质、与线程的一一对应关系、Core Foundation 五大相关类(CFRunLoopRef/Mode/Source/Timer/Observer)、RunLoop 处理流程(休眠与唤醒)、以及 NSTimer 模式问题、常驻线程、自动释放池等典型应用场景。


一、什么是 RunLoop

1. 定义

RunLoop(运行循环)是线程中的一个 do-while 循环,核心作用:

  • 保持程序持续运行:App 启动后不退出,持续等待用户事件。
  • 处理各种事件:触摸事件、定时器事件、Selector 事件、端口消息等。
  • 节省 CPU 资源:有事处理事,没事休眠,不占用 CPU 轮询。

2. 没有 RunLoop 的程序

int main(int argc, char *argv[]) {
    NSLog(@"execute main function");
    return 0; // 执行完立即退出
}

没有 RunLoop,main 函数执行完后程序立即退出。

3. 有 RunLoop 的程序

iOS App 的 UIApplicationMain 内部启动了主线程的 RunLoop,程序保持运行状态,等待事件。

int main(int argc, char *argv[]) {
    @autoreleasepool {
        // UIApplicationMain 内部启动主线程 RunLoop,不会返回(直到 App 退出)
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain 在启动 RunLoop 后不会返回,直到 App 终止。可以通过在 main 函数中 UIApplicationMain 后加打印验证——该打印永远不会执行。

4. RunLoop 本质(模拟实现)

// RunLoop 本质就是一个 do-while 循环
int main(int argc, const char *argv[]) {
    do {
        // 1. 处理事件(Source/Timer/Observer)
        // 2. 没有事件时休眠(mach_msg 等待消息)
        // 3. 被消息唤醒后继续处理
    } while (1);
    return 0;
}

RunLoop 不是简单的空循环(空循环会占用 100% CPU),而是通过 mach_msg 进入内核态休眠,等待消息唤醒,无事时几乎不消耗 CPU。


二、RunLoop 对象与线程

1. 两套 API

框架 类型 说明
Foundation NSRunLoop Objective-C 封装,面向对象
Core Foundation CFRunLoopRef C 语言 API,开源,可研究底层

NSRunLoop 基于 CFRunLoopRef 封装,研究底层原理需看 CFRunLoopRef。

2. RunLoop 与线程的关系

  • 一一对应:每条线程有且仅有一个 RunLoop 对象,存储在全局字典中(key 为线程指针,value 为 RunLoop)。
  • 主线程:RunLoop 已自动创建并启动(UIApplicationMain 中)。
  • 子线程:RunLoop 默认未创建,第一次获取时创建,线程结束时销毁。
  • 子线程 RunLoop 必须手动启动:创建后需调用 run 或 runMode:beforeDate: 才会运行,且必须至少添加一个 Source 或 Timer,否则 RunLoop 会立即退出。

3. 获取 RunLoop 对象

// Foundation
NSRunLoop *current = [NSRunLoop currentRunLoop]; // 当前线程的 RunLoop
NSRunLoop *main = [NSRunLoop mainRunLoop];        // 主线程的 RunLoop

// Core Foundation
CFRunLoopRef cfCurrent = CFRunLoopGetCurrent();
CFRunLoopRef cfMain = CFRunLoopGetMain();

在子线程中调用 [NSRunLoop currentRunLoop] 会触发 RunLoop 的创建。


三、RunLoop 相关类(Core Foundation)

RunLoop 涉及 5 个核心类:

类 说明
CFRunLoopRef RunLoop 对象本身
CFRunLoopModeRef 运行模式
CFRunLoopSourceRef 事件源(输入源)
CFRunLoopTimerRef 定时源
CFRunLoopObserverRef 观察者

1. CFRunLoopModeRef(运行模式)

一个 RunLoop 包含多个 Mode,每个 Mode 包含若干 Source、Timer、Observer。每次 RunLoop 运行只能指定一个 Mode(CurrentMode),切换 Mode 需退出当前 Loop 再重新指定。

Mode 说明
kCFRunLoopDefaultMode(NSDefaultRunLoopMode) App 默认模式,处理大部分事件
UITrackingRunLoopMode 界面追踪模式,UIScrollView 滚动时切换到此模式,保证滑动流畅
UIInitializationRunLoopMode App 启动初始化时的模式,启动完成后不再使用
GSEventReceiveRunLoopMode 系统事件接收模式,内部使用
kCFRunLoopCommonModes(NSRunLoopCommonModes) 模式集合,不是真正的模式,包含 Default 和 Tracking 等;添加到 CommonModes 的事件在 Default 和 Tracking 下都能执行

NSRunLoopCommonModes 是一个模式集合(结构体),将 Timer/Source 添加到 CommonModes 时,系统会同步添加到集合中的所有模式。

2. CFRunLoopSourceRef(事件源)

类型 说明
Source0 非基于 Port,需要手动触发(CFRunLoopSourceSignal + CFRunLoopWakeUp 唤醒 RunLoop)。如 UIEvent 触摸事件、performSelector 等
Source1 基于 Port(mach_port),由内核或其他线程主动唤醒。如系统事件分发、CFMachPort 消息等

Source0 处理完后,RunLoop 会检查是否有 Source1,如果有则跳转到 Source1 处理流程。

3. CFRunLoopTimerRef(定时源)

基于时间的触发器,NSTimer 本质就是对 CFRunLoopTimerRef 的封装。Timer 只能在添加时指定的 Mode 下运行,Mode 不匹配时不会触发。

4. CFRunLoopObserverRef(观察者)

监听 RunLoop 的状态变化,可监听以下活动(CFRunLoopActivity):

活动 说明
kCFRunLoopEntry 即将进入 RunLoop
kCFRunLoopBeforeTimers 即将处理 Timer
kCFRunLoopBeforeSources 即将处理 Source
kCFRunLoopBeforeWaiting 即将进入休眠
kCFRunLoopAfterWaiting 刚从休眠唤醒
kCFRunLoopExit 即将退出 RunLoop
kCFRunLoopAllActivities 所有活动
// 创建 Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
    kCFAllocatorDefault,
    kCFRunLoopAllActivities,
    YES,                       // repeats: 是否重复
    0,                         // order: 优先级,0 为默认
    ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:         NSLog(@"进入 Loop"); break;
            case kCFRunLoopBeforeTimers:  NSLog(@"即将处理 Timer"); break;
            case kCFRunLoopBeforeSources: NSLog(@"即将处理 Source"); break;
            case kCFRunLoopBeforeWaiting: NSLog(@"即将休眠"); break;
            case kCFRunLoopAfterWaiting:  NSLog(@"刚唤醒"); break;
            case kCFRunLoopExit:          NSLog(@"退出 Loop"); break;
            default: break;
        }
    }
);

// 添加 Observer 到主线程 RunLoop 的默认模式
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
CFRelease(observer); // Core Foundation 对象需手动释放

CFRunLoopObserverCreateWithHandler 创建的对象需用 CFRelease 释放(Create 规则)。


四、RunLoop 处理逻辑

RunLoop 每次循环的精确流程:

1. 通知 Observer:kCFRunLoopEntry(进入 Loop)
        ↓
2. 通知 Observer:kCFRunLoopBeforeTimers(即将处理 Timer)
        ↓
3. 通知 Observer:kCFRunLoopBeforeSources(即将处理 Source)
        ↓
4. 处理 Source0(非 Port 事件,如触摸、performSelector)
        ↓
5. 如果有 Source1(Port 事件),跳转到第 9 步处理
        ↓
6. 通知 Observer:kCFRunLoopBeforeWaiting(即将休眠)
        ↓
7. 休眠:调用 mach_msg 进入内核态,等待消息唤醒
   (Timer 到期 / Source1 事件 / 手动唤醒 / 超时)
        ↓
8. 通知 Observer:kCFRunLoopAfterWaiting(刚唤醒)
        ↓
9. 处理唤醒事件:
   - Timer 到期 → 处理 Timer
   - Source1 → 处理 Source1
   - 手动唤醒 → 无操作
        ↓
10. 回到第 2 步(继续循环)
        ↓(RunLoop 退出时)
11. 通知 Observer:kCFRunLoopExit(退出 Loop)

休眠通过 mach_msg 系统调用实现,线程进入内核态等待消息,几乎不消耗 CPU。被唤醒后回到用户态继续处理事件。


五、RunLoop 应用场景

1. NSTimer 的 Mode 问题(UIScrollView 滚动时不执行)

NSTimer 必须添加到 RunLoop 才能执行,且只在指定 Mode 下触发。

问题:用 scheduledTimerWithTimeInterval: 创建的 Timer 默认添加到 NSDefaultRunLoopMode。当 UIScrollView 滚动时,RunLoop 切换到 UITrackingRunLoopMode,Default 模式下的 Timer 不会触发,表现为滚动时定时器暂停。

解决方案:将 Timer 添加到 NSRunLoopCommonModes(模式集合,滚动时也能执行)。

// 方式一:手动创建并添加到 CommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 方式二:scheduled 方法(默认添加到 DefaultMode,滚动时不执行)
NSTimer *scheduledTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];

如果 Timer 任务不要求滚动时执行,用 DefaultMode 即可(滚动时暂停,保证滑动流畅)。如果要求滚动时也执行,用 CommonModes。

2. GCD 定时器(不依赖 RunLoop)

GCD 定时器(dispatch_source_t)基于 GCD 队列执行,不依赖 RunLoop,比 NSTimer 更精准,在子线程也可运行:

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 设置定时器:起始时间、间隔、误差(0 为最精准)
dispatch_source_set_timer(timer,
                          dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),
                          2 * NSEC_PER_SEC,
                          0);

dispatch_source_set_event_handler(timer, ^{
    NSLog(@"GCD 定时器触发");
});

dispatch_resume(timer); // GCD 源默认挂起,必须 resume 才开始

GCD 定时器创建后默认挂起,必须调用 dispatch_resume 才开始执行。取消用 dispatch_cancel。

3. 常驻线程(子线程保持存活)

子线程默认执行完任务后即销毁。如需子线程长期存活(常驻线程),需启动 RunLoop 并添加至少一个 Source 或 Timer:

@interface PermanentThread : NSObject
@property (nonatomic, strong) NSThread *thread;
@end

@implementation PermanentThread

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
        [self.thread start];
    }
    return self;
}

- (void)threadMain {
    @autoreleasepool {
        // 获取 RunLoop(第一次获取时创建)
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        // 必须添加至少一个 Source 或 Timer,否则 RunLoop 立即退出
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        
        // 启动 RunLoop(阻塞当前线程,保持存活)
        [runLoop run];
    }
}

// 在常驻线程执行任务
- (void)performTaskInPermanentThread {
    [self performSelector:@selector(doWork) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)doWork {
    NSLog(@"常驻线程执行任务: %@", [NSThread currentThread]);
}

@end

常驻线程的 RunLoop 必须添加 Source/Timer,否则 run 会立即返回,线程结束。添加 NSMachPort 是常见的保持方式(不实际使用,仅为了让 RunLoop 不退出)。

4. 自动释放池(AutoreleasePool)与 RunLoop

主线程的自动释放池由 RunLoop 的 Observer 管理:

  • 进入 RunLoop(kCFRunLoopEntry):Observer 创建第一个 autoreleasepool。
  • 即将休眠(kCFRunLoopBeforeWaiting):Observer 释放旧的 autoreleasepool,创建新的。
  • 刚唤醒(kCFRunLoopAfterWaiting):不操作 pool(新 pool 已在休眠前创建)。
  • 退出 RunLoop(kCFRunLoopExit):Observer 释放最后一个 autoreleasepool。

这就是为什么主线程不需要手动管理 autoreleasepool——RunLoop 在每次循环休眠前自动释放旧 pool 并创建新 pool,防止内存峰值过高。子线程没有自动创建的 RunLoop,需手动管理 autoreleasepool(如 @autoreleasepool {})。

5. 性能优化:监听 RunLoop 卡顿

通过 Observer 监听主线程 RunLoop 的 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 的耗时,如果超过阈值(如 16ms),说明主线程卡顿,可调用栈信息定位卡顿原因。这是第三方卡顿检测库(如 SMCallStack)的底层原理。


六、Swift 版本对照

获取 RunLoop

let current = RunLoop.current       // 当前线程 RunLoop
let main = RunLoop.main            // 主线程 RunLoop
let cfCurrent = CFRunLoopGetCurrent()
let cfMain = CFRunLoopGetMain()

NSTimer 添加到 CommonModes

let timer = Timer(timeInterval: 2.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .common) // .common 即 NSRunLoopCommonModes

GCD 定时器

let queue = DispatchQueue.global()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 2, repeating: 2.0)
timer.setEventHandler {
    print("GCD 定时器触发")
}
timer.resume() // 必须 resume

常驻线程

class PermanentThread {
    private var thread: Thread?
    
    init() {
        thread = Thread(target: self, selector: #selector(threadMain), object: nil)
        thread?.start()
    }
    
    @objc private func threadMain() {
        autoreleasepool {
            let runLoop = RunLoop.current
            runLoop.add(NSMachPort(), forMode: .default)
            runLoop.run()
        }
    }
    
    func performTask() {
        perform(#selector(doWork), on: thread!, with: nil, waitUntilDone: false)
    }
    
    @objc private func doWork() {
        print("常驻线程: \(Thread.current)")
    }
}

CFRunLoopObserver

let observer = CFRunLoopObserverCreateWithHandler(
    kCFAllocatorDefault,
    CFRunLoopActivity.allActivities.rawValue,
    true,
    0
) { _, activity in
    switch activity {
    case .entry: print("进入 Loop")
    case .beforeTimers: print("即将处理 Timer")
    case .beforeSources: print("即将处理 Source")
    case .beforeWaiting: print("即将休眠")
    case .afterWaiting: print("刚唤醒")
    case .exit: print("退出 Loop")
    default: break
    }
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .defaultMode)

七、总结

  • RunLoop 本质:线程中的 do-while 循环,通过 mach_msg 休眠等待消息,有事处理、无事休眠,保持线程存活并节省 CPU。
  • 与线程关系:一一对应,存储在全局字典;主线程自动创建启动,子线程第一次获取时创建,需手动启动且必须添加 Source/Timer。
  • 五大类:CFRunLoopRef(RunLoop 本身)、CFRunLoopModeRef(模式,每次只能运行一个)、CFRunLoopSourceRef(Source0 手动触发 / Source1 内核唤醒)、CFRunLoopTimerRef(定时源)、CFRunLoopObserverRef(观察者,监听 6 种活动)。
  • Mode 机制:Default(默认)、Tracking(滚动)、CommonModes(模式集合);Timer/Source 只在指定 Mode 执行,UIScrollView 滚动时切换到 Tracking 导致 Default 下 Timer 暂停,解决方案是添加到 CommonModes。
  • 处理流程:Entry → BeforeTimers → BeforeSources → 处理 Source0 → BeforeWaiting → 休眠(mach_msg)→ AfterWaiting → 处理唤醒事件 → 循环;退出时通知 Exit。
  • 应用场景:NSTimer Mode 问题与解决方案、GCD 定时器(不依赖 RunLoop,更精准)、常驻线程(启动 RunLoop + 添加 Source/Timer)、自动释放池(RunLoop Observer 管理,休眠前释放旧 pool 创建新 pool)、卡顿检测(Observer 监听耗时)。
  • 两套 API:Foundation NSRunLoop(面向对象)和 Core Foundation CFRunLoopRef(C 语言,开源),NSRunLoop 基于 CFRunLoopRef 封装。

posted @ 2015-08-21 20:27  Mr.陳  阅读(607)  评论(0)    收藏  举报