Runloop的源码阅读(一)

165 阅读4分钟

RunLoop是什么

runloop是什么?说白一点,他就是一个循环,当然我们代码while也可以让系统循环,但是他会导致CPU进入忙等状态,如果在主线程,还会卡死。而runloop就比较高级一点,他是一种“闲”等待,没事的时候会进入休眠,当有事件触发的时候,runloop会从休眠唤醒,执行任务。

源码分析

runloop的源码在这里下载

  1. 首先要理解一个概念,线程与runloop的关系,线程和runloop是一一对应的关系,甚至,它们在苹果的底层是用字典的key-value的形式来存储的,有朋友可能不信,我们来看看源码。 我们先创建一个线程,线程里面创建一个runloop:
thread = MyThread(target: self, selector: #selector(doSomething), object: nil)
thread?.start()

@objc func doSomething() {
        print("start")
        let runloop = RunLoop.current
        print(runloop)
        runloop.run()
        print("end")
    }

我们打开源码工程,找到Runloop.swift

image.png 我们可以找到current的属性,他是调用底层的_CFRunLoopGet2方法的,我们定位进去发现只能看到这个方法的定义,而看不到实现,怎么办呢?

image.png

这个时候我们就可以看OC的源码了,swift没有,我们就参考下OC的。 我们打开CFRunLoop.c文件,可以看到CFRunloopGetCurrent实际上就是调用CFRunloopGet0方法。

image.png 可以看到CFRunloopGet0的方法,他是如果没有当前runloop,先创建一个runloop,然后放到CFDictionary里面,然后再取出来。

image.png

这就证明了,runloop和线程是一一对应的关系,并且,他们是用字典来存起来的。

  1. 接下来,我们运行一下刚刚的代码,一看运行,发现,runloop并没有起到了线程阻塞循环的作用,依然会打印end,这是为什么呢?

image.png 这就涉及到我们runloop的组成了。runloop由以下几点组成:

  • Modes 查看源码我们发现,runloop苹果公开的只有2种模式,defaultcommon

image.png

  • sources0:非基于内核事件手动触发的,比如用户触摸等。
  • sources1:基于machport的内核事件,主动唤醒触发的。
  • observers:用于监听runloop的状态的监听者。
  • timers:定时任务。

很明显,我们上面的运行打印可以看到,如果只创建一个current的runloop,它的sources0、sources1、timers等都是nil,而结果就是,runloop没有起到阻塞线程的效果,所以我们就可以猜想,是不是sources和timer,至少一个要有呢?不然它怎么去处理事件?

  1. runloop增加sources有2个方法:

image.png 我们增加一个port试试:

@objc func doSomething() {
        print("start")
        let runloop = RunLoop.current
        runloop.add(NSMachPort(), forMode: .common)
        print(runloop)
        runloop.run()
        print("end")
    }

我们发现,end没有打印了,并且,sources有了

image.png 这就印证了我们刚刚的想法,要想runloop起效果,还是得怎家sources,刚刚的add,还有timer的方法,有兴趣的自己可以试试。

  1. 为了验证我们的线程在runloop的加持下,是否一直保活,我们添加一个方法,这个方法就是在我们创建的线程里面执行多次任务,看看是否一直是那个线程执行。
@IBAction func performTask(_ sender: Any) {
        if let thread = thread {
            perform(#selector(addSubThreadAction), on: thread, with: nil, waitUntilDone: false)
        }
    }
    
    
@objc func addSubThreadAction() {
        print("启动runloop后,\(String(describing: RunLoop.current.currentMode))")
        print("---------子线程任务开始\(String(describing: Thread.current))")
        for i in 0...3 {
            Thread.sleep(forTimeInterval: 1.0)
            print("-------子线程任务\(i)")
        }
        print("\(String(describing: Thread.current))----------子线程任务结束")
    }

运行发现,我们可以多次地启动任务,并且都是在同一个线程里面执行,这就证明,该线程在runloop的加持下,一直活着。

image.png

  1. 如果我们想停止runloop怎么做呢?
CFRunLoopStop(CFRunLoopGetCurrent())

这个代码能帮到我们,我们试试? image.png 结果发现,end没有打印,这是为什么呢? 翻查文档我们发现:原来runloop的run方法,启动之后,就无法终止。

image.png 而runloop的启动其实有好几种方式:

image.png 我们翻看下源码发现其实三个方式,最终调用都是

public func run(mode: RunLoop.Mode, before limitDate: Date) -> Bool

image.png

再结合文档的提示,我们把代码改一下:

@objc func addSubThreadAction() {
        print("启动runloop后,\(String(describing: RunLoop.current.currentMode))")
        print("---------子线程任务开始\(String(describing: Thread.current))")
        for i in 0...3 {
            Thread.sleep(forTimeInterval: 1.0)
            print("-------子线程任务\(i)")
        }
        print("\(String(describing: Thread.current))----------子线程任务结束")
        isStop = true
        CFRunLoopStop(CFRunLoopGetCurrent())
        thread = nil
    }
    
    
    @objc func doSomething() {
        print("start")
        let runloop = RunLoop.current
        runloop.add(NSMachPort(), forMode: .common)
        print(runloop)
        while !isStop {
            runloop.run(mode: .default, before: Date.distantFuture)
        }
        print("end")
    }
    

运行后发现,end打印了。

image.png

未完待续.......