引言

在iOS和macOS开发中,Runloop(运行循环)是一个非常重要的概念,它是程序运行的核心机制之一。理解Runloop的底层原理,对于解决诸如界面卡顿、定时器失效、线程保活等问题具有重要意义。本文将深入剖析Runloop的底层实现,带你了解其背后的奥秘。

一、Runloop的基本概念

1.1 定义

Runloop是一种事件处理循环机制,它可以让线程在没有任务时进入休眠状态,有任务时被唤醒并处理任务,从而避免线程不断地空转消耗CPU资源。在Objective - C中,Runloop由Core Foundation框架提供,对应的核心类是CFRunLoopRef

1.2 作用

  • 保持程序的持续运行:主线程的Runloop会一直运行,保证程序不会在启动后立即退出。
  • 处理事件和消息:包括用户的触摸事件、网络请求的响应、定时器的触发等。
  • 节省CPU资源:在没有任务时,线程进入休眠状态,降低CPU的使用率。

1.3 与线程的关系

  • 一一对应:每个线程都有与之对应的唯一的Runloop对象。
  • 按需创建:主线程的Runloop在程序启动时自动创建并启动,而子线程的Runloop默认是没有创建的,只有在第一次调用[NSRunLoop currentRunLoop]时才会创建。
  • 生命周期:线程结束时,其对应的Runloop也会被销毁。

二、Runloop的底层数据结构

2.1 CFRunLoop结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
};
  • _lock:用于保护对模式列表的访问,确保线程安全。
  • _wakeUpPort:用于唤醒Runloop的端口。
  • _commonModes:存储通用模式的集合。
  • _commonModeItems:存储通用模式下的事件源、定时器和观察者。
  • _currentMode:当前正在运行的模式。
  • _modes:存储所有的运行模式。

2.2 CFRunLoopMode结构体

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
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 */
};
  • _name:模式的名称,如kCFRunLoopDefaultModeUITrackingRunLoopMode等。
  • _sources0:存储Source0类型的事件源。
  • _sources1:存储Source1类型的事件源。
  • _observers:存储观察者对象,用于监听Runloop的状态变化。
  • _timers:存储定时器对象。

2.3 事件源(Source)

事件源分为两种类型:

  • Source0:非基于端口的事件源,需要手动唤醒Runloop。例如,UIEvent、performSelector:onThread:等。
  • Source1:基于端口的事件源,能主动唤醒Runloop。例如,系统事件、Mach port等。

2.4 定时器(Timer)

定时器用于在指定的时间或时间间隔后触发事件。常见的定时器有NSTimerCADisplayLink

2.5 观察者(Observer)

观察者用于监听Runloop的状态变化,如进入Runloop、即将处理定时器、即将进入休眠等。可以通过CFRunLoopAddObserver函数添加观察者。

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理定时器
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理事件源
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};

三、Runloop的运行流程

3.1 启动Runloop

在主线程中,Runloop会在程序启动时自动启动。而在子线程中,需要手动启动Runloop。启动Runloop的核心函数是CFRunLoopRunSpecific

1
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
  • rl:要运行的Runloop对象。
  • modeName:运行的模式名称。
  • seconds:Runloop运行的最长时间。
  • returnAfterSourceHandled:处理完一个事件源后是否立即返回。

3.2 完整的运行流程

Runloop的运行流程可以分为以下几个主要步骤:

  1. 通知观察者进入Runloop:调用所有注册的观察者的回调函数,通知它们Runloop即将进入。
  2. 处理即将到期的定时器:检查所有的定时器,找出即将到期的定时器并触发它们的回调函数。
  3. 处理非延迟的performSelector调用:执行所有非延迟的performSelector方法。
  4. 处理Source0事件源:检查所有的Source0事件源,如果有准备好的事件,则处理这些事件。
  5. 检查Source1事件源:如果没有Source0事件需要处理,则检查是否有Source1事件。如果有Source1事件,则处理该事件并唤醒Runloop。
  6. 进入休眠状态:如果没有任何事件需要处理,Runloop会进入休眠状态,等待被唤醒。
  7. 被唤醒并处理事件:当有事件发生(如定时器到期、Source1事件触发等)时,Runloop会被唤醒,并处理相应的事件。
  8. 通知观察者即将退出Runloop:调用所有注册的观察者的回调函数,通知它们Runloop即将退出。

