Runloop

325 阅读6分钟

前言

以前对runloop的了解仅仅停留在do-while,或者是在用到NSTimer的时候会稍微使用一下,最多一点就是关于他跟线程之间一一对应的关系,以及主线程的runloop一直开启,子线程需要时唤醒等等,具体的底层实现以及原理还不太清晰,那么runloop到底做了什么?

官网介绍

runloop官方解释

截屏2021-09-16 上午9.59.11.png

翻译:

runloop是与线程相关的基本基础结构的一部分。runloop是一个事件处理循环,用于安排工作和协调传入事件的接收。运行循环的目的是在有工作要做时让线程保持忙碌,在没有工作时让线程进入睡眠状态

运行循环管理不是完全自动的。您仍然必须设计线程代码,以便在适当的时间启动运行循环并响应传入事件。Cocoa and Core Foundation都提供运行循环对象,以帮助您配置和管理线程的运行循环。应用程序不需要显式创建这些对象;每个线程(包括应用程序的主线程)都有一个关联的运行循环对象。但是,只有子线程需要显式运行其运行循环。作为应用程序启动过程的一部分,应用程序框架在主线程上自动设置并运行run循环。

这边简要的翻译大概同网上讲的一样,大致也就是前言的意思,需要注意的就是,子线程是手动去控制它自己的runloop。

截屏2021-09-16 上午10.46.20.png

截屏2021-09-16 上午10.51.45.png

翻译:

运行循环非常像它的名字。它是线程进入的一个循环,用于运行事件处理程序来响应传入的事件。您的代码提供用于实现运行循环的实际循环部分的控制语句——换句话说,您的代码提供驱动运行循环的while或for循环。在循环中,使用run loop对象“运行”事件处理代码,该代码接收事件并调用已安装的处理程序。

运行循环从两种不同类型的源接收事件。输入源传递异步事件,通常是来自另一个线程或不同应用程序的消息。定时器源提供同步事件,这些事件以预定的时间或重复的间隔发生。两种类型的源都使用特定于应用程序的处理程序例程在事件到达时进行处理。

图3-1展示了运行循环的概念结构和各种来源。输入源将异步事件传递给相应的处理程序,并导致runUntilDate:方法(在线程的关联NSRunLoop对象上调用)退出。计时器源将事件交付给它们的处理程序例程,但不会导致运行循环退出。

归纳总结一下Runloop的作用:

1.保持程序(线程)的持续运行。

2.处理各种响应事件。

3.节省cpu资源,该运行的时候运行,该休息的时候休息。

Runloop常用的事务场景

既然Runloop是那种触发类型的事件或者说事务,那么在平时的使用中,比如说更新UI,延迟操作,回调操作,观察者操作,是否都会引起Runloop?

可以通过打印堆栈分析。

block就是 - __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

1631789345322.jpg

NSTimer打印 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

截屏2021-09-16 下午6.27.16.png

主队列打印调起__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

截屏2021-09-16 下午6.29.01.png

点击事件打印 调起__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

截屏2021-09-16 下午6.29.22.png

observe打印 调起__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__

截屏2021-09-16 下午6.39.23.png

Runloop源码结构分析

底层对于do-while的解释,很简单的实现,就是通过result来控制唤醒,不过如果想具体了解,还需要直到它的底层数据结构。

截屏2021-09-16 下午5.29.41.png

currentRunLoop

在获取当前RunLoop的过程中,类似于取值或者是缓存操作,先看有没有,有的话直接返回,没有的话根据当前的线程创建。

截屏2021-09-16 下午5.42.23.png

首先进来判断当前是不是空,默认空就是主线程,如果是主线程,创建了对应loop然后跟线程绑定放到字典中,如果是非主线程,同样的操作,也是根据线程创建loop。具体是CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));如果t没有值,那么loop就是空,空的话就走下面的if判断作子线程的流程创建。

1631786206841.jpg

__CFRunLoopCreate

从定义中可以看到,有点像平时我们所做的关于对model的赋值操作,他的"属性"包括了一些常见的modes,items等等,并且他们内部都是一个集合,也就是说可以多个存在。item可以理解成我们所操作执行的事务,而mode就是管理这些事务的执行方式。

截屏2021-09-16 下午6.10.18.png

截屏2021-09-16 下午6.14.23.png

在常规的操作中,经常会用到add,类似于把一个事务放到对应的mode中执行,那么一个mode只能放一个事务吗?很明显不是,我们的事务有很多种,经常操作的就是如果是默认的mode,我们可能往里加了很多事务,并不会报错。

截屏2021-09-16 下午6.15.51.png

截屏2021-09-16 下午6.20.07.png

mode管理原理

NSTimer

现在我们常规也就是把事务和mode加到Runloop中,但是具体内部做了什么操作还不太清楚,分析思路可以是由表及里的,可以从NSTimer为例,正常分两步,第一步创建,第二步run。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

CFRunLoopAddTimer

首先对传进来的参数进行异常判断,包括runloop和timer,然后判断当前的名字是不是kCFRunLoopCommonModes集合类型,如果是这种mode,把timer事务赋值给model里的item。如果不是common类型,根据name取到对应的mode,如果不存在就创建一个,并且给mode里的timer赋值,拿到了mode以后,在具体对这个mode里的其他成员赋值。 1631848383979.jpg

CFRunLoopRun

创建成功以后,调起RunLoopRun,这里面会调CFRunLoopRunSpecific方法。

void CFRunLoopRun(**void**) {    /* DOES CALLOUT */

    int32_t result;

    do  {

        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

        CHECK_FOR_FORK();

    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);

}

在kCFRunLoopEntry和kCFRunLoopExit中间执行了__CFRunLoopRun方法,

1631859783719.jpg

其实本次关心的只是timer,所以只需要根据timer去找就行了,这里面也包括了source和observe,好家伙,看注释就知道,必然会调用__CFRunLoopDoTimers,因为在整个过程中mode可能不止一个timer事务,所以需要遍历所有的timer放到数组中,然后分别执行__CFRunLoopDoTimer,在整个过程中,线程都是被锁住的,为了保证线程安全,最终会通过__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__发起回调。

所有的不管是source还是timer还是observe(可以叫做事务),都会被保存在item中,而所有的这些事务都依赖于mode并在runloop中执行。

截屏2021-09-17 下午2.31.05.png

1631861113537.jpg

同样的如果是source还是observe,刚开始CFRunLoopAddObserver或者CFRunLoopAdd也会判断是不是kCFRunLoopCommonModes做赋值操作,然后__CFRunLoopRun的时候走对应的__CFRunLoopDoSource或者__CFRunLoopDoObservers,最后从相应的方法中发起回调,也就是最堆栈中最终看到的打印方法。

什么时候需要用到RunLoop

1631862987043.jpg

1. 使用端口或自定义输入源与其他线程通信。
2. 在线程上使用计时器。
3. 使用任何performSelector…方法
4. 让线程继续执行周期性任务。

后记

Runloop就是一种为了不让程序在执行完以后立刻结束而保持运行状态的机制。在创建的时候会根据对应的线程创建一条runloop,而这个runloop的内部成员变量比如说mode,item(source、timer等等事务)会被一同传入,在run的时候进入循环找到对应的mode去影响处理,处理结束以后,发起相应的回调给result从而跳出循环。