RunLoop小结

作者: 倚楼听风雨wing | 来源:发表于2017-12-27 17:58 被阅读67次

一、先来几个问题

1.在viewDidLoad中添加3个循环的timer,这3个timer都会访问并且改变一个可变数组,请问会有什么问题吗?
2.如何获取子线程runLoop,如何让一个runLoop跑起来,runLoop和线程之间的关系
3.如何结束一个runLoop
4.如何用runLoop来做点事,让你的程序高效起来

二、为什么要有RunLoop

1.能够让程序一直处于运行状态,随时能够和用户交互,并且在没有交互的时候休眠,以节省性能
2.调用解耦(Message Queue) 把用户对界面的操作消息放到消息队列中,然后通过运行循环到消息队列中把消息取出来通知对应需要处理的界面操作或者逻辑等。

三、RunLoop in Cocoa

Foundation
  NSRunLoop [不是线程安全]
Core Foundation (跨平台)
  CFRunLoop [线程安全]

NSRunLoop纯粹是对CFRunLoop的一层面向对象的封装,在使用的时候需要考虑线程安全问题
在我们的日常开发中都有哪些是用到了RunLoop的呢,例如:
NSTimer UIEvent Autorelease
NSObject (NSDelayedPerfroming)
CADisplayLink CATransition CAAnimation
dispatch_get_main_queue() 等等...
或者以前的AFNetworking中的NSURLConnection的代理的回调就是在一个置顶线程的RunLoop中处理的

四、RunLoop机制

runLoop.png

1.Souce是RunLoop的数据源抽象类(Protocol),RunLoop定义了两个版本的Souce:

  • Souce0:处理App内部事件,App自己负责管理(触发),如UIEvent、CFSocket【Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时你需要先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWeakUp(runloop)来唤醒RunLoop,让其处理事件】
  • Source1:由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort 【Source1包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程】

2.AutoreleasePool对对象的释放也是在RunLoop的两次Sleep之间进行Pop和Push将这次循环中产生的Autorelease对象释放
3.RunLoop接收来自两种不同类型源的事件:

  • 输入源:提供异步事件,通常是来自另一个线程或是不同应用程序的消息
  • 定时源:同步事件,发生在预定时间或者是重复间隔

4.RunLoop在同一时间只能并且必须在一种特定的Mode下跑,如果需要更换Mode则需要停止当前的loop然后在指定的Mode下开启loop。Mode是iOS滑动顺畅的关键,有以下几种:

  • NSDefaultRunLoopMode 默认状态,空闲状态
  • UITrackingRunLoopMode 滑动ScrollView时
  • UIInitializationRunLoopMode 私有的,App启动时
  • GSEventReceiveRunLoopMode 接收系统内部事件的Mode,通常用不到
  • NSRunLoopCommonModes 占位Mode,没有实际作用【但我们老用】

更多苹果内部:Mode:http://iphonedevwiki.net/index.php/CFRunLoop
平常我们把timer添加到RunLoop的defaultMode中发现滚动scrollView的时候timer就不工作了就是因为这个时候的mode切换成UITrackingRunLoopMode了。如果想要继续工作的话,只需要把timer添加到commonModes中即可,这个时候不论应用程序在什么mode的情况下都会执行timer。从这里我们也可以看出,如果想要保持用户滑动的流畅性,除了把一些耗时操作放到子线程去做还可以把一些必须在主线程执行的操作放到defaultMode中去执行。例如:

