首页 iOS开发:RunLoop
文章
取消

iOS开发:RunLoop

RunLoop 是 iOS 系统中非常重要的一个概念,它是事件处理的核心机制之一。在 iOS 应用程序中,每个线程都有一个与之关联的 RunLoop 对象,它主要负责管理这个线程所需要执行的任务、消息和事件,并且在必要时切换线程状态,使得线程可以进入休眠或者唤醒。

什么是 Run Loop

Run Loop 是一个事件处理循环,用于接收和处理来自输入源(如触摸、定时器、网络连接等)的事件。简单来说,它是一个不断循环的过程,直到所有事件都被处理完成或者强制停止。

我们可以将 Run Loop 理解为一个邮递员,不断地检查信箱里是否有新的信件,如果有就派送给相应的收件人,并且在空闲时间里休息一下。

Run Loop 的实现原理

在iOS系统中,每个线程都有一个与之关联的 Run Loop 对象。这个对象维护了该线程需要处理的事件和任务,并且提供了一个接口,用于启动和停止循环。

当我们调用 UIApplicationMain 函数来启动一个应用程序时,系统会自动创建一个主线程(也就是 UI 线程),并在其中创建一个 Run Loop 对象。这个 Run Loop 进入一个无限循环,等待事件的发生。

当事件发生时(比如用户点击了屏幕),系统会将事件放入当前线程的 Run Loop 中,并唤醒 Run Loop,从而开始处理事件。处理完事件后,Run Loop 又会进入休眠状态,等待下一个事件的发生。

RunLoop可以注册多种输入源,如:

  • Timer Source: 定时器
  • Dispatch Source: GCD任务
  • Port Based Source: 端口,用于进程间通信
  • Custom Input Sources: 自定义输入源

当某个输入源有事件产生时,RunLoop 会将其放入一个指定的 Mode 下的 Events Queue 中。RunLoop 会按照顺序依次处理此 queue 中的事件,直到 queue 中没有事件为止。

RunLoop 的运行模式

在处理事件时,RunLoop 需要知道哪些事件需要处理,哪些事件不需要处理。为了达到这个目的,RunLoop 引入了 Mode 的概念。Mode 是一个字符串,用于标识一组事件。在一个特定的 Mode 下,RunLoop 只会处理该 Mode 中注册的事件,而忽略其他 Mode 中的事件。iOS 系统中默认提供了五种运行模式:

  1. NSDefaultRunLoopMode:默认模式,处理大部分事件源。
  2. UITrackingRunLoopMode:界面跟踪模式,用于拖动 ScrollView 时接收触摸事件,其他输入源将被暂停处理。
  3. UIInitializationRunLoopMode:启动模式,应用启动时使用,只处理特定事件。
  4. NSRunLoopCommonModes:公共模式,同时处理多个模式下的事件源。
  5. NSConnectionReplyMode:用于处理分布式对象连接的模式。

RunLoop 支持多个不同的 Mode 同时存在,当一个 Mode 中没有任何事件需要处理时,RunLoop 会自动切换到另一个 Mode 中,从而避免阻塞线程。

Run Loop 的内部结构

Run Loop 内部包含以下几个重要的数据结构:

  • Mode:表示 Run Loop 当前正在处理的事件模式。
  • Source:表示 Run Loop 注册的输入源集合。
  • Observer:表示 Run Loop 注册的观察者集合。
  • Timer:表示 Run Loop 注册的定时器集合。
  • Activity:表示 Run Loop 的状态信息,包括是否处于活动状态、是否被暂停、是否正在等待睡眠等。

RunLoop 的内部结构可以类比为一个状态机。在不同的状态下,RunLoop 执行不同的操作,从而实现事件的处理和线程的管理。

Run Loop 的状态转换

RunLoop 可以处于以下几种状态:

  • Inactive:表示 Run Loop 未启动或已停止。
  • Waiting:表示 Run Loop 正在等待输入源的唤醒。
  • Running:表示 Run Loop 正在运行,处理事件队列中的事件。
  • Stopped:表示 Run Loop 已经停止,不再接收新的事件。

RunLoop 的生命周期

RunLoop 的生命周期主要包含以下几个阶段:

创建阶段

RunLoop 对象的创建是在线程第一次获取 RunLoop 时完成的,如果没有获取过就会创建一个默认的 RunLoop。RunLoop 的创建是由 CFRunLoopGetMain() 或 CFRunLoopGetCurrent() 函数实现的,它们返回的对象都可以被称为 RunLoop,但实际上主线程的 RunLoop 和其他线程的 RunLoop 是不同的。

RunLoop 的创建过程会生成一个 CFRunLoopRef 实例。CFRunLoopRef 包含了 RunLoop 的所有信息,例如输入源和定时源等。RunLoop 的初始化方法是 CFRunLoopRun(),它会一直运行,直到 RunLoop 被停止。

运行阶段

RunLoop 进入运行状态后,会不停地从消息队列中获取消息,并且传递给相应的处理器进行处理。当 RunLoop 处理完所有任务后,会进入休眠状态。

RunLoop 的运行方法有两种:CFRunLoopRun() 和 CFRunLoopRunInMode()。CFRunLoopRun() 是一个无限循环的函数,直到 RunLoop 被停止才会返回。CFRunLoopRunInMode() 则是一个带有模式参数的函数,只有在指定的模式下才会处理消息,其他消息会被暂时忽略。

