美文网首页iOS开发
多线程之4-RunLoop

多线程之4-RunLoop

作者: 栋柠柒 | 来源:发表于2021-11-24 22:21 被阅读0次

    什么是RunLoop

    顾名思义,RunLoop就是在‘跑圈’,其本质是一个do
    while循环。RunLoop提供了这么一种机制,当有任务处理时,线程的RunLoop会保持忙碌,而在没有任何任务处理时,会让线程休眠,从而让出CPU。当再次有任务需要处理时,RunLoop会被唤醒,来处理事件,直到任务处理完毕,再次进入休眠。

    为什么会有这样一种机制呢?

    • 大家都知道,一个线程的生命周期分为创建、就绪、运行、阻塞和死亡,当一个线程上的任务执行完毕,这个线程就会死亡,所占的资源也就会被回收,当频繁开启异步操作的时候,就意味着频繁创建和销毁线程,创建和销毁线程是要消耗一些性能的,所以RunLoop 可以在一定程度上解决这个问题。
    • 按常理来讲,线程中的任务执行完毕,线程要被释放,但是却有一个特例,Thread.main,主线程好像一直都存在,不论我们什么时候使用主线程执行代码,主线程都在那里等着你。其实很好理解,如果主线程死亡了,APP 不就没了吗?所以这里也有runLoop 的功劳。

    如果继续问:RunLoop是怎么实现休眠机制的?RunLoop都可以处理哪些任务,又是怎么处理的呢?RunLoop在iOS系统中都有什么应用呢?这些东西在写代码过程中可能不会直接用到,但是了解他的原理,对我们平时开发避坑是有一定作用的。

    RunLoop 的结构组成

    RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):


    image.png

    RunLoop 的结构如下


    image.png

    RunLoop提供了如下功能(括号中CF**表明了在CF库中对应的数据结构名称):

    RunLoop(CFRunLoop)使你的线程保持忙碌(有事干时)或休眠状态(没事干时)间切换(由于休眠状态的存在,使你的线程不至于意外退出)。
    RunLoop提供了处理事件源(source0,source1)机制(CFRunLoopSource)。
    RunLoop提供了对Timer的支持(CFRunLoopTimer)。
    RunLoop自身会在多种状态间切换(run,sleep,exit等),在状态切换时,RunLoop会通知所注册的Observer(CFRunLoopObserver),使得系统可以在特定的时机执行对应的操作。相关的如AutoreleasePool 的Pop/Push,手势识别等。
    RunLoop在run时,会进入如下图所示的do while循环:

    image.png
    需要注意的就是黄色区域的消息处理中并不包含source0,因为它在循环开始之初就会处理,整个流程其实就是一种Event Loop的实现,其他平台均有类似的实现,只是这里叫做Runloop。但是既然RunLoop是一个消息循环,谁来管理和运行Runloop?那么它接收什么类型的消息?休眠过程是怎么样的?如何保证休眠时不占用系统资源?如何处理这些消息以及何时退出循环?

    尽管CFRunLoopPerformBlock在上图中作为唤醒机制有所体现,但事实上执行CFRunLoopPerformBlock只是入队,下次RunLoop运行才会执行,而如果需要立即执行则必须调用CFRunLoopWakeUp。

    RunLoopMode

    Runloop总是运行在某种特定的CFRunLoopModeRef下(每次运行__CFRunLoopRun()函数时必须指定Mode)。而通过CFRunloopRef对应结构体的定义可以很容易知道每种Runloop都可以包含若干个Mode,每个Mode又包含Source/Timer/Observer。每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为** _currentMode**,当切换Mode时必须退出当前Mode,然后重新进入Runloop以保证不同Mode的Source/Timer/Observer互不影响。

    有些难以理解?来看一下创建Timer 的代码,类比一下

    let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)
    

    这个就是在主线程的runLoop里面加了个timer,设置mode为 common。

    系统默认提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,需要切换到对应的Mode时只需要传入对应的名称即可。前者是系统默认的Runloop Mode,例如进入iOS程序默认不做任何操作就处于这种Mode中,此时滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作(除非你将其他Source/Timer设置到UITrackingRunLoopMode下)。
    但是对于开发者而言经常用到的Mode还有一个kCFRunLoopCommonModes(NSRunLoopCommonModes),其实这个并不是某种具体的Mode,而是一种模式组合,在iOS系统中默认包含了
    NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes**组合)。

    RunLoop和线程的关系

    Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装(可以参见Kernel Programming Guide),和NSThread一一对应(而NSThread是一套面向对象的API,所以在iOS开发中我们也几乎不直接使用pthread)。

    image.png
    苹果开发的接口中并没有直接创建Runloop的接口,如果需要使用Runloop通常CFRunLoopGetMain()和CFRunLoopGetCurrent()两个方法来获取。
    只有当我们使用线程的方法主动get Runloop时才会在第一次创建该线程的Runloop,同时将它保存在全局的Dictionary中(线程和Runloop二者一一对应),默认情况下线程并不会创建Runloop(主线程的Runloop比较特殊,任何线程创建之前都会保证主线程已经存在Runloop),同时在线程结束的时候也会销毁对应的Runloop。
    iOS开发过程中对于开发者而言更多的使用的是Runloop,它默认提供了三个常用的run方法:
    open func run()
    
    open func run(until limitDate: Date)
    
    open func run(mode: RunLoop.Mode, before limitDate: Date) -> Bool
    
    • run方法对应上面CFRunloopRef中的CFRunLoopRun并不会退出,除非调用CFRunLoopStop();通常如果想要永远不会退出RunLoop才会使用此方法,否则可以使用runUntilDate。
    • runMode:beforeDate:则对应CFRunLoopRunInMode(mode,limiteDate,true)方法,只执行一次,执行完就退出;通常用于手动控制RunLoop(例如在while循环中)。
    • runUntilDate:方法其实是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),执行完并不会退出,继续下一次RunLoop直到timeout.

    RunLoop应用

    Timer

    前面提到Timer Source作为事件源,事实上它的上层对应就是Timer(其实就是CFRunloopTimerRef)这个开发者经常用到的定时器(底层基于使用mk_timer实现),甚至很多开发者接触RunLoop还是从Timer开始的。其实Timer定时器的触发正是基于RunLoop运行的,所以使用Timer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(Timer提供了一个tolerance属性用于设置宽容度,如果确实想要使用Timer并且希望尽可能的准确,则可以设置此属性)。
    Timer的创建通常有两种方式,尽管都是类方法,一种是init(xxxxx),另一种scheduedTimer(XXX)。

    public /*not inherited*/ init(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool)
    
        open class func scheduledTimer(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool) -> Timer
    
        
        public /*not inherited*/ init(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
    
        open class func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer
    
        
        /// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
        /// - parameter:  timeInterval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
        /// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
        /// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
        @available(iOS 10.0, *)
        public /*not inherited*/ init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
    
        
        /// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
        /// - parameter:  ti    The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
        /// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
        /// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
        @available(iOS 10.0, *)
        open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer
    

    二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的Timer是无法正常工作的。例如下面的代码中如果timer2不加入到RunLoop中是无法正常工作的。同时注意如果滚动UIScrollView(UITableView、UICollectionview是类似的)二者是无法正常工作的,但是如果将RunLoop.Mode.default改为RunLoop.Mode.common则可以正常工作,这也解释了前面介绍的Mode内容。

    class ViewController1: UIViewController {
        var counter = 1
        var timer1: Timer?
        override func viewDidLoad() {
            self.view.backgroundColor = UIColor.white
            
            self.timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
            
            let timer2 = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
            RunLoop.main.add(timer2, forMode: .default)
        }
        
        @objc func timerRun(_ timer: Timer) {
            if timer == timer1 {
                print("timer1 print: \(counter)")
            } else {
                print("timer2 runing\(counter)")
            }
            counter += 1
        }
        deinit {
            debugPrint("\(self.classForCoder) deinit!!!!")
        }
    }
    

    然后我们退出这个vc,居然发现,deinit 语句并没有输出,到底是人性的缺失还是道德的沦丧?

    对于普通的对象而言,执行完viewDidLoad方法之后(准确的说应该是执行完viewDidLoad方法后的的一个RunLoop运行结束)timer2应该会被释放,但事实上timer2并没有被释放。原因是:为了确保定时器正常运转,当加入到RunLoop以后系统会对Timer执行一次retain操作。

    在创建Timer1 和 timer2时指定了target为self,这样一来造成了timer1和timer2对ViewController1有一个强引用。解决这个问题的方法通常有两种:一种是将target分离出来独立成一个对象(在这个对象中创建NSTimer并将对象本身作为Timer的target),控制器通过这个对象间接使用Timer;另一种方式的思路仍然是转移target,只是可以直接增加Timer扩展(分类),让Timer自身做为target,同时可以将操作selector封装到block中。后者相对优雅,也是目前使用较多的方案(目前有大量类似的封装,例如:NSTimer+Block)。显然Apple也认识到了这个问题,如果你可以确保代码只在iOS 10下运行就可以使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。
    当然使用上面第二种方法可以解决控制器无法释放的问题,但是会发现即使控制器被释放了两个定时器仍然正常运行,要解决这个问题就需要调用Timer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。修改后的代码如下:

    class ViewController1: UIViewController {
        var counter = 1
        var timer1: Timer?
        var timer2: Timer?
        override func viewDidDisappear(_ animated: Bool) {
            timer1?.invalidate()
            timer2?.invalidate()
        }
        override func viewDidLoad() {
            self.view.backgroundColor = UIColor.white
            
            self.timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
            
            let timer2 = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
            RunLoop.main.add(timer2, forMode: .default)
            self.timer2 = timer2
        }
        
        @objc func timerRun(_ timer: Timer) {
            if timer == timer1 {
                print("timer1 print: \(counter)")
            } else {
                print("timer2 runing\(counter)")
            }
            counter += 1
        }
        deinit {
            debugPrint("\(self.classForCoder) deinit!!!!")
        }
    }
    
    timer1 print: 1
    timer2 runing2
    timer1 print: 3
    timer2 runing4
    timer1 print: 5
    timer2 runing6
    timer1 print: 7
    timer2 runing8
    "ViewController1 deinit!!!!"
    

    NSURLSession

    (pending 待更新)

    GCD和RunLoop的关系

    在RunLoop的源代码中可以看到用到了GCD的相关内容,但是RunLoop本身和GCD并没有直接的关系。当调用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作是全部由libDispatch驱动的。

    RunLoop 的其他使用

    RunLoop包含多个Mode,而它的Mode又是可以自定义的,这么推断下来其实无论是Source1、Timer还是Observer开发者都可以利用,但是通常情况下不会自定义Timer,更不会自定义一个完整的Mode,利用更多的其实是Observer和Mode的切换。
    例如很多人都熟悉的使用perfromSelector在默认模式下设置图片,防止UITableView滚动卡顿([[UIImageView allocinitWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。还有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。再有老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。

    相关文章

      网友评论

        本文标题:多线程之4-RunLoop

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