Fork me on GitHub

iOS开发——RunLoop

前言

想要理解 RunLoop 其实并不难,但是由于 RunLoop 非常抽象化,很容易忘记,所以需要把它记下来,常回顾一下。

何为 RunLoop ?

  1. 什么是 RunLoop?

    ​ 顾名思义,RunLoop 就是在程序运行过程中循环做一些事情。准确是说,RunLoop 就是为了让线程处于激活状态。其作用如下:

    1. 保持程序的持续运行
    2. 处理 App 中的各种事件(触摸、滑动、定时器等等)
    3. 节省 CPU 资源,提高程序性能:该做事的时候做事,该休息的时候休息
  2. 项目中哪些地方会用到 RunLoop ?

    1. 控制线程的生命周期(线程保活 - 经常使用一个线程,希望它不要销毁)
    2. 解决 NSTimer 在 ScrollView 滑动时停止工作的问题
    3. 监控 App 卡顿
    4. 性能优化

RunLoop与线程

  1. 每一个线程都有一个唯一的 RunLoop 对象与之对应。
  2. 这些 RunLoop 对象保存在一个全局的 CFMutableDictionary 里,以线程为 key,以 RunLoop 对象为 Value。
  3. 线程刚创建的时候并没有 RunLoop 对象,RunLoop 会在第一次获取它的时候创建,main 线程的 RunLoop 刚开始会有人获取。 子线程默认是没有的,所以子线程默认情况下会在代码执行完毕后自动退出。
  4. RunLoop 会在线程结束时销毁。
  5. 主线程的 RunLoop 已经自动获取(创建),子线程默认没有开启 RunLoop。

个人理解:RunLoop其实就是用来线程保活的,频繁的创建销毁线程会消耗资源。

RunLoop 的 Mode(CFRunLoopModeRef)

  1. CFRunLoopModeRef 代表 RunLoop 的运行模式。

  2. 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source0、Source1、Timers、Observers

  3. RunLoop 启动的时候只能选择其中一个 Mode,作为 CurrentMode。多个 Mode 相互独立 互不干扰。

  4. RunLoop 同时可以有多个Mode,但当前只能有一个 Mode。要切换 Mode,需要退出 Loop, 重新选择 Mode 再进来执行。

  5. 如果 Mode 里没有任何 Source0、Source1、Timers、Observers, RunLoop会立即退出。

细节:通过 Foundation API 和 C语言 API 获取到的 RunLoop 对象的内存地址是不一样的,因为 NSRunLoop 对 CFRunLoopRef 做了一层封装。

常见的两种 Mode

  1. KCFRunLoopDefaultMode(NSDefaultRunLoopMode):App 的默认 Mode,通常主线程是在这个 Mode 下运行。
  2. UITrackingRunLoopMode:界面跟踪 Mode ,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  3. 还有一个假的模式:NSRunLoopCommonModes,注意它后面带了个 s ,它只是一个标记,意为着在设置了 common 标记的模式下都能运行。KCFRunLoopDefaultMode,UITrackingRunLoopMode 正好都在 _commonModes 数组中(有 common 标记)。

Mode 内包含的 字段的含义

  1. Source0,处理 UI点击事件,performSelector:onThread。
  2. Source1,基于 Port 的线程间通信,系统事件的捕捉(然后分发到 Source0进行处理)。
  3. Timers,NSTimer,performSelector:withObject:afterDelay: 。
  4. Observers,用于监听 RunLoop 的状态,UI 刷新(beforeWaiting),Autorelease Pool。

RunLoop 的执行流程

  1. 通知Observers:进入 RunLoop
  2. 通知Observers:即将处理 Timers
  3. 通知Observers:即将处理 Sources
  4. 处理 Blocks
  5. 处理 Source0(可能会再次处理 Blocks)
  6. 如果存在 Source1,直接跳转到第8步里的第3步
  7. 通知 Observers:开始休眠(等待消息唤醒)
  8. 通知 Observers:结束休眠(被某个消息唤醒)
    1. 处理 Timers
    2. 处理 GCD Async To Main Queue
    3. 处理 Source1
  9. 处理 Blocks
  10. 根据前面的执行结果,决定接下来如何操作
    1. 回到第2步
    2. 退出当前 Loop
  11. 通知 Observers:退出 Loop

因为苹果已经开源了 CoreFoundation 源代码,因此很容易找到 CFRunloop 源代码
从代码可以看出 CFRunloopRef 核心方法是 __CFRunloopRun() ,为了便于阅读我们可以看一下伪代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int32_t __CFRunLoopRun()
{
// 通知 Observers: 即将进入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);

do
{
// 通知 Observers: 将要处理timer和source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);

// 处理非延迟的主线程调用
__CFRunLoopDoBlocks();
// 处理Source0事件
__CFRunLoopDoSource0();

if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks();
}
/// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort();
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();

// 通知 Observers: 即将进入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

// 等待内核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();

// 等待。。。

// 通知 Observers: 从等待中醒来
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

// 处理因timer的唤醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();

// 处理异步方法唤醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()

// 处理Source1
else
__CFRunLoopDoSource1();

// 再次确保是否有同步的方法需要调用
__CFRunLoopDoBlocks();

} while (!stop && !timeout);

// 通知 Observers: 即将退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}

RunLoop 休眠的实现原理

RunLoop 中调用了由操作系统提供的 mach_msg() 函数,让线程由用户态转变为内核态,从而实现既不占用 CPU 资源也不退出的休眠状态。

一些零碎的注意点

注意:控制器的 -dealloc 默认是在主线程执行的。

注意:performSelector:withObject:afterDelay: 的本质是往Runloop中添加定时器,子线程默认没有启动RunLoop。

注意:一个线程一旦执行完任务就会销毁,再也无法执行其他任务。例如下面的 NSThread 对象,一旦 block 里的代码执行完,这个 thread 将不再执行任何代码。如果需要线程保活,就要用到 RunLoop。

1
2
3
4
5
NSThread *thread = [[NSThread alloc] initWithBlock:^{
// 默认情况下,执行完这个打印,该线程就会销毁
NSLog(@"abc");
}];
[thread start];

注意:主线程几乎所有的事情都交给了 RunLoop 去做,比如 UI 刷新,点击事件的处理,performSelector 等。

------------- 本文结束感谢您的阅读 -------------