下面的代码中,手动创建了一个后台线程,并且在其中创建了一个 RunLoop 对象。我们通过添加一个定时器事件源来模拟后台任务的执行,并且在主线程中设置 shouldStop 标志位来控制 RunLoop 的结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)startBackgroundTask {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //创建 RunLoop 对象
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        //添加一个定时器事件源
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(backgroundTask:) userInfo:nil repeats:YES];
        [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
        
        //启动 RunLoop,直到被停止
        while (!self.shouldStop) {
            [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    });
}

- (void)backgroundTask:(NSTimer *)timer {
    //执行后台任务,并根据需要更新 UI 界面
}

休眠阶段

如果当前线程没有任务需要执行,那么 RunLoop 就会进入休眠状态,直到有新的任务被加入到消息队列中才会重新唤醒。RunLoop 的休眠方法是 CFRunLoopRunInMode(),我们可以设置超时时间来避免 RunLoop 永久休眠。

停止阶段

RunLoop 可以手动停止,也可以由于某些异常情况导致自动停止。RunLoop 停止的方法是 CFRunLoopStop(),它可以立即停止 RunLoop 的运行,并且退出当前的 CFRunLoopRun() 或 CFRunLoopRunInMode() 函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (void)startObservingRunLoop {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    
    //创建一个 Observer 对象,监听 RunLoop 的 Entry、BeforeWaiting 和 Exit 事件
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                                        kCFRunLoopEntry | kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                                                        true,
                                                                        0,
                                                                        ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
                                                                            NSLog(@"RunLoop is %s", [self stringFromRunLoopActivity:activity]);
                                                                        });
    //将 Observer 添加到当前 RunLoop 中
    CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
    
    //释放 Observer 对象
    CFRelease(observer);
}

- (NSString *)stringFromRunLoopActivity:(CFRunLoopActivity)activity {
    switch (activity) {
        case kCFRunLoopEntry:
            return @"Entry";
        case kCFRunLoopBeforeTimers:
            return @"Before Timers";
        case kCFRunLoopBeforeSources:
            return @"Before Sources";
        case kCFRunLoopBeforeWaiting:
            return @"Before Waiting";
        case kCFRunLoopAfterWaiting:
            return @"After Waiting";
        case kCFRunLoopExit:
            return @"Exit";
        default:
            return @"Unknown";
    }
}

RunLoop 的使用场景

RunLoop 可以在许多情况下提高程序的性能和响应速度,以下是常见的 RunLoop 使用场景:

  1. 处理异步任务

当需要在后台执行一些比较耗时的任务时,我们可以在其他线程上手动创建一个 RunLoop 对象,并且在任务执行结束后关闭它,以保证线程不会因为长时间阻塞而导致无法响应其他事件。这种方式可以避免使用 GCD 等异步执行方法时可能出现的问题,例如任务执行顺序不可控、不能中途取消任务等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)startBackgroundTask {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //创建 RunLoop 对象
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        //添加一个端口输入源
        NSMachPort *port = [NSMachPort port];
        [port setDelegate:self];
        [runLoop addPort:port forMode:NSDefaultRunLoopMode];
        
        //执行后台任务
        [self doBackgroundTask];
        
        //停止 RunLoop
        CFRunLoopStop([runLoop getCFRunLoop]);
    });
}

- (void)doBackgroundTask {
    //执行后台任务,并根据需要更新 UI 界面
}

- (void)handleMachMessage:(void *)msg {
    //处理从主线程发送的消息
}
  1. 控制下载速度

当需要控制下载速度时,可以通过定时器来限制每次下载的数据量,并且在下载完成后休眠一段时间再继续下载。这样可以避免过多的网络请求和数据传输占用系统资源,提高下载效率和稳定性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)startDownloadTask {
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.timeoutIntervalForRequest = 10.0;
    
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com"]];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    
    //启动下载任务,并且在 RunLoop 中控制下载速度
    [task resume];
    [self delayWithTimeInterval:0.5 block:^{
        [task cancel];
    }];
}

- (void)delayWithTimeInterval:(NSTimeInterval)interval block:(dispatch_block_t)block {
    //创建定时器事件源,并且添加到当前线程的 RunLoop 中
    NSRunLoopTimer *timer = [[NSRunLoop currentRunLoop] timerWithTimeInterval:interval repeats:NO block:^(NSTimer * _Nonnull timer) {
        if (block) {
            block();
        }
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
  1. UI 渲染

UIKit 中大量的 UI 操作都是通过 RunLoop 来实现的。当界面需要更新时,RunLoop 会将相关的刷新操作加入到消息队列中,并且在下一次循环中执行,从而保证界面的流畅性。如果没有使用 RunLoop,那么界面更新可能会出现卡顿甚至崩溃的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)startRenderingTask {
    dispatch_async(dispatch_get_main_queue(), ^{
        //获取当前 RunLoop 对象
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        //添加一个定时器事件源,每隔 1/60 秒触发一次
        CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderingTask:)];
        [displayLink addToRunLoop:runLoop forMode:NSRunLoopCommonModes];
        
        //启动 RunLoop
        [runLoop runUntilDate:[NSDate distantFuture]];
    });
}

- (void)renderingTask:(CADisplayLink *)displayLink {
    //执行 UI 渲染操作
}
本文由作者按照 CC BY 4.0 进行授权