Runloop 小结
一、RunLoop 简介
1、RunLoop:运行循环
(1)本质与作用
- RunLoop是用来处理事件的循环。NSRunloop是CFRunloop的封装,CFRunloop是一套C接口。
- RunLoop 实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行。
- 一般来讲,一个线程一次只能执行一个任务,执行完毕后线程就会退出,RunLoop可以使得线程不退出,循环执行任务。
(2)流程
- RunLoop处理消息的流程是“接收消息->恢复活跃->处理消息->进入休眠”。
- RunLoop 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。
2、 RunLoop 和线程
(1)与线程的关系:
- 线程和 RunLoop 之间是一一对应的(一个线程一个runloop),其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 对象的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。
- RunLoop 并不保证线程安全。你只能在当前线程内部操作当前线程的 RunLoop 对象,而不能在当前线程内部去操作其他线程的 RunLoop 对象方法。你只能在一个线程的内部获取其 RunLoop(主线程除外)
- 主线程的 RunLoop 对象系统自动帮助我们创建好了,而子线程的 RunLoop对象需要我们主动创建和维护。
(2)主线程Runloop原理
- 在启动一个iOS程序的时候,系统会调用创建项目时自动生成的 main.m 的文件。其中
UIApplicationMain
函数内部帮我们开启了主线程的 RunLoop,UIApplicationMain
内部拥有一个无限循环的代码,只要程序不退出/崩溃,它就一直循环(UIApplicationMain
函数一直没有返回,我们在运行程序之后程序不会马上退出,会保持持续运行状态)
- 官方配图:runloop模型图
二、RunLoop 结构组成
1、Core Foundation框架下关于 RunLoop 的 5 个类
- CFRunLoopRef:代表 RunLoop 的对象
- CFRunLoopModeRef:代表 RunLoop 的运行模式
- CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源
- CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源
- CFRunLoopObserverRef:观察者,能够监听 RunLoop 的状态改变
2、结构
(1)图示
(2)组成
(3)相互关系
一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。
- 每次 RunLoop 启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作当前运行模式(CurrentMode)。
- 如果需要切换运行模式(CFRunLoopModeRef),只能退出当前 Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
- 这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响 。
3、RunLoop 5个类的详解
(1)CFRunLoopRef 类
CFRunLoopRef 是 Core Foundation 框架下 RunLoop 对象类。可通过以下方式来获取 RunLoop 对象:
<Core Foundation>框架
CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
CFRunLoopGetMain(); // 获得主线程的 RunLoop 对象
<Foundation>框架
[NSRunLoop currentRunLoop]; // 获得当前线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; // 获得主线程的 RunLoop 对象
(2)CFRunModeRef 类
系统默认定义了5种运行模式(CFRunLoopModeRef),其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes 是我们开发中需要用到的模式:
- kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式(后边会用到)
- UIInitializationRunLoopMode:在刚启动App时第一个进入的Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
(3)CFRunLoopTimerRef 类
CFRunLoopTimerRef是定时源,理解为基于时间的触发器(NSTimer)
下面是一个理解CFRunLoopModeRef和CFRunLoopTimerRef结合的简单例子:
现象:
- 如果不拖动textview,run方法正常调用。
- 如果拖动textview并且不松开手指,run方法停止;松开手指后,run回复执行。
原因:
- 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。
- 而当拖动Text View的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以NSTimer就不工作了。
- 但当松开手指的时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了。
- 定时执行的定时器,底层基于使用mk_timer实现,受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。
- 伪模式 kCFRunLoopCommonModes 其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行。
- 被标记上了Common Modes的模式包括:NSDefaultRunLoopMode和 UITrackingRunLoopMode。
- 操作:将NSTimer添加到当前RunLoop的kCFRunLoopCommonModes,[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]
- NSTimer的scheduledTimerWithTimeInterval方法:会自动添加到Runloop的NSDefaultRunLoopMode下。
CADisplayLink
是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的CADisplayLink
对象,把它添加到一个runloop
中,并给它提供一个target
和selector
在屏幕刷新的时候调用。- 不同之处:
<1> 原理不同
- CADisplayLink是以屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
- NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。
<2>周期设置方式不同
- iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector 默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置, CADisplayLink的selector每秒调用次数=60/ frameInterval。比如当 frameInterval设为2,每秒调用就变成30次。因此, CADisplayLink 周期的设置方式略显不便。
- NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多。
<3>精确度不同
- iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
- NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在忙于别的调用,触发时间就会推迟到下一个runloop周期。另外,NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间范围。
-
使用CADisplayLink时,要注意:iOS设备每一次刷新的时间就是1/60秒 ,大概16.7毫秒。当我们的frameInterval值为1的时候我们需要保证的是 CADisplayLink调用的`target`的函数计算时间不应该大于 16.7毫秒,否则就会出现严重的丢帧现象。
<4>使用场合
- CADisplayLink 使用场合相对专一, 适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
- NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。
- CADisplayLink举例:
(4)CFRunLoopSourceRef类
CFRunLoopSourceRef是事件源,有两种分类方法。
- 第一种按照官方文档来分类(就像RunLoop模型图中那样),之前的分法:
- Port-Based Sources(基于端口)
- Custom Input Sources(自定义)
- Cocoa Perform Selector Sources
- 第二种按照函数调用栈来分类:
- Source0 :触摸事件,PerformSelectors,非基于Port的(Button的点击事件,就是source0里处理的)
- Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件
(5)CFRunLoopObserverRef类
CFRunLoopObserverRef是观察者,用来监听RunLoop的状态改变。CFRunLoopObserverRef可以监听的状态改变有以下几种:
监听代码举例
三、RunLoop 的运行逻辑
1、在每次运行开启RunLoop的时候,所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者。
2、具体的顺序如下:
(1)通知观察者RunLoop已经启动
(2)通知观察者即将要开始的定时器
(3)通知观察者任何即将启动的非基于端口的源
(4)启动任何准备好的非基于端口的源
(5)如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
(6)通知观察者线程进入休眠状态
(7)将线程置于休眠知道任一下面的事件发生:
- 某一事件到达基于端口的源
- 定时器启动
- RunLoop设置的时间已经超时
- RunLoop被显示唤醒
(8)通知观察者线程将被唤醒
(9)处理未处理的事件
- 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
- 如果输入源启动,传递相应的消息
- 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
(10)通知观察者RunLoop结束。
四、RunLoop 的实战使用
1、NSTimer的使用
上面已经说过
2、ImageView推迟显示
实现tableview、textview拖拽的时候,不加载图片(虽然可以提高流畅,但是会有留白,也影响用户体验),停止拖拽时再加载。
3、后台常驻线程
我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。
- performSelector:系列方法是runloop方法,只有开启runloop才能执行。主线程默认开启runloop,子线程要手动开启、关闭。
- 示例代码
之后如果有需要在子线程里处理的事情,可以直接丢到这个常驻线程里面。
四、RunLoop 与自动释放池
1、主线程自动释放池的创建和销毁:
- 每一次主线程的消息循环开始的时候会先创建自动释放池。
- 消息循环结束前,会释放自动释放池。
- 自动释放池被销毁或耗尽时会向池中所有对象发送 release 消息,释放所有 autorelease 的对象(引用计数-1)。
- 自动释放池随着消息循环的开始和结束不断的重建和销毁。
2、子线程的自动释放池:
- 在子线程开启时手动创建释放池。因为主线程可以自动生成释放池,而子线程不可以。为了保证消息循环结束(线程结束)时,所有的对象可以正常入池和释放,必须手动添加。
- 其他情况和主线程相同。
3、什么时候使用自动释放池:(官方文档建议)
- 开启子线程时。
- 在一个循环中,生成了大量的临时变量,需要手动在循环内部加入释放池(否则内存会爆。。)。
- 例如:
参考链接 https://www.jianshu.com/p/d260d18dd551