简介
Run loops are part of the fundamental infrastructure associated with threads. A runloop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
Runloop是与线程相关联的基础架构的一部分,它用来接受循环事件和安排线程的工作,在有工作时让线程处于繁忙状态,没有事件需要处理时让线程休眠;
1 | RunLoop管理事件,让线程在没有消息时休眠以避免占用资源,由用户态切换到内核态;在消息到来时被唤醒,由内核态切换到用户态,这种机制,叫做“事件循环”机制。 |
用户态和内核态
内核态
内核是一种特殊的软件程序,控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序进行。
用户态
用户态就是提供应用程序运行的空间
系统调用
为了使应用程序访问到内核管理的资源如CPU,内存,I/O。内核提供一组通用的访问接口,这些接口就叫系统调用。系统调用是操作系统的最小功能单位。
为什么要区分用户态与内核态
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。CPU指令分为特权指令,和非特权指令,Intel的CPU将特权指令分为4个等级,RING0(内核态),RING1,RING2,RING3(用户态).
CPU总处于以下状态中的一种
- 内核态,运行于进程上下文,内核代表进程运行于内核空间
- 内核态,运行于中断上下文,内核代表硬件运行于内核空间
- 用户态,运行于用户空间
用户态到内核态怎样切换
- 系统调用,比如fork()
- 异常,比如缺页异常
- 外设中断,比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作
Runloop和线程的关系
- 一个线程对应一个Runloop
- 主线程的默认就有Runloop
- 子线程的Runloop以懒加载的形式创建
- Runloop存储在一个全局的可变字典里吃,线程是key,Runloop是value。
子线程RunLoop获取
1 | CFRunLoopGetCurrent() 或 [NSRunLoop currentRunLoop]; |
RunLoop的作用
- 让线程一直活着
- 处理线程活着遇到的各种事件
- 节省CPU时间(有事件处理事件,无事件休息)
RunLoop内部结构
RunLoop内部结构
CFRunLoopModelRef
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified
一个run loop mode是一个集合(input sources 和 timers被监测,observers接受通知)
每次开启runloop都要指定一个mode来运行,在运行期间,只有该mode下对应的事件源才会被监测以及允许传递事件(同样的,该mode对应的observers才能接受runloop进程的通知),其他mode下的事件源的事件将会等待直到切换到对应的mode.更改mode只能重新开启runloop
在设置Run Loop Mode后,你的Run Loop会自动过滤和其他Mode相关的事件源,而只监视和当前设置Mode相关的源(通知相关的观察者)。大多数时候,Run Loop都是运行在系统定义的默认模式上。
Run Loop运行时只能以一种固定的Mode运行,只会监控这个Mode下添加的Timer source和Input source。如果这个Mode下没有添加事件源,Run Loop会立刻返回
一个RunLoop包含若干个Mode,每个Mode又包含若干个Soure/Timer/Oberver,每次调用RunLoop时,只能指定其中一个Mode(每次运行CFRunLoopRun()函数时必须指定Mode,CFRunLoopRun()就是runloop的入口函数),这个mode被称作CurrentMode,如果要切换Mode,只能退出当前的循环,再重新指定一个Mode进入。
上面的Source/Timer/Observer被统称为mode item,一个item可以被同时加入多个mode,但一个item被重复加入同一个mode时是不会有效果的,如果一个mode中一个item都没有,则RunLoop会直接退出,不进入循环。
CGRunLoopMode结构:
1 | struct __CFRunLoopMode { |
系统默认提供五个Model:
1 | { |
CFRunLoop提供的管理Mode接口
1 | CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); |
Mode管理mode item的接口
1 | CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); |
只能通过mode name来操作内部的mode,当你传入一个新的mode name但RunLoop内部没有对应mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。
CFRunLoopSourceRef
A run loop receives events from two different types of sources. Input sources deliver asynchronous events, usually messages from another thread or from a different application. Timer sources deliver synchronous events, occurring at a scheduled time or repeating interval. Both types of source use an application-specific handler routine to process the event when it arrives.
RunLoop 接受的事件源有两种大类: Input sources, Timer sources:
InputSources : 传递递异步事件,通常消息来自另外的线程或者程序;
Timer sources:传递同步事件,发生在预定时间或者是重复间隔;
从上图可以看到,Input sources 传递异步事件给相应的方法处理,并且通过runUntilDate:(由线程对应的NSRunLoop 对象执行)来退出;
Timer sources 传递同步事件给它们对应的例行程序来执行但是不会导致run loop退出。
Input Sources
Input sources 异步地传递事件到线程,而事件的源来至于其中一种:Port-based input sources、Custom input sources 这两种souces的实现处理方式都一样
- Port-based input sources :监听程序的Mach ports,kernel自动发信号
Source1:基于mach_Port,来自系统内核活着其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(mach_port可以理解成进程间通信的一种机制) - Custom input sources :监听自定义的活动,在其他线程手动的发信号
- Cocoa Perform Selector Sources : Cocoa 框架定义的Custom input sources
1 | Source0 :只包含一个函数指针(回调方法),不能自动触发,只能手动触发,触发方式是先通过CFRunLoopSourceSignal(source)将这个Source标记为待处理,然后再调用CFRunLoopWakeUp(runloop) 来唤醒RunLoop处理这个事件。 |
配置Input Sources
Port-Based Sources通过内置的端口相关的对象和函数,配置基于端口的Input source。 (比如在主线程创建子线程时传入一个NSPort对象,主线程和子线程就可以进行通讯。NSPort对象会负责自己创建和配置Input source。)
Custom Input Sources我们可以使用Core Foundation里面的CFRunLoopSourceRef类型相关的函数来创建custom input source,系统也有提供一些方法。
Cocoa框架为我们定义了一些Custom Input Sources,允许我们在线程中执行一系列selector方法,这些在NSThread类里面。和Port-Based Sources一样,这些selector的请求会在目标线程中序列化,以减缓线程中多个方法执行带来的同步问题。和Port-Based Sources不一样的是,一个selector方法执行完之后会自动从当前Run Loop中移除。
1 | performSelectorOnMainThread:withObject:waitUntilDone: |
Timer Sources
Timer sources 在未来的特定时间同步地传递事件给线程,Timer是一种提醒线程做事的方式。
尽管Timer是一种基于时间的通知,但是并不是实时机制,如果不是对应的Mode,timer并不会被fire除非切换到对应的Mode.
如果timer的fire时间,runloop正在处理其他事件,等待超过tolerance,那么这一次fire就会错过,等待下一次来执行,如果runloop退出,那么timer就再也不会fire了。
间隔时间是跟上一次之后的间隔,是timer自己调度的,所以可能并不是跟实际时间完全吻合(因为存在等待,这些需要叠加)。
Timer应用
除了scheduledTimerWithTimeInterval开头的方法创建的Timer都需要手动添加到当前Run Loop中。(scheduledTimerWithTimeInterval 创建的timer会自动以Default Mode加载到当前Run Loop中。)
Timer在选择使用一次后,在执行完成时,会从Run Loop中移除。选择循环时,会一直保存在当前Run Loop中,直到调用invalidated方法。一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoop 的 default mode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode 。而这个时候, Timer 就不会运行。
有如下两种解决方案:
- 设置 RunLoop Mode,例如 NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个 Mode 的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
- 另一种解决 Timer 的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新 UI。
CFRunLoopObserverRef
在处理事件源时,runloop会产生关于这些行为的通知,可以往Run Loop中加入自己的观察者以便监控Run Loop的运行过程。
Run Loop Observer会与以下事件相关联:
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) |
和Timer一样,Run Loop Observers也可以使用一次或者选择repeat。如果只使用一次,Observer会在它被执行后自己从Run Loop中移除。而循环的Observer会一直保存在Run Loop中.
一旦runloop跑起来,线程的runloop就会处理等待的事件并且给observer发送通知
RunLoop生命周期
1 | int32_t __CFRunLoopRun() |
可以看到RunLoop事件源执行的顺序是Timer->source0->source1
什么时候才需要使用runloop
只有在你开启了子线程的情况下才需要运行runloop
对于子线程,是否需要运行runloop取决于实际需要,并不是所有的子线程都需要运行runloop。如果子线程要跑一个确定的而且长的任务,就没必要开启runloop。runloop更适合当你需要与线程更多交互的情景。以下情景需要开启runloop:
1 | 使用ports或者自定义input sources与其他线程联通在线程上使用Timer |
如果你在子线程运行了runloop,你要准备好在合适的时候退出该子线程,因为相比于强制让runloop终止,通过让线程终止来让runloop退出更好。
子线程的runloop是要手动开启的,一个runloop必须有至少一个input source或者timer去监听,否则会立即退出。
RunLoop在休息的时候是一个什么状态
RunLoop是一个死循环,有执行任务,退出,和休息三种状态,解释休息这个问题前,需要先理解mach_port
mach是一个内核,提供CPU调度,进程间通信等一些基础服务,在Mach中,进程,线程,和虚拟内存都被称为“对象”,和其他架构不同,Mach的对象间不能直接调用,只能通过消息传递实现对象间的通信,“消息”是Mach中最基础的概念,一条消息包含当前端口local_port和目标端口remote_port,消息在两个端口(port)之间传递,这就是Mach的IPC(进程间通信)的核心。消息的发送和接收使用<mach/message.h>中的mach_msg()函数,而mach_msg()的本质是一个调用mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换,让程序处于休眠状态,这个状态就是:
APP依然在内存中,但不主动申请CPU资源,然后一直监听某个端口(port),等待内核向该端口发送消息,监听到消息后,从睡眠状态中恢复(重新启动RunLoop循环,处理完事件后继续休眠)