文章目录
  1. 1. RunLoop 概念
    1. 1.1. 概念解释
    2. 1.2. 作用意义
    3. 1.3. 构成详解
      1. 1.3.1. Modes
      2. 1.3.2. Source
      3. 1.3.3. Timer
      4. 1.3.4. Observer
    4. 1.4. 运行机制
  2. 2. RunLoop 系统应用
    1. 2.1. 事件响应(source1)
    2. 2.2. 手势识别(source0)
    3. 2.3. Scrollview滑动
    4. 2.4. UI界面刷新、CATransition、CAAnimation
    5. 2.5. CADisplayLink
    6. 2.6. NSTimer
    7. 2.7. AutoRelease
    8. 2.8. GCD
    9. 2.9. NSObject
    10. 2.10. NSURLConnection
  3. 3. RunLoop 实践使用
    1. 3.1. 使用范例
    2. 3.2. Mode切换:Scrollview中出现耗时操作
    3. 3.3. RunLoop监听:AFNetworking中守护线程
    4. 3.4. 程序崩溃重启
    5. 3.5. 异步调试
  4. 4. RunLoop 常见问题
  5. 5. 引申概念说明
  6. 6. 参考文献

RunLoop 概念

概念解释

先来看一下官方文档的定义:

Run loops are part of the fundamental infrastructure associated with threads.A run loop 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是与线程相关的系统基础框架的一部分。而RunLoop的作用,顾名思义就是用来接收、协调所传入事件的”事件处理循环”。RunLoop目的就是让线程在有任务的时候努力工作,在没有任务的时候进入休眠。

(如果有兴趣可以看看源码:CFRunloop

有没有接地气一点的解释?有啊,RunLoop说白了其实就是一个一直运行着的循环(带条件)。跟我们平常经常使用的for循环类似,只是RunLoop采用了一些特殊的机制让这个循环一直运行不退出,以用来持续接收并处理系统收到的事件,这样用户就可以一直在app里进行想要的操作了,并且app会保活并处理用户的各种操作。

而RunLoop构建在线程框架的基础之上。当用户从Spring board上点击一个app图标启动这个App开始,这个App就可以一直进行例如:接收用户操作、跟远程服务器进行网络数据交换、将数据展示到各种类型的视图上……等等工作。而系统具体是怎么实现这些操作的呢?原理就是系统给运行的App分配了进程,用以给app运行分配资源(CPU、内存、储存空间、GUP等),每个app有自己的进程,并且在app内部可以给相应的专有任务开辟线程。在iOS系统中,每个App启动伊始就会创建一个线程:主线程,这个线程会自带一个RunLoop,用以保持该App一直活跃来处理接收到的事件。

作用意义

了解了RunLoop的概念后,那么RunLoop存在的意义是什么呢?从上文的内容可以看出,线程中不一定必须有RunLoop,只有想一直维持这个线程的时候才会用到RunLoop。那我们大可以自己创建一个循环来保持线程持续运行啊,为什么非要用到RunLoop呢?这个就很有意思了,因为RunLoop通过系统的底层的消息机制,实现了”事件驱动运行”。可以达到如下目的:

  1. 持续等待事件发生:让程序可以一直保持”可接受用户事件”的状态。
  2. 计划事件发生:安排程序在什么时候应该处理什么事件。
  3. 事件调用解耦:事件的发送方不需要同步事件的结果,可以直接发起下一个事件。
  4. 节省CPU时间:等待事件时CPU不会空转消耗,接受事件后CPU才开始处理。

既节省了资源,又保证了需要的操作,简直非常棒!

构成详解

RunLoop结构图

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

Source有两个版本:Source0 和Source1
Source0只包含了一个回调(函数指针),并不能主动触发事件,需要手动触发,
需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

Modes

RunLoopMode就是RunLoop当前运行的模式,类似于变形金刚汽车形态可以乘坐,机器人形态可以打架一样,不同的mode下,RunLoop会有不同的处理侧重。

RunLoop的mode有以下几种类型:

1
2
3
4
* NSDefaultRunLoopMode:RunLoop的默认运行状态,空闲状态。
* UITrackingRunLoopMode:有滑动、点击等其他需要追踪的事件发生时RunLoop会切换的模式。
* NSInitializationRunLoopMode:初始化模式,是一种私有模式。App启动时会处于此模式下,启动完成后进入到主界面后,App会切换到默认模式。
* NSRunLoopCommonModes:通用模式。这个就比较有意思,看完RunLoop的运行原理后会更好理解,它默认”指向“了NSDefaultRunLoopModeUITrackingRunLoopMode,所以不管是以上两种的哪种模式下运行,只要监听了NSRunLoopCommonModes,都会收到回调。并且,这个模式也是用户自定义模式添加的入口。

需要注意的几点是:

  • 一个确定的RunLoop可以运行在不同的RunLoopMode下,但是同一时间它只能运行在其中一个RunLoopMode下。
  • 如果要切换RunLoopMode,需要先停止RunLoop,修改RunLoopMode,再重启新的RunLoop。
  • App在启动时,会运行在NSInitializationRunLoopMode下,App启动完成后运行在NSDefaultRunLoopMode下,当出现用户操作时App会切换运行在NSTrackingRunLoopMode下。
  • 默认的RunLoop至少带有:
    • Default mode (默认mode,大部分操作所在mode)
    • Event Mode (追踪用户操作事件、交互事件的mode)
    • Common Modes (是一个特殊的Mode。RunLoop会把加入到CommonItems的事件同步到commonModes中的所有Mode上,默认已有上面所说两种,自定义的Mode也是添加到这个集合modes中)

Source

  • source0:

  • source1:

  • 自定义source:

Timer

Observer

发起逻辑请求方,可以挂载Observer到RunLoop上,RunLoop跟怒数据源里设定的条件,处理完成后,会回调数据源的Observer。

回调RunLoopObserver时对应的Activity状态,就反应了RunLoop当前的状态。

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //InputSource/Timer已经加入到RunLoop了
kCFRunLoopBeforeTimers = (1UL << 1), //Timer即将要被执行了
kCFRunLoopBeforeSources = (1UL << 2), //InputSource即将要被执行了
kCFRunLoopBeforeWaiting = (1UL << 5), //RunLoop即将休眠了
kCFRunLoopAfterWaiting = (1UL << 6), //RunLoop即将被唤醒
kCFRunLoopExit = (1UL << 7), //RunLoop停止运转了
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
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
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();


/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

运行机制

Runloop底层用到了:GCD、mach kernel、block、p_thread

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
SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
do {
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);

__CFRunLoopDoBlocks();
__CFRunLoopDoSource0();

CheckIfExistMessagesInMainDispatchQueue(); // GCD

__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// mach_msg_trap
// Zzz...
// Received mach_msg, wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// Handle msgs
if (wakeUpPort == timerPort) {
__CFRunLoopDoTimers();
} else if (wakeUpPort == mainDispatchQueuePort) {
// GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else {
__CFRunLoopDoSource1();
}
__CFRunLoopDoBlocks();
} while (!stop && !timeout);

RunLoop 系统应用

事件响应(source1)

苹果注册了一个Source1(基于mach port)用来接收系统事件,其回调函数为
__IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture、处理屏幕旋转、发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别(source0)

当上文事件响应中的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个手势更新相关的 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

Scrollview滑动

切换mode以保证滑动不卡顿。

主线程的RunLoop里预置两个被标记了”Common”属性的mode:UITrackingRunLoopMode、kCFRunLoopDefaultMode。平常状态下,主线程的RunLoop处于kCFRunLoopDefaultMode下,当Scrollview滑动时,就会切换到UITrackingRunLoopMode下。

如果创建一个Timer并加入到DefaultMode下,那么当用户滑动时,只会回调UITrackingRunLoopMode下的Source、Timer的Observer,自主创建的timer就不会被回调。如果需要创建一个在两种mode下都要执行的timer,那么就需要加入到主线程RunLoop的commomModeItems中去,会被RunLoop自动更新到具有”Common”属性的Mode里去。

UI界面刷新、CATransition、CAAnimation

当UI界面改动后,比方说改动了Frame、更新了UIView/CALayer的层次、手动调用了UIView/CALayer的setNeedDisplay/setNeedLayout方法后,这些更新了的UIView/CALayer就会被标记为待处理,并被提交到一个全局的容器里。

iOS系统自动注册了Observer用来监听BeforeWaiting(即将进入休眠)和Exit(即将推出RunLoop)事件,回调会执行一个函数(),这个函数里面就对那些标记了待处理的UIView/CALayer遍历进行了处理,进行了实际绘制修改,并更新到UI界面。

动画也是同样的处理时机,当动画标记待执行后,也是在RunLoop的BeforeWaiting时才执行这个动画。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source1)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。

NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

AutoRelease

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监听的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监听了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显式创建 Pool 了。

GCD

但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

NSObject

PerformSelector:afterDealy:方法簇

NSObject的performSelecter:afterDelay: 实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
NSObject的performSelector:onThread: 实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

