面试经常会被问到的一些问题:
1.讲讲 RunLoop,项目中有用到吗?
2.RunLoop内部实现逻辑?
3.Runloop和线程的关系?
4.timer 与 Runloop 的关系?
5.程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
6.Runloop 是怎么响应用户操作的, 具体流程是什么样的?
7.说说RunLoop的几种状态?
8.Runloop的mode作用是什么?
一. RunLoop简介:
运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。
二. RunLoop基本作用:
1.保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行.
2.处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
3.节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情
我们先通过API内一张图片来简单看一下RunLoop内部运行原理
通过图片可以看出,RunLoop在跑圈过程中,当接收到Input sources 或者 Timer sources时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。这里只是简单的理解一下这张图,接下来我们来了解RunLoop对象和其一些相关类,来更深入的理解RunLoop运行流程。
三. RunLoop在哪里开启:
UIApplicationMain函数内启动了Runloop,程序不会马上退出,而是保持运行状态。因此每一个应用必须要有一个runloop,我们知道主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。
进入UIApplicationMain
我们发现它返回的是一个int数,那么我们对main函数做一些修改
运行程序,我们发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行。
我们来看到RunLoop的源码
我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoop,UIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了。
四. RunLoop对象:
因为Fundation框架是基于CFRunLoopRef的一层OC封装,这里我们主要研究CFRunLoopRef源码
如何获得RunLoop对象
五. RunLoop和线程间的关系:
通过源码查看上述对应
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 里。所以我们创建子线程RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop];如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其 RunLoop [NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop会被销毁。
六. RunLoop结构体:
通过源码我们找到__CFRunLoop结构体
除一些记录性属性外,主要来看一下以下两个成员变量
CFRunLoopModeRef 其实是指向__CFRunLoopMode结构体的指针,__CFRunLoopMode结构体源码如下
主要查看以下成员变量
通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。
Source1/Source0/Timers/Observer分别代表什么
1. Source1 : 基于Port的线程间通信
2. Source0 : 触摸事件,PerformSelectors
我们通过代码验证一下
打断点之后打印堆栈信息,当xcode工具区打印的堆栈信息不全时,可以在控制台通过“bt”指令打印完整的堆栈信息,由堆栈信息中可以发现,触摸事件确实是会触发Source0事件。
同样的方式验证performSelector堆栈信息
可以发现PerformSelectors同样是触发Source0事件
3. Timers : 定时器,NSTimer
通过代码验证
打印完整堆栈信息
4. Observer : 监听器,用于监听RunLoop的状态
七. 详解RunLoop相关类及作用:
通过上面的分析,我们对RunLoop内部结构有了大致的了解,接下来来详细分析RunLoop的相关类。以下为Core Foundation中关于RunLoop的5个类
1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source、Timer、Observer每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode如果需要切换Mode,只能退RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
如图所示:
注意:一种Mode中可以有多个Source(事件源,输入源,基于端口事件源例键盘触摸等) Observer(观察者,观察当前RunLoop运行状态) 和Timer(定时器事件源)。但是必须至少有一个Source或者Timer,因为如果Mode为空,RunLoop运行到空模式不会进行空转,就会立刻退出。
系统默认注册的5个Mode:
RunLoop 有五种运行模式,其中常见的有1.2两种
Mode间的切换:
我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况,我们通过一段代码来看一下
代码中的注释也很重要,展示了我们探索的过程
由上述代码可以看出,NSTimer不管用是因为Mode的切换,因为如果我们在主线程使用定时器,此时RunLoop的Mode为kCFRunLoopDefaultMode,即定时器属于kCFRunLoopDefaultMode,那么此时我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop的Mode切换回kCFRunLoopDefaultMode,所以NSTimer就又管用了。
同样道理的还有ImageView的显示,我们直接来看代码,不再赘述了
使用GCD也可是创建计时器,而且更为精确我们来看一下代码
2. CFRunLoopSourceRef事件源(输入源)
Source分为两种
触摸事件及PerformSelectors会触发Source0事件源在前文已经验证过,这里不在赘述
3. CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
我们直接来看代码,给RunLoop添加监听者,监听其运行状态
我们来看一下输出
以上可以看出,Observer确实用来监听RunLoop的状态,包括唤醒,休息,以及处理各种事件。
八. RunLoop处理逻辑:
这时我们再来分析RunLoop的处理逻辑,就会简单明了很多,现在回头看官方文档RunLoop的处理逻辑,对RunLoop的处理逻辑有新的认识。
源码解析:
下面源码仅保留了主流程代码
上述源代码中,相应处理事件函数内部还会调用更底层的函数,内部调用才是真正处理事件的函数,通过上面bt打印全部堆栈信息也可以得到验证。
RunLoop处理逻辑流程图:
九. RunLoop退出:
1.主线程销毁RunLoop退出
2.Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出
3.我们在启动RunLoop的时候可以设置什么时候停止
十. RunLoop应用:
1. 常驻线程
常驻线程的作用:我们知道,当子线程中的任务执行完毕之后就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远都存在,那么我们就会面临一个问题,如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个RunLoop
注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。
子线程开启RunLoop的代码,先点击屏幕开启子线程并开启子线程RunLoop,然后点击button。
注意:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃
2. 自动释放池:
Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
那么什么时候释放,怎么释放呢?
RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。
NSTimer、ImageView显示、PerformSelector等在上面已经有过例子,这里不再赘述。