private func test() {
  let img = UIImage(named: "xxx.png")
  perform(#selector(setImg(_:)), with: img) // 默认就是defaultMode
}

@objc private func setImg(_ img: UIImage) {
   imageView.image = img
}

5.事件运行循环顺序

1.通知观察者已经输入了运行循环。
2.通知观察者,任何准备好的计时器即将开火。
3.通知观察者,任何不是基于端口的输入源都将被触发。
4.触发任何可以触发的非基于端口的输入源。
5.如果基于端口的输入源已准备就绪并正在等待触发,请立即处理该事件。转到第9步。
6.通知观察者线程即将睡眠。
7.使线程进入睡眠状态,直到发生以下事件之一:
[7.1]一个事件到达一个基于端口的输入源。
[7.2]计时器启动。
[7.3]为运行循环设置的超时值已过期。
[7.4]运行循环明确地被唤醒。
8.通知观察者线程刚刚醒来。
9.处理未决事件。
[9.1]如果用户定义的定时器触发,则处理定时器事件并重新启动循环。转到第2步。
[9.2]如果输入源被触发,则交付事件。
[9.3]如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
10.通知观察者运行循环已经退出。

基于这些指定的顺序苹果给出了特定的api可以检测到runloop的所处的运行状态

/* Run Loop Observer Activities */
public struct CFRunLoopActivity : OptionSet {

    public init(rawValue: CFOptionFlags)
    // 1.进入runloop
    public static var entry: CFRunLoopActivity { get }
    // 2.即将处理定时源
    public static var beforeTimers: CFRunLoopActivity { get }
     // 3.即将处理输入源
    public static var beforeSources: CFRunLoopActivity { get }
     // 4.即将进入睡眠状态
    public static var beforeWaiting: CFRunLoopActivity { get }
     // 5.被唤醒
    public static var afterWaiting: CFRunLoopActivity { get }
     // 6.退出runloop
    public static var exit: CFRunLoopActivity { get }
     // 7.以上所有状态集合
    public static var allActivities: CFRunLoopActivity { get }
}

五、Coding

1.主线程的RunLoop是默认开启的,如果是我们自己创建的子线程,默认是没有开启RunLoop的,只有我们去获取的时候才会以懒加载的形式创建。注意RunLoop需要在线程内部获取,一旦开启线程将不会被释放。这里可能有人会问为什么下面要用while循环的方式不停的调用run方法,这是苹果推荐的一种RunLoop开启方式,是为了方便我们停止RunLoop。如果只是调runloop.run()其实内部也是一个while循环在不停的调用runloop.run(mode: .defaultRunLoopMode, before: Date.distantFuture)

let thread = Thread {
    let runloop = RunLoop.current
    runloop.add(Port(), forMode: .defaultRunLoopMode)
    self.rl = CFRunLoopGetCurrent()
    while (self.isRun) {
        runloop.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
    }
    
    runloop.run()
}
thread.start()

结束一个正在运行的RunLoop

CFRunLoopStop(rl)
thread?.cancel()
isRun = false

2.给RunLoop添加观察者

let rl = CFRunLoopGetMain()
let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { (observer, activity) in
    if activity == .allActivities {
        print("allActivities")
    } else if activity == .beforeTimers {
        print("beforeTimers")
    } else if activity == .beforeSources {
        print("beforeSources")
    } else if activity == .beforeWaiting {
        print("beforeWaiting")
    } else if activity == .afterWaiting {
        print("afterWaiting")
    } else if activity == .entry {
        print("entry")
    } else if activity == .exit {
        print("exit")
    }
}
CFRunLoopAddObserver(rl, ob, .defaultMode)
observer = ob

一般来说我们只需要观察beforeWaiting在一次RunLoop结束的时候做点事情就可以了
3.移除RunLoop中的observer,这里多说一点,在swiftCore Foundating中的内容也不用我们手动释放了,系统帮我们处理了,所以我们不用主动释放observer

 CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .defaultMode)

完整代码:

import UIKit

class ViewController: UIViewController {

    private var observer: CFRunLoopObserver?
    private var thread: WYThread?
    private var rl: CFRunLoop!
    private var isRun = true
    
    private func startRunLoop() {
        thread = WYThread(block: { [unowned self] in
            let runloop = RunLoop.current
            runloop.add(Port(), forMode: .defaultRunLoopMode)
            self.rl = CFRunLoopGetCurrent()
            while (self.isRun) {
                runloop.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
            }
            
            runloop.run()
        })
        
        thread?.name = "wy_test"
        thread?.start()
    }
    
    private func stopRunLoop() {
        CFRunLoopStop(rl)
        thread?.cancel()
        isRun = false
    }
    
    private func addRunLoopObserver() {
        let rl = CFRunLoopGetMain()
        let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { (observer, activity) in
            if activity == .allActivities {
                print("allActivities")
            } else if activity == .beforeTimers {
                print("beforeTimers")
            } else if activity == .beforeSources {
                print("beforeSources")
            } else if activity == .beforeWaiting {
                print("beforeWaiting")
            } else if activity == .afterWaiting {
                print("afterWaiting")
            } else if activity == .entry {
                print("entry")
            } else if activity == .exit {
                print("exit")
            }
        }
        CFRunLoopAddObserver(rl, ob, .defaultMode)
        observer = ob
    }

    private func removeRunLoopObserver() {
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .defaultMode)
    }
}

相关文章

  • NSRunLoop

    【iOS程序启动与运转】- RunLoop个人小结 RunLoop总结:RunLoop的应用场景(三) 走进Run...

  • runLoop小结

    1、runloop和线程有什么关系? runloop:正如其名,loop表示某种循环,和run放在一起就是表示一直...

  • Runloop小结

    作用: 是一个死循环,保持线程活着,有活干活,没活休眠。而不会让一个线程一个任务执行完了,马上释放掉。 Runlo...

  • RunLoop小结

    一、先来几个问题 1.在viewDidLoad中添加3个循环的timer,这3个timer都会访问并且改变一个可变...

  • runloop 小结

    OC的两大核心runtime和runloop runloop简介 runloop本质上是一个do-while循环,...

  • RunLoop入门学习补充资料

    本文是对iOS RunLoop入门小结一文的资料补充 1.RunLoop运行逻辑 以下是伪代码1,摘自https:...

  • 浅谈RunLoop

    由于各种原因,需要对RunLoop进行研究,通过阅读大神的文章对RunLoop也有了一些了解,在这里进行下小结。 ...

  • iOS RunLoop小结

    原文链接:http://yupeng.fun/2019/05/30/runloop/ RunLoop 简介 Run...

  • iOS RunLoop 小结

    RunLoop,这个东西被大牛的都说烂了(很透彻了),但是为了向大牛迈进的我(吊丝),也忍不住要说一说。 概念:看...

  • runloop知识小结

    什么是runloop? 一个runloop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。使用r...

网友评论

    本文标题:RunLoop小结

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