源码解读RunLoop,理解以后面试必加分

2,745 阅读6分钟

继上一篇博客

中高级iOS必备知识点之RunLoop(一)

RunLoop的状态

首先我们去RunLoop的源码去查看它有几种状态,如下图:

它一共有上面的这几种个状态

/* Run Loop Observer Activities */

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry = (1UL << 0), //即将进入loop

kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer

kCFRunLoopBeforeSources = (1UL << 2), //即将处理source

kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠

kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒

kCFRunLoopExit = (1UL << 7), //即将退出loop

kCFRunLoopAllActivities = 0x0FFFFFFFU //所有模式

};

现在我们来试一试,怎么去监听RunLoop的状态,是这样,RunLoop的监听模式没有OC的代码,我们可以用C语言代码来实现,如下:

从执行的结果来看,确实是这么多,点击事件是在source0执行,所以看log日志也是很清楚,

接下来我们看一下定时器唤醒RunLoop.我们知道kCFRunLoopBeforeWaiting是休眠睡觉,而kCFRunLoopAfterWaiting是唤醒休眠,我们看一下定时器是不是唤醒休眠,请看下面的代码:

从输出结果来看,确实是唤醒休眠.执行block

证明模式切换会退出RunLoop,再重新进入RunLoop

由上面的监听,我们是很容易可以证明这个结果吧?我们看一下代码,这次用上面说的另一种创建observe的方法,请看下图:随便创建一个可以滚动的view,比如我创建的Scrollerview.

当在滚动的时候,我们明显可以看到2种模式在切换,而且RunLoop也是需要退出重新进入才会切换到新的模式.

深入理解RunLoop的执行流程

网上的答案很多,这次我们从源码解析,一步一步的查看RunLoop的执行流程到底是怎么样的:因为源码比较抽象,是纯c语言的,不像我们之前的有c++源码比较好懂一点,那我们怎么找runloop开始的函数呢?很容易,我们知道点击也是通过runloop来处理的,那我们直接看点击事件的函数调用栈就知道入口了,请看下图:

从上面的代码可以很清楚的看出来是调用了CFRunLoopRunSpecific这个函数.那我们就去源码搜索这段代码很容易就搜索到.请看下面:

一看上面的源码,我们发现执行了非常多,你看绿色里面有锁,有多线程等等,所以我们没有必要研究得很透彻,浪费时间也没有意义,我们只要把大致流程捋顺了就行了,所以我们只要看关键代码,我只展示我们要看的关键代码如下:

接下来我们就去看__CFRunLoopRun源码里面到底执行了哪些操作,接下来的源码是我精简了的,大家可以参照源码看一下,因为里面东西非常多,我们没有必要全部了解,只要知道它的执行流程即可.请看下图:

如果条件不成立,就会设置返回值,到时候直接退出RunLoop,上面的流程相信我备注的已经非常清楚,对照一下源码看一下,你会印象更加深刻.下面我们用文字总结一下流程如下:

再放一个文字版的:

这个和源码基本一模一样的流程,可以参照一下,源码还是比较抽象的

接下来再看一个知识点.

__CFRunLoopDoBlocks、__CFRunLoopDoObservers、__CFRunLoopDoTimers里面执行了什么

其中RunLoop做了这么几件大事中比如blocks,timers,observers,我们看下里面具体是执行了什么,继续看源码,请看下图,比如我们就看__CFRunLoopDoSources0:

其实最终处理的就是这个:

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

我们再看下之前的一个截图

它上面两个处理的函数是一摸一样,也进一步的证明了,触摸事件是source0在处理.

定时器也是一样的道理,大家可以自己尝试一下.

理解线程休眠具体是什么意思?

RunLoop的线程休眠是真的休眠,它是不会占用任何cpu的资源,完全休息.它和white(1)这种还是有本质的区别,white(1)它是一直在执行,转成汇编会有几条指令,一直在执行,一直占用cpu资源,比如我们想做优化,那RunLoop的这种休眠模式是不是更节约cpu资源,说白了更省电些.就是如下源码的位置:

就是执行到当前代码,就不会往下走了,不会接着执行下面的代码,就会堵在这里,一旦别人唤醒它了,它才会接着往下执行.我们可能奇怪它是怎么做到的这种休眠?

我们看一下里面具体是怎么实现的,其实里面是执行了非常内核的函数叫做:mach_msg,我们可以看下

其实IPA可以分为内核层面的IPA:它是非常非常底层的,是操作系统层面的,它可以让线程休眠,也是真的休眠,不占用任何cpu资源,一般是不开放给我们程序员使用的,因为是比较内核的,给程序员使用危险也比较大,而应用层面的IPA,都是网络请求什么,页面什么.

所以mach_msg,我们面试的时候可以答出这个函数.

RunLoop休眠实现的原理

就是用户态和内核态的切换,用户态发消息,内核态休眠,再被唤醒,用户态处理消息.

所以如果面试官问:RunLoop里面线程阻塞是怎么样的?我们千万不能答,里面是个死循环.就是上面刚刚说的那些.

RunLoop与NSTimer的故事

相信我们在开发中遇到的次数是非常的多,这里稍微提一下.

比如我们常见的mode模式是有2种

1.KCFRunLoopDefaultMode (NSDefaultRunLoopMode):App的默认Mode,通常是主线程是在这个Mode下运行

2.UITrackingRunLoopMode : 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响

我们在把Timer添加到runloop的时候,直接传入通用模式即可NSRunLoopCommonModes即可.

主要说一下它是怎么个原因.

我们看一下上个博客说的RunLoop的结构:

我们可以理解为传入NSRunLoopCommonModes,就是把这2种模式,放入_commonModes里面,也就是timer可以_commonModes数组中的模式下进行工作.注意NSRunLoopCommonModes这个不是一种模式哈.

而_commonModeItems里面存放的都是可以在_commonModes模式下工作的,比如刚刚的timer.



接下来我会继续介绍'线程保活'知识点.

如果觉得我写得对您有所帮助,请关注我,我会持续更新😄