在编程过程中,了解和掌握NSRunLoop的工作原理至关重要,尤其是在处理多线程和事件驱动的应用场景时。本文旨在全面解析NSRunLoop,帮助读者消除学习中的盲点。
NSRunLoop概述
NSRunLoop是一种事件处理循环,负责接收和分发事件到相应的处理器。其基本工作流程类似于一个无限循环,具体如下:
main() { initialize(); do { message = get_next_message(); process_message(message); } while (message != quit); }
在这个过程中,NSRunLoop会等待消息(休眠),接收到消息后进行处理。NSRunLoop通过高度抽象的方式封装了这一过程,使得开发者在日常开发中几乎感觉不到其存在。
NSRunLoop可以理解为一个函数,其核心功能是消息循环。有消息时进行处理,无消息时进入休眠状态。
简单示例:创建一个新线程,添加一个定时器,并启动NSRunLoop。
- (void)timerFire { NSLog(@"mode:%@", [[NSRunLoop currentRunLoop] currentMode]); } - (void)runLoopTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(timerFire) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }); }
如果你有嵌入式操作系统开发经验,可能会对以下代码感到熟悉:
void ledTask (void *p_arg) { initialize(); while (1) { LED_ON(); delay_ms(500); LED_OFF(); delay_ms(500); } }
这段代码实现了一个LED闪烁线程,功能简单:初始化后进入无限循环,通过延时函数使线程进入休眠状态以节省CPU资源。NSRunLoop的工作原理与此类似。
消息类型(事件源)
NSRunLoop的消息类型非常多样,主要包括:
- Port(端口):监听程序的Mach端口,Mach端口是一种底层通信机制,用于内核与进程之间的通信。
- Custom(自定义):由开发者自定义的消息源,通常涉及复杂的处理逻辑,苹果提供了CFRunLoopSource来辅助处理。
- Selector Sources(选择器源):通过NSObject类的方法添加到NSRunLoop中,例如performSelector系列方法。
- Timer Sources(定时器源):事件的发送是同步的,常用于定时任务。
- Observers(观察者):用于观察NSRunLoop的状态变化,如进入、退出、处理定时器等。
其中,Port、Custom和Selector Sources都是异步执行的。
运行模式
NSRunLoop的运行模式使其更加灵活,适应不同的应用场景。常见的模式包括:
- kCFRunLoopDefaultMode:默认模式,通常用于主线程。
- UITrackingRunLoopMode:用于跟踪用户界面的触摸滑动。
- UIInitializationRunLoopMode:App启动时的初始模式,启动完成后不再使用。
- NSRunLoopCommonModes:包含多种模式,如默认、模态和跟踪模式。
开发者还可以自定义运行模式,通过设置不同的模式来控制事件源的响应。
生命周期管理
NSRunLoop的生命周期可以分为三个阶段:创建、运行和退出。
1. 创建
NSRunLoop是随着线程的创建而创建的,每个线程都有一个唯一的NSRunLoop实例。主线程的NSRunLoop由系统自动创建,而非主线程的NSRunLoop需要显式调用[NSRunLoop currentRunLoop]方法来创建。
2. 运行
主线程的NSRunLoop由系统自动运行,而非主线程的NSRunLoop需要显式调用run、runUntilDate或runMode:beforeDate方法来启动。
运行过程中,如果当前模式下没有事件源,NSRunLoop会直接退出。此外,某些特殊情况(如performSelector系列方法)也可能导致NSRunLoop退出。
3. 退出
NSRunLoop可以通过以下方式退出:
- 设置最大运行时间到期。
- 当前模式下的事件源为空。
- 调用CFRunLoopStop方法。
需要注意的是,使用run或runUntilDate方法启动的NSRunLoop,即使调用CFRunLoopStop,也可能无法立即退出,因为这些方法会不断调用runMode:beforeDate来维持运行。
嵌套运行
NSRunLoop支持嵌套运行,即在一个runloop的回调函数中启动另一个runloop。这种机制在处理复杂的事件驱动逻辑时非常有用。
示例代码如下:
+ (void)nestTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *timer1 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle1) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:2]]; NSLog(@"-end-"); }); } + (void)timerHandle1 { NSLog(@"timer111-%@", [[NSRunLoop currentRunLoop] currentMode]); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSTimer *timer2 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle2) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode]; }); [[NSRunLoop currentRunLoop] runMode:UITrackingRunLoopMode beforeDate:[NSDate distantFuture]]; } + (void)timerHandle2 { NSLog(@"timer222-%@", [[NSRunLoop currentRunLoop] currentMode]); CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]); }
在这个示例中,外层runloop运行在NSDefaultRunLoopMode模式下,内层runloop运行在UITrackingRunLoopMode模式下。通过调用CFRunLoopStop方法可以停止内层runloop,从而返回到外层runloop。
当同一模式下的runloop出现嵌套时,苹果的处理机制仍然能确保正确的执行顺序,避免了潜在的死锁问题。
注意:虽然r1和r2代表同一个runloop,但由于调用栈的不同,它们可以被视为嵌套层。理解这一点有助于更好地掌握NSRunLoop的嵌套运行机制。