首发于 个人博客
RunLoop是什么
runloop 是什么?Runloop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠
开始之前,先想想这几道面试题
- runloop和线程的关系?
- timer 与 runloop 的关系?
- 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
- runloop内部实现逻辑?
- runloop 是怎么响应用户操作的, 具体流程是什么样的?
- 说说runLoop的几种状态
- runloop的mode作用是什么?
源码分析
回答问题之前,我们先看源码
RunLoop 源码 https://opensource.apple.com/tarballs/CF/ 里面数字最大的是最 新的,下载最新的 CF-1153.18.tar.gz(写本文时候的最新版本)
查看源码 中的
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
通过
1 | _CFRunLoopGet0 获取的 |
进去查看做了什么
1 | loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); |
发现有这么一个获取线程的方法,也就是传入一个线程作为key,获取一个loop,如果loop为空,就以这个线程为key创建runloop
小结:
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁(下面分析这一条)
runloop的mode
接下来认识一下runloop的主要类
Core Foundation中关于RunLoop的5个类
1 | CFRunLoopRef |
看一下runloop结构体
1 | struct __CFRunLoop { |
只保留主要的就剩下了
1 | typedef struct __CFRunLoop * CFRunLoopRef; |
理解为CFRunLoopRef中包含有_modes,modes是由 CFRunLoopModeRef组成的集合
这些modes中,只有一种是当前模式,称为 _currentMode
接下来我们看看runloopmode中究竟有什么,同样,只保留主要的,关键就是下面4个
总结起来就是
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
- RunLoop启动时只能选择其中一个Mode,作为currentMode
- 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
- 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
- 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
1 | typedef struct __CFRunLoopMode *CFRunLoopModeRef; |
我们可以理解为,RunLoop中有许多模式,但当前运行的只有一种,一个图来表示,就是
具体在某一种runloop中的运行逻辑,官方给出下图
那么,前面说的,_sources0、_sources1、_observers、_timers J究竟包含了什么呢?
先用一张图来总结一下,然后再详细介绍
如上图所示,_sources0 包含触摸事件,和 performSelector:onThread:
跑一下代码证明一下,
首先创建一个新项目,实现点击事件,NSLog只是为了打断点
1 |
|
从打印日志来看 调用了CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION 这个SOURCE0方法
那么接下来验证一下performSelector
由上图可知,performSelector 也是执行了source0
那我们再看一下Timer
如上图所示,这次是CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
对于其他几种情况,读者课自行验证。
用一幅图来总结RunLoop的运行逻辑
源码内部细节分析
要想分析源码首先要知道入口在哪里,由前面的断点可知
入口为 CFRunLoopRunSpecific
去源码中找到之后发现有很多。只保留关键信息
1 |
|
那我们继续跟__CFRunLoopRun 看看做了什么,发现里面很长的东西,整理了一下,只保留关键代码,如下
1 | /* rl, rlm are locked on entrance and exit */ |
截图下来的话,就是这样的
###调用细节
前面说了大概的流程,那么,具体怎么调用的呢,lldb调试堆栈的时候,那些方法怎么调用的呢?这里以 __CFRunLoopDoTimers 为例,看下源码怎么调用的
上图可知,关键代码是 CFRunLoopTimerRef
继续查看 CFRunLoopTimerRef
关键代码是CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
看到这里是不是对前面截图中的调用堆栈更清晰了呢。
其他几种也都是类似的逻辑,就不赘述了。
目前已知的Mode有五种
1 | 目前已知的Mode有5种 |
RunLoop的状态
1 | /* Run Loop Observer Activities */ |
常见的2种Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
RunLoop 和 NSTimer
我们知道,默认情况下,NSTimer计时器,会被UIScrollView 打断,会影响计时器的使用。原因就是滚动时候,RunLoop切换到了UITrackingRunLoopMode模式下,但计时器在NSDefaultRunLoopMode下,所以就停止了。解决办法就是设置NSRunLoopCommonModes。特别注意的是:
NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
本文参考资料: