学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

RunLoop

简介

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总处于以下状态中的一种

  1. 内核态,运行于进程上下文,内核代表进程运行于内核空间
  2. 内核态,运行于中断上下文,内核代表硬件运行于内核空间
  3. 用户态,运行于用户空间

用户态到内核态怎样切换

  1. 系统调用,比如fork()
  2. 异常,比如缺页异常
  3. 外设中断,比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作

Runloop和线程的关系

  1. 一个线程对应一个Runloop
  2. 主线程的默认就有Runloop
  3. 子线程的Runloop以懒加载的形式创建
  4. Runloop存储在一个全局的可变字典里吃,线程是key,Runloop是value。

子线程RunLoop获取

1
CFRunLoopGetCurrent() 或 [NSRunLoop currentRunLoop];

RunLoop的作用

  1. 让线程一直活着
  2. 处理线程活着遇到的各种事件
  3. 节省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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

系统默认提供五个Model:

1
2
3
4
5
6
7
8
{
kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes //这是一个占位用的Mode,不是一种真正的Mode,而是一种模式组合,一个操作 Common 标记的字符串
你可以用这个字符串来操作 Common Items
}

CFRunLoop提供的管理Mode接口

1
2
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

Mode管理mode item的接口

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

只能通过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:

  1. InputSources : 传递递异步事件,通常消息来自另外的线程或者程序;

  2. 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
2
3
4
Source0 :只包含一个函数指针(回调方法),不能自动触发,只能手动触发,触发方式是先通过CFRunLoopSourceSignal(source)将这个Source标记为待处理,然后再调用CFRunLoopWakeUp(runloop) 来唤醒RunLoop处理这个事件。

Source1 :基于port的Source源,包含一个port和一个函数指针(回调方法)。该Source源可通过内核和其他线程相互发送消息,而且可以主动唤醒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
2
3
4
5
6
7
8
9
10
11
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

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 就不会运行。

有如下两种解决方案:

  1. 设置 RunLoop Mode,例如 NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个 Mode 的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
  2. 另一种解决 Timer 的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新 UI。

CFRunLoopObserverRef

在处理事件源时,runloop会产生关于这些行为的通知,可以往Run Loop中加入自己的观察者以便监控Run Loop的运行过程。

Run Loop Observer会与以下事件相关联:

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) 
{
kCFRunLoopEntry = (1UL << 0),//即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理 Timer
kCFRunLoopBeforeSources= (1UL << 2), //runloop即将处理input sources的事件
kCFRunLoopBeforeWaiting = (1UL << 5), //runloop即将休眠
kCFRunLoopAfterWaiting= (1UL << 6), //runloop已经唤醒,但是唤醒runloop的事件还没有处理。
kCFRunLoopExit = (1UL << 7), //即将退出runloop

和Timer一样,Run Loop Observers也可以使用一次或者选择repeat。如果只使用一次,Observer会在它被执行后自己从Run Loop中移除。而循环的Observer会一直保存在Run Loop中.

一旦runloop跑起来,线程的runloop就会处理等待的事件并且给observer发送通知

RunLoop生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int32_t __CFRunLoopRun()
{
// 通知即将进入runloop
//创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRunLoopDoObservers(KCFRunLoopEntry);

do
{
// 通知将要处理timer和source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);

// 处理非延迟的主线程调用
__CFRunLoopDoBlocks();
// 处理UIEvent事件
__CFRunLoopDoSource0();

// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();

// 即将进入休眠
//释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

// 等待内核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();

// Zzz...

// 从等待中醒来
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

if (wakeUpPort == timerPort){// 处理因timer的唤醒
__CFRunLoopDoTimers();
}else if (wakeUpPort == mainDispatchQueuePort){// 处理异步方法唤醒,如dispatch_async
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else{// UI刷新,动画显示
__CFRunLoopDoSource1();
}
// 再次确保是否有同步的方法需要调用
__CFRunLoopDoBlocks();

} while (!stop && !timeout);

// 通知即将退出runloop
//释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRunLoopDoObservers(CFRunLoopExit);
}

可以看到RunLoop事件源执行的顺序是Timer->source0->source1

什么时候才需要使用runloop

只有在你开启了子线程的情况下才需要运行runloop

对于子线程,是否需要运行runloop取决于实际需要,并不是所有的子线程都需要运行runloop。如果子线程要跑一个确定的而且长的任务,就没必要开启runloop。runloop更适合当你需要与线程更多交互的情景。以下情景需要开启runloop:

1
2
使用ports或者自定义input sources与其他线程联通在线程上使用Timer
使用performSelector这类方法保持线程执行定期周期任务

如果你在子线程运行了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循环,处理完事件后继续休眠)

参考

  1. Runloop
  2. 关于RunLoop你想知道的事
  3. 用户态和内核态