目录
1. Runloop定义
Runloop是一个事件处理循环,用于调度工作并协调接收传入事件。
Runloop 的目的是在有工作要做的时候让线程保持忙碌,在没有工作的时候让线程休眠。
Runloop管理不是完全自动的,我们仍然需要在适当的时间启动Runloop并响应传入的事件。应用程序不需要显式地创建Runloop对象,每个线程(包括主线程),都有一个关联的Runloop对象,但是,只有子线程需要显式地开启Runloop。在应用程序启动的时候,主线程的Runloop默认已经开启。
Runloop接收来自两种不同类型的源的事件。输入源传递异步事件,通常是来自另一个线程或来自不同应用程序的消息。计时器源传递同步事件,发生在预定时间或重复间隔。
下图显示了Runloop和各种源的概念结构。输入源将异步事件传递给相应的处理程序,并运行runUntilDate:方法(在线程关联的NSRunloop对象上调用)退出。计时器源将事件传递给处理程序,但不会导致Runloop退出。
另外,除了处理输入源之外,Runloop也能发送一些关于循环行为的通知。注册Runloop的观察者可以接收这些通知并使用它们在线程上做额外的处理。
2. Runloop结构及创建
Runloop是基于CFRunloop封装的,下面看一下CFRunloop底层结构:
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;
CFTypeRef _counterpart;
};
由上面结构可知,Runloop中包含了Modes以及ModeItems。
Runloop的创建也不需要程序员手动创建的,主线程的Runloop在程序启动的时候已经创建好,而子线程的Runloop则在第一次获取的时候创建。
下表中列举了获取Runloop对象的两种方法:
方法 | 描述 |
---|---|
class var current: RunLoop | 返回当前线程的Runloop,如果在子线程调用,则返回子线程Runloop,如果在主线程调用,则返回主线程Runloop。 |
class var main: RunLoop | 返回主线程Runloop。 |
3. Runloop Modes(模式)
Runloop模式是一个监视输入源和定时器的集合以及要通知的Runloop观察者的集合。每次运行Runloop时,都需要显式或隐式的指定运行模式。在Runloop的传递过程中,只监视与该模式相关的源并允许传递事件,以及与该模式相关的观察者才会收到Runloop通知。与其他模式相关联的源则会保留其事件,直到Runloop切换到其模式再会执行。
下表中列出了Runloop常用的模式:
名称 | 描述 |
---|---|
static let common: RunLoop.Mode | 使用该值作为模式被添加到Runloop的对象,将会被Runloop的所有的模式监控,也就是该模式包含了default和tracking两个模式。 |
static let `default`: RunLoop.Mode | 处理输入源的模式,Runloop常用的模式,App的默认Mode,通常主线程是在这个Mode下运行。 |
static let tracking: RunLoop.Mode | 在跟踪控件的时候使用该模式。界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。 |
Mode底层结构如下:
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 */
};
上面结构中包含了几个重要的事件源:sources0、sources1、timer以及observer。
因此可知:Runloop对象包含多个Mode对象,而每个Mode又包含了多个事件源。
线程也会根据不同的Mode处理Mode下面的事件。
4. Runloop Mode items(事件源)
由上面可知Mode items主要有sources0、sources1、timer、observer,下面分别来看一下。
4.1 CFRunLoopSourceRef
Source分为两种:
Source0:非基于Port的,用于用户主动触发的事件(点击button 或点击屏幕)
Source1:基于Port的,通过内核和其他线程相互发送消息(与内核相关)
注意:Source1在处理的时候会分发一些操作给Source0去处理
除了基于端口的源外,系统还定义了一个自定义输入源,允许在任何线程上执行方法。执行的方法在目标线程上序列化,从而缓解了在一个线程上运行多个方法时可能出现的一些同步问题。这些方法事件源在执行之后从Runloop中删除自己。
具体方法见下表:
以上方法不管运行在哪个线程上,都必须确保该线程的Runloop已经开启了,对于自己创建的线程,只有开启了Runloop代码才会开始执行,对于主线程来说,程序启动后Runloop就已经开启了。
Runloop在一次循环内会处理所有排队的事件源,而非每循环一次处理一个。
4.2 CFRunLoopTimer
Timer会在未来的预设时间同步地向线程传递事件。Timer是线程通知自己做某事的一种方式。
虽然它生成基于时间的通知,但Timer不是采用实时机制。与输入源一样,Timer与Runloop的特定模式相关联。如果Timer不处于当前由Runloop的模式中,Timer不会触发,直到切换到Timer支持的模式中。如果Timer到了触发时间,而Runloop正处于处理某个事件当中,那么Timer则会在下一次循环中再次触发。
Timer可以配置为只触发一次事件或者重复触发事件。如果是按照预设的时间重复触发事件,那么每次触发事件的时间都会略晚于预设的时间,比如每5秒触发一次事件,那么实际上事件的触发间隔会略大于5秒。如果Timer事件触发时间延迟了太多,多余一个间隔时间或者多个间隔时间,那么timer在这个错过的时间内只会触发一次时间,而下一次触发时间则会根据上一次出发时间再安排。
常用的Timer方法如下:
4.3 CFRunLoopObserverRef
CFRunLoopObserverRef能够监听RunLoop的状态改变。Runloop会在自身循环特定的位置通知观察者,例如:
- Runloop运行入口。
- 当Runloop即将处理Timer的时候。
- 当Runloop即将处理输入源的时候。
- 当Runloop即将进入睡眠的时候。
- 当Runloop被唤醒时,但在处理唤醒它的事件之前。
- Runloop的退出。
我们可以通过以下两个方法添加一个observer到Runloop中,并监听其所有状态。
public func CFRunLoopObserverCreateWithHandler(_ allocator: CFAllocator!, _ activities: CFOptionFlags, _ repeats: Bool, _ order: CFIndex, _ block: ((CFRunLoopObserver?, CFRunLoopActivity) -> Void)!) -> CFRunLoopObserver!
public func CFRunLoopAddObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFRunLoopMode!)
测试代码如下:
func observerDemo() {
DispatchQueue.global().async {
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { (oberver, activity) in
switch activity {
case .entry:
print("\(Date()) Runloop进入了")
case .beforeTimers:
print("\(Date()) Runloop即将处理Timers")
case .beforeSources:
print("\(Date()) Runloop即将处理Sources")
case .beforeWaiting:
print("\(Date()) Runloop要休眠了")
case .afterWaiting:
print("\(Date()) Runloop被唤醒了")
case .exit:
print("\(Date()) Runloop退出了")
default :
print("\(Date()) allActivities")
}
}
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, CFRunLoopMode.defaultMode)
Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { (timer) in
print("\(Date()) Timer")
}
RunLoop.current.run()
}
}
上面的demo中在子线程的Runloop中添加了一个observer,并开启了一个timer,运行后打印结果如下:
2020-11-26 05:28:12 +0000 Runloop进入了
2020-11-26 05:28:12 +0000 Runloop即将处理Timers
2020-11-26 05:28:12 +0000 Runloop即将处理Sources
2020-11-26 05:28:12 +0000 Runloop要休眠了
2020-11-26 05:28:15 +0000 Runloop被唤醒了
2020-11-26 05:28:15 +0000 Timer
2020-11-26 05:28:15 +0000 Runloop退出了
从结果可以看出Runloop开启后,因没有事件需要处理,立即进入了睡眠,当3秒后Timer将Runloop唤醒,并执行了Timer的回调方法,Timer只执行一次,因此执行完Runloop立即退出了。
5. Runloop事件处理逻辑
- 通知观察者已经进入Runloop。
- 通知观察者即将处理Timer。
- 通知观察者即将处理Sources0事件。
- 处理Sources0事件。
- 如果有Sources1事件就绪,等待执行,那么直接跳到第9步.
- 通知观察者线程即将进入睡眠。
- 线程进入睡眠,直到下列之一的事件将其唤醒。
- 基于端口的输入源事件到达。
- Timer事件。
- Runloop超多了设定的运行时间。
- 显示的唤醒。
-
通知观察者线程已经唤醒。
-
处理唤醒时的事件
-
如果自定义Timer启动,则处理Timer事件并重新启动循环。 进入第2步。
-
如果输入源事件触发,处理输入源事件。
-
如果Runloop被显式唤醒,但尚未超时,请重新启动循环。 进入第2步。
-
-
通知观察者Runloop已经退出。
6. Runloop与线程
虽然RunLoop与线程的关系十分密切,但是并不是每个线程都拥有一个RunLoop的。
主线程的Runloop由系统在程序开始运行的时候就已经创建并开启了。
子线程的Runloop则需要手动获取的时候由系统创建。
有一个全局的Dictionary将线程与Runloop对应进行存储,当调用Runloop.current的时候,系统便会根据线程标识去Dictionary查找其对应的Runloop是否创建了,找到了直接返回,否则创建一个再返回。
本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。