前言
线程知识:
线程是任务分解成不同的工作单元分配给线程去执行,解决了多任务并发执行的问题,提高了系统性能。在现代交互式系统中,还存在大量的未知不确定的异步事件,这时候线程是一直是出于等待状态的,直到有事件发生才会唤醒线程去执行,执行完成后系统又恢复到以前的等待状态。
如何控制线程在等待和执行任务状态间无缝切换,就引入了RunLoop的概念。
一、RunLoop概念介绍
RunLoop称为事件循环,可以理解为系统中对各种事件源不间断的循环的处理。
应用在运行过程中会产生大量的系统和用户事件,包括定时器事件,用户交互事件(鼠标键盘触控板操作),模态窗口事件,各种系统Source事件,应用自定义的Source事件等等,每种事件都会存储到不同的FIFO先进先去的队列,等待事件循环依次处理。
被RunLoop管理的线程在挂起时,不会占用系统的CPU资源,可以说RunLoop是非常高效的线程管理技术。
RunLoop的作用:
- a.使程序一直运行并接受用户输入
- b.决定程序在何时应该处理哪些Event
- c.调用解耦(Message Queue消息队列)- 比如用户手势操作,被调方队列处理一系列的事件
- d.节省CPU时间(操作系统按照时间片调度)每个程序按照时间片切分执行,RunLoop使线程没事就休眠
二、RunLoop组成与机制
RunLoop与线程的关系
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)
组成
系统提供了两个对象:NSRunLoop 和 CFRunLoopRef,CFRunLoopRef
是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop
是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
RunLoop 的状态处理及mode切换
|
|
RunLoop注意点:
1.runloop在同一段时间只能且必须在一种特定Mode下run
2.要换Mode时,需要停止当前Loop,然后重启新loop
3.Mode是iOS App滑动顺畅的关键
4.自己也可以定制Mode
- NSDefaultRunloopMode(默认状态,空闲状态)
- UITrackingRunloopMode(滑动scrollview)
- UIInitializationRunloopMode(私有,App启动时)
- NSRunLoopCommonModes(mode集合,包含前2个)
在 CoreFoundation 里面关于 RunLoop 有5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
切换mode时需要退出loop重新指定一个mode进入,主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
RunLoop Source
source是runloop的数据抽象类(比如protocol)
source0:处理App内部事件,App自己管理(触发) 如:UIEvent,CFSocket等
source1:由runloop和内核管理,Mach port驱动
自定义source:自定义Source实现
1、定义3个回调函数,将Source添加到线程的RunLoop,最后就是等待Signal信号触发事件唤醒RunLoop,最终执行回调函数RunLoopSourcePerformRoutine中的处理方法;
2、Source对应的处理线程,线程创建Source事件源并将其绑定到当前的runLoop,while循环中执行运行RunLoop到超时后退出当前RunLoop。由于没有注册到RunLoop的事件发生,线程被挂起休眠等待事件触发。
3、Appdelegate,启动线程后,建立Source,Source建立的回调通知AppDelegate当前正在注册source,AppDelegate将其保存在属性sources中。
UI界面创建按钮绑定事件到fireInputSource方法,用户点击按钮立即触发一次自定义的source事件。
RunLoop Timer
- NSTimer 的2种初始化方式区别
|
|
注:GCD方式创建的timer不受runloop影响;
|
|
- performSelector delay 之类的延迟执行 也是 timer的封装
- CADisplayLink
RunLoop Observer
cocoa框架中有很多机制都是有RunloopObserver触发,如CAAnimation
分析:比如多个Animation都被设置,在一个runloop结束后(知道所有设置值)开始跑animation
UIButton点击事件中,UITouch事件什么时候被释放 - 应该是observe被触发的时候释放的。
UIKit通过RunloopObserver在runloop两次sleep间对AutoreleasePool进行Pop和Push,将这次loop中产生的Autorelease对象释放
RunLoop内部流程
runloop的挂起和唤醒 :
- 指定用于唤醒的mach-port端口
- 调用mach-msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach-msg-trap状态
- 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,runloop继续开始干活
三、RunLoop实践介绍
RunLoop在很多地方都有很好的实践:
NSTimer UIEvent Autorelease(autoreleasepool什么时候释放)
NSObject(NSDelayedPerforming,NSThreadPerformAddition)
CADisplayLink(60帧或30帧) CATransition CAAnimation
dispatch_get_main_queue() NSURLConnection
AFNetworking NSPort
AFNetwork2.x中保持线程
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop。
|
|
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:]
将这个任务扔到了后台线程的 RunLoop 中。
RunLoop在ASIHTTPRequest中实践
创建单例线程
|
|
RunLoop在GCDAsyncSocket中实践
|
|
|
|
读写流都在default mode
tableview cell高度预缓存
高度预缓存是一个优化功能,它要求页面处于空闲状态时才执行计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验。
每个任务都被分配到下个“空闲” RunLoop 迭代中执行,其间但凡有滑动事件开始,Mode 切换成 UITrackingRunLoopMode,所有的“预缓存”任务的分发和执行都会自动暂定,最大程度保证滑动流畅。
|
|
参考文章
深入理解RunLoop