以下是一个简化的伪代码表示:

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
// 启动Runloop
while (true) {
// 通知观察者进入Runloop
notifyObservers(kCFRunLoopEntry);

// 处理即将到期的定时器
handleTimers();

// 处理非延迟的performSelector调用
handleNonDelayedPerformSelectors();

// 处理Source0事件源
if (hasReadySource0()) {
handleSource0();
} else {
// 检查Source1事件源
if (hasSource1()) {
handleSource1();
continue;
}

// 进入休眠状态
notifyObservers(kCFRunLoopBeforeWaiting);
sleep();
notifyObservers(kCFRunLoopAfterWaiting);
}

// 检查是否需要退出Runloop
if (shouldExit()) {
notifyObservers(kCFRunLoopExit);
break;
}
}

四、Runloop的模式(Mode)

4.1 模式的概念

Runloop的模式是一种隔离机制,不同的模式下可以有不同的事件源、定时器和观察者。Runloop在同一时间只能运行在一种模式下,只有在该模式下注册的事件源、定时器和观察者才会被处理。

4.2 常见的模式

  • kCFRunLoopDefaultMode:默认模式,大多数情况下Runloop运行在这个模式下。
  • UITrackingRunLoopMode:用于处理UI跟踪事件,如滚动视图的滚动。当用户拖动滚动视图时,Runloop会切换到这个模式。
  • kCFRunLoopCommonModes:这不是一个实际的模式,而是一个标记。可以将事件源、定时器和观察者注册到这个标记下,这样它们就可以在多个模式下都能被处理。

4.3 模式切换

Runloop在不同的情况下会切换运行模式。例如,当用户开始拖动滚动视图时,Runloop会从kCFRunLoopDefaultMode切换到UITrackingRunLoopMode,以确保滚动事件能够得到及时处理。当滚动结束后,Runloop又会切换回原来的模式。

五、Runloop与线程的唤醒和休眠

5.1 休眠机制

Runloop在没有任务需要处理时会进入休眠状态,以节省CPU资源。在休眠状态下,线程会释放CPU资源,等待被唤醒。Runloop进入休眠状态的关键是调用mach_msg函数,该函数会使线程进入内核态,等待消息的到来。

5.2 唤醒机制

当有事件发生时,Runloop会被唤醒。唤醒Runloop的方式有多种:

  • Source1事件:基于端口的事件源(Source1)可以主动唤醒Runloop。
  • 定时器到期:当定时器到期时,会触发相应的回调函数,从而唤醒Runloop。
  • 手动唤醒:可以通过调用CFRunLoopWakeUp函数手动唤醒Runloop。

六、Runloop与AutoreleasePool

6.1 主线程的AutoreleasePool

在主线程中,Runloop会自动管理AutoreleasePool。每次Runloop循环开始时,会创建一个新的AutoreleasePool,在循环结束时,会释放这个AutoreleasePool。这样可以确保在一次Runloop循环中创建的自动释放对象在循环结束时被释放。

6.2 子线程的AutoreleasePool

在子线程中,需要手动管理AutoreleasePool。如果在子线程中创建了大量的自动释放对象,而没有及时释放,可能会导致内存泄漏。因此,在子线程中启动Runloop时,通常需要手动创建和释放AutoreleasePool。

1
2
3
4
5
6
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@autoreleasepool {
// 子线程任务
[[NSRunLoop currentRunLoop] run];
}
});

七、总结

Runloop是Objective - C中一个非常重要的机制,它通过事件循环的方式,让线程在有任务时处理任务,没有任务时进入休眠状态,从而提高了程序的性能和响应能力。深入理解Runloop的底层原理,包括其数据结构、运行流程、模式机制、唤醒和休眠机制以及与AutoreleasePool的关系,对于优化代码性能、解决各种开发中的问题具有重要意义。希望本文能帮助你更好地理解Runloop的底层原理。

参考资料