RunLoop F.A.Q

作者: seedante | 来源:发表于2018-02-01 19:00 被阅读303次

    以问答的形式介绍以下内容:从线程的角度理解 RunLoop,RunLoop Mode 的设计机制及使用技巧,以 RunLoop 为基础的日常场景以及注意事项。

    推荐下面两个资料:

    1. Run Loops 官方资料:详细介绍了 RunLoop 的基础概念、运行机制和使用方法,入门首选,在阅读其他任何有关 RunLoop 的文章遇到难以理解的概念时,可以在这个官方资料里找到最原始、最准确的解释。建议把这个官方的资料读上三遍,再动手写个代码以加强理解。
    2. 深入理解RunLoop:这篇文章结合 RunLoop 的开源版本代码给出了更多的实现细节,并且讨论了很多 RunLoop 的实际应用。记得看下面的评论,知道了知识点不一定懂得怎么运用以及解决问题,评论里有很多实际的问题可以帮助你更好地理解 RunLoop。

    本文主要是记录下自己的理解,并提供了些上面两篇资料没有顾及到的新内容。

    Q: RunLoop 与 Thread 的关系?

    A: 了解 RunLoop 首先得了解 NSThread,开头给出的官方资料其实是 Threading Programming Guide 中的一个章节。不必害怕 NSThread,它的易用性实际上和通常的并发首选 GCD 或者 NSOperation 差不多,只不过后两者是对 NSThread 的封装,使用上更加方便,而且能够避免大部分使用 NSThread 时的陷阱,关于这点可以看并发编程:API 及挑战中的讨论。

    不管是 GCD, NSOperation,抑或是 NSThread 的底层 pthread,我们用多线程最终是为了运行你的代码而已,等任务代码运行完毕,这个线程就无法再次使用了,来看下面的例子:

    // NSThread 还提供了两个基于 selector 的方法,可以避免子类化。
    class SEThread: Thread {
        // 任务代码放在 main() 方法里
        override func main() {
            // 打印 log 来说明当前代码是否运行在主线程
            NSLog("\(Thread.current) is main thread: \(Thread.isMainThread)")
        }    
    }
    
    func useAThread(){
        let thread = SEThread.init()
        // 启动线程,这个方法调用main()并维护相关状态,实际上调用start()
        // 才会真正分配资源生成线程,文档里明确告诉我们是这么做的。
        thread.start()
    }
    

    让一段代码在 NSThread 子类对象里执行,条件是很苛刻的:只有main()运行在分配的线程里,而且必须通过start()启动该线程,这点可通过配合上面的 log 来验证,因此必须在main()里调用这段代码。如果你维护一个上面线程的引用,在其他地方再次start(),应用就直接 crash 了,唯一的提示是你尝试再次启动这个线程,也就是说在线程的生命周期里只能启动一次。所以,NSThread 就是个一次性的资源。

    那么有没有这么一种机制,保留 NSThread 的资源,只在需要的时候使用呢?RunLoop 就是为此设计的。RunLoop 就是一个无限循环,停留在main()里,通过添加 Sources(比如 NSTimer),姑且称之为事件源,有事件发生时就唤醒线程(和通知机制类似)来干活(干活本质上还是运行一段代码,这段代码可以有多种来源),活干完了就让线程休眠,然后 RunLoop 也进入休眠状态,等待下个事件到来后唤醒线程干活,如此循环,具体的处理流程在官方资料里有详细的描述。我想了很多现实里的关系来比喻这两者,但没有很合适的。

    总结下:NSThread 有两种工作方式,一种是直接运行任务代码,比如上面的例子,比如通过 GCD 的 Block 提供任务代码,是个一次性的资源,用完即废;另一种就是搭配 RunLoop,需要处理事件时被 RunLoop 唤醒执行代码,执行完毕后进入休眠,按需使用,只要 RunLoop 还存在,线程就可以一直提供服务。

    那么这两种方式能不能结合起来呢,一举两得?写个代码测试下。

    override func main() {
       // NSTimer 的另外一种使用方法,timer 绑定的方法将会在当前线程里按照设定的时间执行
       RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
       // 除了主线程的 RunLoop,其他线程的 RunLoop 必须手动启动
       RunLoop.current.run()
       
       // 任务代码,只是在当前线程里循环1000000次。
       for i in 0..<1000000 {
           if i % 100000 == 0{
               NSLog("Iterate to \(i)")
           }
       }
    }
    

    RunLoop 运行后,代码就一直停留在运行这里无限循环,正如其名,其实还可以指定超时时间,到达时间后自动退出循环。如果 RunLoop 的事件源被移除或者结束后,RunLoop 无事可干,就会退出循环。在上面的代码里,如果 timer 是重复执行的,在 timer 结束前(可以使用invalidate()来停止),RunLoop 不会退出循环,下面的任务代码永远不会被执行。所以,结合这两种方式是没有意义的:要么直接运行一段任务代码,结束后不再使用该线程;要么配合 RunLoop,有活干活,没活休眠。

    RunLoop 无法通过 init 的方式生成,只能通过 RunLoop 类的类变量获取:

    //这两者在 Core Foundation 下都有对应的方法: CFRunLoopGetCurrent(), CFRunLoopGetMain()
    class var current: RunLoop
    class var main: RunLoop
    

    而只有在线程内RunLoop.current获取的才是那个线程的 RunLoop,线程的内部环境有以下几个入口:

    1. NSThread 子类的main()里,而且只有通过调用start()main()内部才是这个线程的环境,比如直接调用thread.main(),这时候main()内部的线程环境是当前线程,而不是 thread 本身的线程;

    2. NSThread 两个基于 selector 的 API,调用的 selector 内部;

    3. GCD 和 NSOperationQueue 基于 Block 的 API,Block 里面。需要注意的是,GCD 里 sync 一类的方法出于优化的目的,会尽可能在当前线程里执行,这时候 Block 里的线程环境很可能不是你想要的,比如:

       DispatchQueue.global().sync {
           // 这里的线程环境很大可能是调用 DispatchQueue.global().sync{} 这条语句所在的线程
           // 这一段代码和直接写成RunLoop.current.run()无异
           RunLoop.current.run()
       }
      

    NSThread 对 RunLoop 是必需的,但 RunLoop 对 NSThread 并不是必需的,除了主线程,其他线程并不会主动生成 RunLoop,除非你通过RunLoop.current主动索取,如果当前线程没有 RunLoop,这个方法会自动生成一个,在 NSThread 的生命周期内用的是同一个 RunLoop 对象。

    Q: RunLoop mode 是什么?

    A: 本来这似乎不是个问题,不过当初看到深入理解RunLoop这篇文章的时候,每次看到讲解 RunLoop mode 的时候我就晕了,但我这次直接看了官方资料的解释后立马就理解了,可能每个人对其他人的概念解释的接受度不一样,但几乎每次看不懂中文版本的概念解释时,回头看原始版本的解释就明白了。如果你看这篇文章有概念不清楚的地方,去看官方资料。

    RunLoop 和事件源的关系与通知机制类似,订阅某些对象来获取它们的动态,上一个问答里笼统提到的事件源被 RunLoop 大致分为两类:

    Structure of a run loop and its sources

    右边的 Sources 在官方资料里有详细的分类介绍,其中 Input sources 成分比较复杂,除了 performSelector,其他两个 Port source, Custom source 是一些比较底层的东西,暂时不懂也没有关系(我不懂);而 Timer source 就是 NSTimer。

    RunLoop 可以接受多个 sources,不同场景下可能只需要接受某些事件源的信号,怎么办呢?RunLoop mode 就是为了解决这个问题的,将这些事件源任意组合,在运行 RunLoop 时指定接受哪一个组合的信号,这就是 mode 了,打个比方,RunLoop mode 就像早些年功能手机的模式:静音模式,会议模式,等等。在具体的实现中,做法是先建立模式名称,然后往模式里添加事件源。

    // RunLoopMode 是一个结构体,defaultRunLoopMode 是其一个预定义的 mode 名
    // 这行代码将 timer 添加到这个 mode 下了
    RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
    // run() 这个方法实际上是调用 run(mode:before:),这里的 mode 参数就是 .defaultRunLoopMode
    // 从这行代码开始进入无限循环,到了预定时间点就调用 timer 绑定的方法
    RunLoop.current.run()
    

    RunLoop mode 的设计对多个事件源进行了分组隔离,但也带来一个副作用:要想在某个模式下处理某个事件源,必须把这个事件源明确地添加到这个模式里,这有点繁琐。为了解决这个小问题,引入了commonModes,被添加到这个模式的事件源,可以在所有其他的模式里使用,不必再手动添加一次了。你可能见到这样的技巧,让添加到主线程的 NSTimer 在视图滚动的时候也能触发:

    RunLoop.main.add(timer, forMode: .commonModes)  
    

    视图滚动时主线程的 RunLoop 会切换到UITrackingRunLoopMode,由于 timer 被添加到commonModes,其他任何模式下都会处理这个 timer。

    以上出现的三个 mode: defaultRunLoopMode, UITrackingRunLoopMode, commonModes 就是 iOS 平台的预定义模式。RunLoop 也支持自定义模式,虽然只在run(mode:before:)的文档几个词里提到,还好深入理解RunLoop里明确指出了这点,在run(mode:before:)直接使用新的自定义的 RunLoopMode 即可,就像 Dictionary 那样,使用自定义模式的代码如下:

    let customMode = RunLoopMode.init("CustomMode")
    RunLoop.current.add(timer, forMode: customMode)
    // 这里指定了 RunLoop 的失效时间,而实际上 .distantFuture 也相当于无穷时间了
    RunLoop.current.run(mode: customMode, before: .distantFuture)
    

    自定义模式也能使用添加到commonModes的所有事件源,但是需要处理下才能享受这个待遇:

     //又是 CF 里的方法,使用起来相当不方便;这行代码让 customMode 下也能处理所有添加到 commonModes 下的事件源。
     //这行代码放哪呢?只要在 RunLoop 运行之前就行了。
     CFRunLoopAddCommonMode(RunLoop.current.getCFRunLoop(), CFRunLoopMode.init("CustomMode" as CFString))  
    

    注意: 虽然添加到 commonModes 的事件源可以在所有其他的 mode 处理,但是 RunLoop 本身并不能在 commonModes 下运行,RunLoop.current.run(mode: .commonModes, before: .distantFuture) 是无法启动 RunLoop 的。文档里没提到这点,还是蛮坑的。

    RunLoop 如何切换 mode 呢?这部分看末尾的回答。

    Q: 如何创建一个在后台线程里运行的 NSTimer?

    A: 这篇文章的起因就是我要实现这个需求,当初我是这样达到同样的目的的:直接将 timer 添加到主线程里,然后在绑定的方法里使用 GCD 切换到后台线程。为了比较两种方法里的性能差异,我用下面的方法实现了一个直接在后台线程里运行的 NSTimer。

    DispatchQueue.global().async {
       // sheduledTimer 这个方法将创建的 NSTimer 添加到当前线程的 RunLoop 的 defaultMode 下
       Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
    }
    

    NSTimer 无法触发,即使在代码里手动触发,也只有一次执行。原因在哪里呢?现在我们知道因为这个线程的 RunLoop 没有运行,再添加一行代码即可:

    RunLoop.current.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
    

    然而这样的代码还有一个问题:无法停止那个 timer,RunLoop 不会退出循环,进而这个线程永远不会消失。所以我们需要维持一个对 timer 的引用,并且合适的时间停止它,这样 RunLoop 会退出循环,进而线程也会被回收销毁,另外,由于 timer 会保持对 target 的强引用,为了避免引用循环,target 里应该维持一个弱引用。你可以给线程一个名字,在 timer 的 selector 里添加断点来观察这个线程的调用帧以及它的销毁。

    不过当初的解决过程并不是这样的,我搜索到了这个页面: Run repeating NSTimer with GCD?,一个近6年前的问题,下面唯一的高票答案虽然本身提供了一些替代方向,不过他最大的错误是认为 timer 无法触发的原因是因为 GCD 派发的 thread 没有 RunLoop,完全是胡说八道,并且他对下面另外一个比较接近的答案(添加了RunLoop.current.run())给出了错误的指导意见,让这个答案止步于此。我起初看到这个页面时没有死心,尝试用 NSThread 去解决这个问题,无意中成功了,下面是使用 Thread 的实现:

    func configurateTimerInBackgroundThread(){
        let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
        //这里有个问题,这个方法返回后,thread会被回收销毁吗?
        //不会,除非 RunLoop 退出循环,这里的达成条件是停止timer。
        //查看内存引用图,发现Foundation框架本身会持有新的线程对象,当然
        //在RunLoop退出或者没有RunLoop运行的时候,会及时断开引用,
        //以便对象被回收销毁,不过,上面提到有时候在模拟器里不起作用。
        thread.start()
    }
    
    @objc func addTimer() {
        weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
        // 这行关键代码不知道怎么加上去的,那个页面下接近的那个答案我看到投票是为-1就略过了
        RunLoop.current.run()
    }
    

    既然 NSThread 可以,GCD 没道理不行。抱着这个疑惑,我最终决定还是好好学习下 RunLoop 的官方资料,于是问题解决了。所有的 NSThread 都有对应的 RunLoop(官方资料开头原文是:each thread, including the application’s main thread, has an associated run loop object)。即使是高票答案也不能全信,即使是一个多年经验的工程师,不保持学习是不行的。如果我的文章里有任何错误的地方,请指出来。

    解决了这个问题后,我想起来直接搜索这个问题,然后找到了这个: How do I create a NSTimer on a background thread?,同样古老的问题,可以看到多个基于 GCD 的 答案(有个大神也现身了),有些东西我从来没用过,比如 GCD 版本的 timer。如果你是写 Swift 的,看这些答案会很痛苦,因为 GCD 的 API 在 Swift 里有很多版本,而且很多根本就没文档。

    Q: 如何创建一个常驻后台的线程?

    A: 这是一个很常见的的问题,几乎所有关于 RunLoop 的文章都会拿 AFNetworking 早期版本的代码做范例。来分析下这个需求,要求线程常驻后台以便响应事件,这个需求嘛,从 RunLoop 的设计来看,线程的设计目标之一就是随时响应事件。现在来讨论下怎样实现是最合适的?

    要求线程不退出,那就是保持 RunLoop 不退出循环,让 RunLoop 不退出循环就要求当前运行的模式下持有有效的 sources: Input sources 或者 Timer sources,其中 NSTimer 会定期唤醒线程执行绑定的方法,其实让线程一直保持休眠是最好不过了,只要 source 不触发事件,线程和 RunLoop 就会一直休眠下去,直到被 source 唤醒,那么 Input sources 是比较合适的,Input sources 有三类,其中最合适的是 Port-Based Sources,它的代码最少,AFNetworking 的代码就是这样做的,用 Swift 写就是下面这样:

    // 添加一个 mach port,但啥也不做,RunLoop 会一直等待 mach port 发送消息
    RunLoop.current.add(NSMachPort.init(), forMode: .defaultRunLoopMode)
    RunLoop.current.run(mode: .defaultRunLoopMode, before: .distantFuture)
    

    而另一个 Custom Input Sources 则要多两行代码,而且得用 Core Foundation 框架,示例如下:

    // 像上面的 mach port 一样,只是添加一个占位 input source
    var sourceContext = CFRunLoopSourceContext.init()
    let customSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext)
    CFRunLoopAddSource(RunLoop.current.getCFRunLoop(), customSource, CFRunLoopMode.defaultMode)
    RunLoop.current.run(mode: .defaultRunLoopMode, before: .distantFuture)
    

    深入理解RunLoop 在讲解这个案例时提到:

    RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source

    Observer 是 RunLoop Observer,看名字就知道是干什么的了,它能跟踪 RunLoop 的运行状态,在 Run Loop Observers 里有详细的描述,也有使用范例。实际上,Observer 并不能维持 RunLoop 的运行,它只是用来跟踪 RunLoop 的状态,官方资料在 Configuring the Run Loop 是这样介绍 RunLoop 的运行条件的:

    Before you run a run loop on a secondary thread, you must add at least one input source or timer to it. If a run loop does not have any sources to monitor, it exits immediately when you try to run it.

    写个代码测试下就知道了,在 RunLoop 运行前只添加一个 Observer,运行 RunLoop 后根本不能观察到相关状态,因为 RunLoop 根本就没有启动,run(mode:before:)会返回一个 Bool 值来表示 RunLoop 是否启动成功,用这个方法来启动 RunLoop 就知道结果了。

    上面没有提到线程从哪儿获取,NSThread, GCD, NSOperationQueue? 后两者后基于某些规则维护着一个线程池,提交的 Block 不一定能及时分配到线程,使用 NSThread 就没问题了(当然这时候系统本身的资源很可能就不足了,这又是另外一个问题了);另外,日后若是想给这个提交 Block 的线程发送个任务,需要我们为这个线程维护一个引用(强行找理由)。如果是需要这个线程单独处理任务,还是使用 NSThread 比较好。

    iOS 似乎不鼓励使用NSMachPort,它传递的消息NSPortMessage类只在 macOS 上是公开的;而NSMachPort的替代者NSMessagePort的文档里又建议避免使用,这个类基本上也废弃了。在 RunLoop 响应事件的流程里,Custom Input Sources 优先于 Port-Based Sources,而且在 iOS 里也有直接的应用,比如接下来要讲到的 NSObject 的 performSelector 系列方法。Custom Input Sources 本身我还没有深入了解,不懂。

    Q: NSObject 的 performSelector 系列方法...

    A: 本来假装写成"如何实现的?",但发现这并不是一个好问题,就是用 RunLoop 相关的技术实现的嘛,像 afterDelay 的两个方法,文档里就直接指明使用了 timer。不过随手使用了几个方法后,发现这个系列的方法有很多需要注意的地方。在这系列方法里你可以看到很多深入理解RunLoop里提到的有关代码层次的细节(可通过在 selector 里设置断点来查看帧栈),借此验证下也可以帮助你更好地理解深入理解RunLoop这篇文章,这些细节当然在上面的内容也有,不过放到上面内容就太繁杂了。

    官方资料将 Input sources 细分成三个类别:

    1. Port-Based Sources: 对应深入理解RunLoop里的 Source1,回调函数为__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
    2. Custom Input Sources: 对应 Source0,回调函数为__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    3. Cocoa Perform Selector Sources:就是 NSObject 的 performSelector 系列。

    如下,带 modes 参数的可以指定运行模式,没带的则在.defaultRunLoopMode模式下运行:

    performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:
    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:
    performSelector:withObject:afterDelay:
    performSelector:withObject:afterDelay:inModes:
    

    这里面 afterDelay 的方法基于 Timer,可以算是 Timer sources 一类,而其他的方法基于 Source0,估计官方也不好分类,就把它搁 Input sources 里了。这个系列还有个performSelectorInBackground:withObject:,这个方法并没有被 RunLoop 官方资料归纳到这部分,查看调用帧栈发现这个方法的确没有用到 RunLoop。

    现在我们知道这些方法生效的首要前提当然是线程的 RunLoop 在运行,而且是以方法指定的模式运行,然而线程的 RunLoop 你不主动获取根本不会生成,主线程还好,它的 RunLoop 在应用启动过程中就激活了,而从线程外部是无法获取它的 RunLoop 的,那么有两种用法:一是使用 GCD 或者 NSOperation 中带有 Block 的 API,这样就直接获得了线程所在的环境,由于这些 performSelector 方法本身会给线程添加一个 source,剩下的只需要让 RunLoop 运行即可;二是预先让线程的 RunLoop 运行,使用 NSThread 基于 selector 的 API 或者子类化,像 AFNetworking 那样添加一个 mach port 让 RunLoop 不退出。

    onThread 的方法有一个副作用需要特别注意:排他性。如果 RunLoop 在 onThread 方法指定的模式下运行,它会移除 RunLoop 在这个模式下的所有事件源,主线程除外。这到底是个 Bug 还是个 Feature(确保添加的 selector 能够得到执行,我瞎猜的)不得而知,按说这么重要的事情,文档里应该指出来,居然没有,那这很可能是个 Bug。另外,如果你在自定义模式下运行 RunLoop,并且将这个自定义模式通过CFRunLoopAddCommonMode(_:_:)将其加入.commonModesperformSelector:onThread:withObject:waitUntilDone:也会生效,并且移除添加到commonModes的事件源,不过这种情况下使用带modes参数的API并指定.defaultRunLoopMode的同等代码没有这样的效果。

    于是使用 onThread 方法时,当 selector 执行完毕后,RunLoop 里已经没有事件源了,直接退出循环;一次性执行多个 onThread 方法则会在全部执行完毕后退出 RunLoop。

    深入理解RunLoop 在分析 AFNetworking 的建立一个常驻后台的线程的做法时指出:

    当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

    由于 onThread 方法的特性,这个办法其实只能奏效一次。那么有没有办法可以多次在同一个线程对象上执行这个方法呢?可以的,让线程的 RunLoop 重新启动就可以了,但是 RunLoop 只能从线程的内部获取,怎么解决呢?我们可以利用 RunLoop Observer 为线程实现 RunLoop 自启动的功能,参考下一个回答。

    Q: 如何切换 RunLoop 的 mode?如何重启 RunLoop?

    A: 其实后一个问题是前一个问题的子集。

    RunLoop 的文档和开头给出的官方资料都没有直接提到如何切换 RunLoop 的 mode。一个正常的想法是:先退出当前模式,然后在run(mode:before:)里重新选择模式开始新的循环。

    如何退出 RunLoop 呢?(退出后不再处理这个模式下的事件)

    1. run(mode:before:)里可以指定失效时间,到了指定的时间 RunLoop 会自动退出循环。

    2. RunLoop 运行后会检查当前运行模式下的 Input sources 和 Timer sources,如果 Input sources 的数量为0, Timer sources 里有效的 timer 数量也为0,RunLoop 就会退出循环,在上面这个简单的例子里,只要timer.invalidate()即可。不过文档指出,移除你已知的事件源并不能保证 RunLoop 退出循环,在 macOS 平台下,出于某些原因,系统会自动添加某些事件源,但没有明确指出其他平台会不会这样做。我在Demo里测试时发现在多个模拟器上这样做无法让 RunLoop 退出循环,而在真机上没有问题,但是呢,同样的手法在我其他项目的模拟器上又没有问题。

    3. 还可以强制退出 RunLoop,需要使用 Core Foundation 里的方法:

       CFRunLoopStop(RunLoop.current.getCFRunLoop())
      

      RunLoop 是 CFRunLoopRef 的封装,但是有不少相关的方法没有封装到 RunLoop 类里,必须使用 CF 框架里的方法,很不方便。

    注意:NSThread 在其生命周期内使用的 RunLoop 都是同一个对象,退出后通过RunLoop.current获取的 RunLoop 并不是重新生成的新对象。

    从代码上看,运行 RunLoop 应该是 NSThread 线程入口比如main()里的最后一行代码,不然后面的代码就没什么意义了。那切换模式的代码在哪里处理呢?

    这里就要用到 RunLoop Observer 了,RunLoop 本身并没有提供接口查询它的状态,只能通过添加 observer 来跟踪它的状态。RunLoop Observer 的唯一版本是 Core Foundation 里的 CFRunLoopObserver,有两个方法可以创建 Observer:

    CFRunLoopObserverCreateWithHandler
    CFRunLoopObserverCreate
    

    建议使用带闭包的方法,另外一个方法里捕获变量非常曲折。

    // 放在 main() 里,RunLoop 运行之前
    func addObserver(){
        let observer = CFRunLoopObserverCreateWithHandler(
            kCFAllocatorDefault, //不懂,默认参数就好
            CFRunLoopActivity.allActivities.rawValue, //这个值表示观察所有的状态,由于类型不兼容,只能使用原始值
            true, //这个参数和NSTimer的repeats 参数一样,
            0 // 优先级,如果有多个observer,这个值越小优先级越高)
        { (observer, activity) in
            switch activity {
            case .entry://进入了RunLoop,开始循环检查
            case .beforeTimers://检查timer
            case .beforeSources://检查input source,有事件就处理
            //如果input sources里没有事件发生并且timer的触发点还没到来,进入休眠
            //或者input sources的事件处理完毕并且timer的触发点还没到来,进入休眠
            case .beforeWaiting:
            //被唤醒,准备处理触发唤醒的事件,比如timer的触发时间点到了,
            //或者input sources发来了信号。如果是input sources,从头开始检查一遍
            case .afterWaiting:
            case .exit://已退出 RunLoop
                /.添加 sources,重新(选择模式)启动 RunLoop./
                RunLoop.current.run(mode: aMode, before: aDate)
            default: break
            }
        }
        //Observer需要指定要观察的模式,RunLoop在这个模式下运行时,发送以上通知给这个observer
        //如果想跟踪所有模式,就指定commonModes
        CFRunLoopAddObserver(RunLoop.current.getCFRunLoop(), observer, CFRunLoopMode.defaultMode)
    }
    

    我建议你添加一个 observer 观察 RunLoop 来验证上面的状态变化,特别是主线程的 RunLoop,你可以看到视图开始滚动以及停止时,RunLoop 会退出然后重新进入新的模式(这就是重启了)。通过实际观察,你会发现官方资料里有些地方写的不是那么完善,比如beforeTimers这个通知,下面是最清楚的一条解释:

    When the run loop is about to process a timer.
    

    这个描述看上去是 timer 的触发时间点到了,RunLoop 要开始处理了,「深入理解RunLoop」在 RunLoop 的内部逻辑对 CFRunLoop 的源码进行了解读,也沿用了这个解释;我这里将其解释为检查timer,做个测试,添加一个 mach port 让 RunLoop 不退出,会发现不管有没有 timer,RunLoop 都会给 observer 发送beforeTimers通知,回头再来看RunLoop 的内部逻辑里这个简化版本的代码:

    /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            
    /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
    /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
    if (msg_is_timer) {
        __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
    }
    

    所以我将.beforeTimers解释为检查 timer 是说得过去的,.beforeSources通知同理。

    RunLoop 的处理流程可以这样简单理解:进入 RunLoop 后,开始循环,首先检查 timer(发送.beforeTimers),然后再检查 input sources(发送beforeSources),如果inpt sources有事件发生,就处理,接下来就进入休眠(发送beforeWaiting)。之后如果有 timer 到了触发点,RunLoop 被唤醒(发送afterWaiting),执行 timer 绑定的方法,然后重新开始循环,检查 timer,检查 input source,没事做就休眠等待信号。那么 input sources 怎么处理的呢?因为 input sources 的信号随时都可能来,来了之后,如果 RunLoop 在休眠,唤醒(发送afterWaiting),又开始从头检查,检查 timer,检查 input sources,诶,有事情,处理,处理完了休眠。

    实际上,不退出 RunLoop 直接使用run(mode:before:)里选择新的模式运行也是可以的。官方资料在 Starting the Run Loop 里提到 RunLoop 是可以递归运行的,也就是说 RunLoop 里可以再嵌套一个 RunLoop:

    recursive runloop

    像上面这种正常的嵌套会有以下影响:

    1. .exit通知会出现重复现象,如果依赖.exit通知,注意过滤重复的通知;
    2. 虽然将 observer 添加到commonModes能让 observer 观察所有模式,但是在这种 RunLoop 嵌套的情况下,还是必须再添加一次;

    如果你不按上面的套路出牌,比如在两种模式中来回嵌套,或是同一种模式里嵌套自身,在某些条件下的确是会出问题的。

    相关文章

      网友评论

        本文标题:RunLoop F.A.Q

        本文链接:https://www.haomeiwen.com/subject/xihjzxtx.html