引言
在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 | struct __CFRunLoop { |
_lock
:用于保护对模式列表的访问,确保线程安全。_wakeUpPort
:用于唤醒Runloop的端口。_commonModes
:存储通用模式的集合。_commonModeItems
:存储通用模式下的事件源、定时器和观察者。_currentMode
:当前正在运行的模式。_modes
:存储所有的运行模式。
2.2 CFRunLoopMode结构体
1 | struct __CFRunLoopMode { |
_name
:模式的名称,如kCFRunLoopDefaultMode
、UITrackingRunLoopMode
等。_sources0
:存储Source0类型的事件源。_sources1
:存储Source1类型的事件源。_observers
:存储观察者对象,用于监听Runloop的状态变化。_timers
:存储定时器对象。
2.3 事件源(Source)
事件源分为两种类型:
- Source0:非基于端口的事件源,需要手动唤醒Runloop。例如,UIEvent、performSelector:onThread:等。
- Source1:基于端口的事件源,能主动唤醒Runloop。例如,系统事件、Mach port等。
2.4 定时器(Timer)
定时器用于在指定的时间或时间间隔后触发事件。常见的定时器有NSTimer
和CADisplayLink
。
2.5 观察者(Observer)
观察者用于监听Runloop的状态变化,如进入Runloop、即将处理定时器、即将进入休眠等。可以通过CFRunLoopAddObserver
函数添加观察者。
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
三、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的运行流程可以分为以下几个主要步骤:
- 通知观察者进入Runloop:调用所有注册的观察者的回调函数,通知它们Runloop即将进入。
- 处理即将到期的定时器:检查所有的定时器,找出即将到期的定时器并触发它们的回调函数。
- 处理非延迟的
performSelector
调用:执行所有非延迟的performSelector
方法。 - 处理Source0事件源:检查所有的Source0事件源,如果有准备好的事件,则处理这些事件。
- 检查Source1事件源:如果没有Source0事件需要处理,则检查是否有Source1事件。如果有Source1事件,则处理该事件并唤醒Runloop。
- 进入休眠状态:如果没有任何事件需要处理,Runloop会进入休眠状态,等待被唤醒。
- 被唤醒并处理事件:当有事件发生(如定时器到期、Source1事件触发等)时,Runloop会被唤醒,并处理相应的事件。
- 通知观察者即将退出Runloop:调用所有注册的观察者的回调函数,通知它们Runloop即将退出。
以下是一个简化的伪代码表示:
1 | // 启动Runloop |
四、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 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
七、总结
Runloop是Objective - C中一个非常重要的机制,它通过事件循环的方式,让线程在有任务时处理任务,没有任务时进入休眠状态,从而提高了程序的性能和响应能力。深入理解Runloop的底层原理,包括其数据结构、运行流程、模式机制、唤醒和休眠机制以及与AutoreleasePool的关系,对于优化代码性能、解决各种开发中的问题具有重要意义。希望本文能帮助你更好地理解Runloop的底层原理。
参考资料: