iOS Runloop探索(快速了解Runloop)

目录

1. Runloop定义

2. Runloop结构及创建

3. Runloop Modes(模式)

4. Runloop Mode items(事件源)

4.1 CFRunLoopSourceRef

4.2 CFRunLoopTimer

4.3 CFRunLoopObserverRef

5. Runloop事件处理逻辑

6. Runloop与线程


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 */
};

上面结构中包含了几个重要的事件源:sources0sources1timer以及observer

因此可知:Runloop对象包含多个Mode对象,而每个Mode又包含了多个事件源。

线程也会根据不同的Mode处理Mode下面的事件。

4. Runloop Mode items(事件源)

由上面可知Mode items主要有sources0sources1timerobserver,下面分别来看一下。

4.1 CFRunLoopSourceRef

Source分为两种:

Source0:非基于Port的,用于用户主动触发的事件(点击button 或点击屏幕)
Source1:基于Port的,通过内核和其他线程相互发送消息(与内核相关)
注意:Source1在处理的时候会分发一些操作给Source0去处理

除了基于端口的源外,系统还定义了一个自定义输入源,允许在任何线程上执行方法。执行的方法在目标线程上序列化,从而缓解了在一个线程上运行多个方法时可能出现的一些同步问题。这些方法事件源在执行之后从Runloop中删除自己。

具体方法见下表:

MethodsDescriptions
func perform(Selector, with: Any?, afterDelay: TimeInterval)Invokes a method of the receiver on the current thread using the default mode after a delay.
func perform(Selector, with: Any?, afterDelay: TimeInterval, inModes: [RunLoop.Mode])Invokes a method of the receiver on the current thread using the specified modes after a delay.
func performSelector(onMainThread: Selector, with: Any?, waitUntilDone: Bool)Invokes a method of the receiver on the main thread using the default mode.
func performSelector(onMainThread: Selector, with: Any?, waitUntilDone: Bool, modes: [String]?)Invokes a method of the receiver on the main thread using the specified modes.
func perform(Selector, on: Thread, with: Any?, waitUntilDone: Bool)Invokes a method of the receiver on the specified thread using the default mode.
func perform(Selector, on: Thread, with: Any?, waitUntilDone: Bool, modes: [String]?)Invokes a method of the receiver on the specified thread using the specified modes.
func performSelector(inBackground: Selector, with: Any?)Invokes a method of the receiver on a new background thread.
class func cancelPreviousPerformRequests(withTarget: Any)Cancels perform requests previously registered with the perform(_:with:afterDelay:) instance method.
class func cancelPreviousPerformRequests(withTarget: Any, selector: Selector, object: Any?)Cancels perform requests previously registered with perform(_:with:afterDelay:).

以上方法不管运行在哪个线程上,都必须确保该线程的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方法如下:

方法名称描述
class func scheduledTimer(withTimeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void) -> Timer创建一个Timer,并自动将其加入到当前Runloop的默认模式下。
class func scheduledTimer(timeInterval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool) -> Timer创建一个Timer,并自动将其加入到当前Runloop的默认模式下。
class func scheduledTimer(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool) -> Timer创建一个Timer,并自动将其加入到当前Runloop的默认模式下。
init(timeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void)初始化一个Timer,需要手动调用add(_:forMode:) 方法将其加入到指定的Runloop的模式下。
init(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool)初始化一个Timer,需要手动调用add(_:forMode:) 方法将其加入到指定的Runloop的模式下。
init(timeInterval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool)初始化一个Timer,需要手动调用add(_:forMode:) 方法将其加入到指定的Runloop的模式下。
init(fire: Date, interval: TimeInterval, repeats: Bool, block: (Timer) -> Void)初始化一个在指定时间触发的Timer,需要手动调用add(_:forMode:) 方法将其加入到指定的Runloop的模式下。
init(fireAt: Date, interval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool)初始化一个在指定时间触发的Timer,需要手动调用add(_:forMode:) 方法将其加入到指定的Runloop的模式下。

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时,它都会处理挂起的事件,并为附加的观察者生成通知。 其执行顺序具体如下:
  1. 通知观察者已经进入Runloop。
  2. 通知观察者即将处理Timer。
  3. 通知观察者即将处理Sources0事件。
  4. 处理Sources0事件。
  5. 如果有Sources1事件就绪,等待执行,那么直接跳到第9步.
  6. 通知观察者线程即将进入睡眠。
  7. 线程进入睡眠,直到下列之一的事件将其唤醒。
    1. 基于端口的输入源事件到达。
    2. Timer事件。
    3. Runloop超多了设定的运行时间。
    4. 显示的唤醒。
  8. 通知观察者线程已经唤醒。

  9. 处理唤醒时的事件

    1. 如果自定义Timer启动,则处理Timer事件并重新启动循环。 进入第2步。

    2. 如果输入源事件触发,处理输入源事件。

    3. 如果Runloop被显式唤醒,但尚未超时,请重新启动循环。 进入第2步。

  10. 通知观察者Runloop已经退出。

6. Runloop与线程

虽然RunLoop与线程的关系十分密切,但是并不是每个线程都拥有一个RunLoop的。

主线程的Runloop由系统在程序开始运行的时候就已经创建并开启了。

子线程的Runloop则需要手动获取的时候由系统创建。

有一个全局的Dictionary将线程与Runloop对应进行存储,当调用Runloop.current的时候,系统便会根据线程标识去Dictionary查找其对应的Runloop是否创建了,找到了直接返回,否则创建一个再返回。

 

本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值