NSURLConnection

sorce0 处理回调 & sourc1 接收 Socket 回调

iOS 中,关于网络请求的接口自下至上有如下几层:

1
2
3
4
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
  • CFSocket 是最底层的接口,只负责 socket 通信。
  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

NSURLConnection流程图

参考资料

RunLoop 实践使用

使用范例

Mode切换:Scrollview中出现耗时操作

RunLoop监听:AFNetworking中守护线程

程序崩溃重启

异步调试

RunLoop 常见问题

①在子线程里启动一个Timer,但是这个timer一次也不会被调用,为什么?

②在一个线程里发起NSURLConnection网络数据请求,但是NSURLConnection的delegate没有回.为什么?

③在主线程环境下的一个方法体里的第一行调用performSelector:withObject:afterDelay:这种带afterDelay的方法簇时,这一行代码实际执行时机往往是在方法体执行过程的最后。为什么?

引申概念说明

<font/ size=3 color=red style=”font-weight:bold;”> <span/ id=”process”>①进程

<font/ size=2>进程是计算机科学的一个基础概念.是计算机中一个具有独立功能的程序关于某个数据集合的一次运行活动。进程其实就是程序处理任务的实例,进程还可以包含多个线程,而线程就是处理某个具体任务的实例。接地气点解释就是:程序相当于一个工厂,而进程就是生产车间,而线程就是生产线;正常状态下,一个工厂可以有多个生产车间,一个生产车间可以有多条生产线;而每个车间具体生成某个产品,而每条生产线具体生产产品的某个部件,大概就是这么个意思。

<font/ size=2>线程才是处理具体任务的对象,开启多线程的原因就是为了防止线程的堵塞,比方说处理界面更新操作、处理大批计算操作如果同步进行,CPU同步处理,就可能会出现CPU忙不过来造成界面卡顿的问题,而给每个任务开单独的线程,就可以将任务异步进行,不会彼此影响(CPU允许范围内)。接地气点解释就是:就有点类似于生产车间生产摩拜(摩拜请付给我广告费~)自行车,如果只有一条生产线(线程),就只能车把、车轮、链条……这样一个部分等着另一部分完成再继续生产的进行;而有多条生产线,就可以同时生产各个部分,最后组装即可。

<font/ size=2>而当下的移动端开发同样的也采用了类似的操作。但是Android系统和iOS系统还有点不太一样,Android系统可以给一个app开启多个进程,而在iOS中一个app只能开启一个进程。但是线程都是同样的,可以开启多个。而一般情况下一个线程只能执行一个任务,执行完毕就会退出销毁。但是不行啊,有时候我们需要一个线程一直运行着,不管有没有任务,需要它等着随时有任务的到来(App主线程),所以才有了这么一个机制叫做时间循环,iOS中叫做RunLoop,Android中大概叫Looper。

<font/ size=3 color=red style=”font-weight:bold;”> <span/ id=”tollfreebridged”>②toll-free bridge

<font/ size=2>这个是苹果iOS系统专有的一个名词。如果某些数据类型能够在Core Foundation和Foundation之间互换使用,那这种可被互换的数据类型就叫做Toll-Free Bridged类型。参考资料

参考文献

1.RunLoop源码Objective-C
2.RunLoop源码Swift
3.RunLoop知识树
4.
5.
6.

文章目录
  1. 1. RunLoop 概念
    1. 1.1. 概念解释
    2. 1.2. 作用意义
    3. 1.3. 构成详解
      1. 1.3.1. Modes
      2. 1.3.2. Source
      3. 1.3.3. Timer
      4. 1.3.4. Observer
    4. 1.4. 运行机制
  2. 2. RunLoop 系统应用
    1. 2.1. 事件响应(source1)
    2. 2.2. 手势识别(source0)
    3. 2.3. Scrollview滑动
    4. 2.4. UI界面刷新、CATransition、CAAnimation
    5. 2.5. CADisplayLink
    6. 2.6. NSTimer
    7. 2.7. AutoRelease
    8. 2.8. GCD
    9. 2.9. NSObject
    10. 2.10. NSURLConnection
  3. 3. RunLoop 实践使用
    1. 3.1. 使用范例
    2. 3.2. Mode切换:Scrollview中出现耗时操作
    3. 3.3. RunLoop监听:AFNetworking中守护线程
    4. 3.4. 程序崩溃重启
    5. 3.5. 异步调试
  4. 4. RunLoop 常见问题
  5. 5. 引申概念说明
  6. 6. 参考文献