文章目录
  1. 1. Runloop是什么
  2. 2. Runloop的组成
    1. 2.1. Runloop构成
    2. 2.2. RunLoop主要处理以下6类事件
  3. 3. RunLoop的Mode
  4. 4. RunLoop的运行机制
    1. 4.1. RunLoop的挂起和唤醒
      1. 4.1.1. RunLoop的挂起
      2. 4.1.2. RunLoop的唤醒
      3. 4.1.3. 处理事件
      4. 4.1.4. 事件处理完成进行判断
    2. 4.2. RunLoop的底层实现
  5. 5. RunLoop和线程
  6. 6. 苹果用RunLoop实现的功能
    1. 6.1. Autoreleasepool
    2. 6.2. NSTimer(timer触发)
    3. 6.3. 和GCD的关系
    4. 6.4. PerformSelector
    5. 6.5. 网络请求
    6. 6.6. 事件响应
    7. 6.7. 手势识别
    8. 6.8. UI更新
  7. 7. 参考资料

在简书上浏览到了一篇文章,总结了<font/ color=red>Runloop这个技术点的大大小小的几乎全部内容,看完感觉写的不错,然后就转载过来,趁此机会自己也梳理一下关于<font/ color=red>Runloop的一些知识点.

Runloop是什么

RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方.

Runloop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的个各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)和消息,从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序的性能.

Example1

  • mach kernel属于苹果内核,RunLoop依靠它实现了休眠和唤醒而避免了CPU的空转.
  • Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API,它是mach thread的上层封装(可以参见Kernel Programing Guide),和NSTread一一对应(而NSTread是一套面向对象的API,所以在iOS开发中我们也几乎不用直接使用pthread).

Example2

Runloop的组成

Runloop构成

在Foundation Framework中有相应的NSRunLoop API来操作RunLoop,其实它是对CoreFoundation Framework中的CFRunLoop的封装.CFRunLoop的底层实现用到了machkernel、block、pthread等技术.这里核心的是machkernel,RunLoop依靠它实现了休眠和唤醒而避免了CPU的空转.

与RunLoop相关的技术我们经常用到,它们是:UI层相关的NSTimer、UIEvent、Autorelease;NSObject相关的NSObject (NSDelayedPerforming)方法簇、NSObject (NSThreadPerformAdditions)方法簇;CA相关的CADisplayLink、CATransition、CAAnimation;GCD中的dispatch_get_main_queue();以及NSURLConnection.

以下是关于RunLoop的一些官方文档解释:

CFRunLoop对象可以检测某个task或者dispath的输入事件,当检测到有输入源事件,CFRunLoop将会将其加入到线程中进行处理.比方说用户输入事件、网络连接事件、周期性或者延时事件、异步的回调等.

RunLoop可以监测的事件类型一共有3种,分别是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver,可以通过CFRunLoopAddSource、CFRunLoopAddTimer或者CFRunLoopAddObserver添加相应的事件类型.

要让一个RunLoop跑起来还需要run loop modes,每一个source、timer和observe添加到RunLoop中时必须要与一个模式(CFRunLoopMode)相关联才可以运行.

RunLoop的主要组成

RunLoop共包含5个类,但公开的只有Source、Timer、Observe相关的三个类.

Example3

Example4

CFRunLoopSourceRef

source是RunLoop的数据源(输入源)的抽象类(protocol),Source有两个版本:Source0和Source1.

  • source0: 只包含了一个回调(函数指针),使用时,你需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来花型RunLoop,让其处理这个事件.处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef.

  • Source1: 由RunLoop和内核管理,由mach_port驱动(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort.特别需要注意一下Mach port的概念,他是一个轻量级的进程间通讯的方式,可以理解为它是一个通讯通道,假如同时有几个进程都挂在这个通道上,那么其他进程向这个通道发送消息后,这些挂在这个通道上的进程都可以收到相应的消息.这个port的概念非常重要,因为他是RunLoop休眠和被唤醒的关键,他是RunLoop与系统内核进行消息通讯的窗口.

CFRunLoopTimerRef

它是基于时间的触发器,它和NSTimer是toll-free bridged的,可以混用(底层基于使用mk_timer实现).它受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调.如果线程阻塞或者步子啊这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发.

CFRunLoopObserverRef

它是观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化.可以观测的时间点有以下几个:

Example5

这里需要提一句的是,timer和source1(也就是基于port的source)

RunLoop主要处理以下6类事件

Example6

RunLoop的Mode

CFRunLoopMode和CFRunLoop的结构大致如下:

Example7

一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer,每次调用RunLoop的主函数的时候,只能指定其中一个Mode,这个Mode被称作CurrentMode.如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入.这样设计的目的主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响,下面是5种Model:

  • kCFDefaultRunLoopMode: app的默认Mode,通常主线程是在这个Mode下运行的
  • UITrackingRunLoopMode: 界面跟踪Mode,用于Scrollview追踪触摸滑动,保证界面滑动是不受其他Mode的影响.
  • UIInitializationRunLoopMode: 在刚启动app的时候进入的第一个Mode,启动完成后就不再使用了.
  • GSEventReceiveRunLoopMode: 接受系统事件的内部Mode,不是一种真正的Mode.
<font/ color=red>其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是苹果公开的,其余的mode都是无法添加的.

但是,我们平常使用RunLoop的时候却有以下的这种用法:

1
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

不是说只有上面提到的Default、Tracking的mode可以用么,为什么我们还可以使用CommonMode?什么是CommonMode?

一个mode可以将自己标记为”Common”属性(通过将其ModeName添加到RunLoop的”CommonModes”中).每当RunLoop的内容发生了变化时,RunLoop都会自动将_commonModeItems里的Source/Observer/Timer同步到具有”Common”标记的所有Mode里.

主线程的RunLoop里有KCFRunLoopDefaultMode和UITrackingRunLoopMode,这两个Mode都已经被标记为”common”属性.

当你创建一个Timer并加入到DefaultMode的时候,Timer会得到持续重复回调,但此时滑动一个scrollView时,RunLoop会将Mode切换为TrackingRunLoopMode,这时Timer就不会被回调,并且也不会影响到滑动操作.如果想让Scrollview滑动时timer可以正常调用,一种办法就是手动将这个Timer分别加入这两个Mode,另一种方法就是将Timer加入到CommonMode中去.

那么怎么讲事件加入到CommonMode中呢?

我们调用上面的代码,将timer加入到CommonMode时,但实际并不存在真实的CommonMode,其实系统是将这个Timer加入到了顶层的RunLoop的commonModeItems中,commonModeItems会被RunLoop自动更新到所有具有”common”属性的Mode里去了.

这一步其实是系统帮我们将Timer加入到了KCFRunLoopDefaultMode和UITrackingRunLoopMode中.

RunLoop的运行机制

Example8

当调用CFRunLoopRun()时,线程就会一直停留在这个循环里,直到超时或被手动停止,该函数才会返回.每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行.RunLoop运行时首先根据ModeName找到对应的Mode,如果mode里没有source/tiemr/observer,直接返回.

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回.
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调.
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息.
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep).
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息.线程将进入休眠, 直到被下面某一个事件唤醒.
/// • 一个基于 port 的Source 的事件.
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了.
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息.
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调.
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block.
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回.
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop.
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出.
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

RunLoop的挂起和唤醒

RunLoop的挂起

RunLoop的挂起是通过 <font/ color=red>_CFRunLoopServiceMachPort →call→ <font/ color=red>mach_msg →call→ <font/ color=red>mach_msg_trap 这个调用顺序来告诉内核RunLoop监听哪个mach_port(上面提到的消息通道),然后等待事件的发生(等待InputSource、Timer描述内容相关的事件),这样内核就把RunLoop挂起了,即RunLoop休眠了.

RunLoop的唤醒

以下情况下会被唤醒:

  1. 存在Source0被标记为待处理,系统调用CFRunLoopWakeUp唤醒线程处理事件
  2. 定时器时间到了
  3. RunLoop自身的超时时间到了
  4. RunLoop外部调用者唤醒

当RunLoop被挂起后,如果之前监听的事件发生了,由另一个线程(或另一个进程中的某个线程)向内核发送这个mach_port的msg后,trap状态被唤醒,RunLoop继续运行.

处理事件

  1. 如果一个Timer到时间了,出发这个Timer的回调
  2. 如果有dispatch到main_queue的block,执行block
  3. 如果一个Source1发出事件了,处理这个事件

事件处理完成进行判断

  1. 进入loop时传入参数指明处理完事件就返回(stopAfterHandle)
  2. 超出传入参数标记的超时时间(timeout)0
  3. 被外部调用者强制停止_CFRunLoopStopped(runloop)
  4. source/timer/observer全都空了_CFRunLoopModelsEmpty(runloop、currentMode)

RunLoop的底层实现

关于RunLoop底层实现的原理,可以阅读ibireme的深入理解RunLoop一文.
Mach消息发送机制可以看这篇Mach消息发送机制.

如下只介绍一些简单的原理知识:为了实现消息的发送和接收,mach_msg()函数实际上是调用了一个Mach陷阱(trap),也就是函数mach_msg_trap(),陷阱这个概念在Mach中等同于系统调用.当你在用户态调用mach_msg_trap()时会触发陷阱机制,切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作.如下图:

Example9

RunLoop的核心就是一个mach_msg()(见上面代码的第7步),RunLoop调用这个函数去结束消息,如果没有别人发送port消息过来,内核会将线程置于等待状态.例如你在模拟器里跑起一个iOS的app,然后在App静止时点击暂停,你会看到主线程调用栈是停在mach_msg_trap()这个地方.

RunLoop和线程

RunLoop和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或者多个任务,但是在默认情况下,线程执行完了就会推出,就不能再执行任务了.这时我们就需要用一种方式来让线程能够持续处理任务,不会退出.所以,就有了RunLoop.

iOS开发中能遇到两个线程对象: pthread_t和NSThread,pthread_t和NSThread是一一对应的.比如,获取主线程可以使用pthread_main_thread_np(),也可以使用[NSTread mainThread]; 获取当前线程,可以使用pthread_self(),也可以使用[NSThread currentThread].而CFRunLoop就是基于pthread来进行管理的.

线程与RunLoop是一一对应的关系(对应关系保存在一个全局的Dictionary中),线程创建后是没有RunLoop的(主线程除外),RunLoop的创建是发生在第一次获取时,销毁则是在线程结束的时候.<font/ color=red>只能在当前线程中操作当前的RunLoop,不能去操作其他线程的RunLoop.

苹果不允许直接创建RunLoop,但是可以通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()来获取(如果没有就自动创建一个).

Example10

开发过程中需要RunLoop时,则需要手动创建和运行RunLoop(尤其是在子线程中,主线程中的main RunLoop除外).下面有一个很有意思的例子:

1
调用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]带有schedule的方法簇来启动Timer.

用此方法会创建Timer,并把Timer放到当前线程的RunLoop中,随后RunLoop会在Timer设定的时间点回调Timer绑定的selector或者invocation.但是,在主线程和子线程中调用此方法的效果是有差异的,即在主线程中调用scheduledTimer方法时,timer可以在设定的时间点触发;但是在子线程中则不能触发.这是因为在子线程中没有创建RunLoop且没有启动RunLoop,而在主线程中RunLoop是默认创建好并且一直在运行的,所以,子线程中调用以上方法,需要使用如下的方法:

1
2
3
4
5
6
7
8
9
10
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] run];
});

那为什么下面这样调用同样不会触发Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop currentRunLoop] run];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});

我分析原因是:shceduledTimerWithTimeInterval内部在向RunLoop传递Timer的时候,是调用了与线程实例相关的单例方法[NSRunLoop currentRunLoop]来获取RunLoop实例的,也就是如果RunLoop不存在就创建一个与当前线程相关的RunLoop并把Timer传递到RunLoop中,存在则直接传递Timer到RunLoop中即可. 而在RunLoop开始运行后再向其传递Timer时,由于dispatch_async中的代码是顺序执行的,[[NSRunLoop currentRunLoop] run]是一个么有结束时间的RunLoop,无法执行到”[NSTimer scheduledTimerWithTimeInterval:…”这行代码,Timer也就没有被加入到当前RunLoop中,所以更不会出发Timer了.

苹果用RunLoop实现的功能

Autoreleasepool

App启动之后,系统启动主线程,并且创建了RunLoop,在main thread中注册了两个observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler()

第一个Observer监控的事件

  1. 即将进入Loop(kCFRunLoopEntry): 其回调内会调用_objc_autoreleasePoolPush()创建自动释放池.气order是-2147483648,优先级最高,保证创建自动释放池发生在其他任何回调之前.

第二个Observer监控的事件

  1. 准备进入休眠(kCFRunLoopBeforeWaiting): 此时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()来释放旧的池,并创建新的池.
  2. 即将推出Loop(KCFRunLoopExit),此时调用_objc_autoreleasePoolPop()释放’自动释放池’.这个Observer的order是2147483647,确保了’自动释放池’的释放在所有的回调之后.

那接下来,我们知道auto release对象是被AutoReleasePool管理的,那么AutoRelease对象在什么时候被回收呢?

第一种情况:在我们自己实现的for循环或者线程体里面,我们都习惯用AutoReleasePool来管理一些临时变量的auto release,可以使得在for循环或者线程体结束并且回收AutoReleasePool的时候,将auto release 对象也回收掉.

另一种情况:在主线程中创建的auto release对象,这些对象并不能够等到回收Main AutoReleasePool的时候才被回收,因为app一直在运行的过程中,Main AutoReleasePool是不会被回收的.那么这种AutoRelease对象的回收就依赖Main RunLoop的运行状态,Main RunLoop的Observer会在Main RunLoop结束休眠被唤醒时(KCFRunLoopAfterWaiting)通知UIKit,UIkit在收到这一msg后,就会调用_CFAutoReleasePoolPop方法来回收主线程中的所有auto release对象了.

在主线程中执行代码,一般都是卸载时间回调或者Timer回调中,这些回调都被加入了main thread的自动释放池中,所以在ARC模式下我们不用关心对象什么时候释放,也不用去创建和管理Pool(如果事件不在主线程中,要注意手动创建自动释放池,避免出现内存泄漏的维问题).

NSTimer(timer触发)

之前提到了CFRunLoopTimerRef,其实NSTimer的原型就是CFRunLoopTimerRef.当一个Timer注册RunLoop后,RunLoop会为这个Timer的重复时间点注册好事件,有两点需要注意的:

  1. RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer.Timer有个属性叫做Tolerance(宽容度),标示了当时间点到达后,容许有多大的误差,这个误差默认为0,我们可以手动设置这个误差.文档中还强调了,为了防止时间点偏移,系统有权利重置这个属性的值,无论用户配置了什么值.即使RunLoop模式正确,当前的线程并不阻塞,系统依然可能会在NSTimer上加上很小的容差.
  2. 我们在哪个线程调用NSTimer,就必须在哪个线程终止.

在RunLoop的Modes中也有说过,NSTimer使用的时候,需要注意Mode.比方如下图中的例子,用NSTimer实现一个轮播图,如果不设置Timer的Mode为commonModes,那么在滑动的时候轮播图就会停止轮播:

Example11

和GCD的关系

  1. RunLoop底层使用了GCD
  2. RunLoop与GCD并没有直接的关系,但当使用到main_queue时才有关系,如下:

Example12

当调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的RunLoop发送消息,RunLoop会被唤醒,并从消息中取得这个block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()里执行这个block.但是这个逻辑仅限于dispatch到主线程,dispatch到其他线程仍然是由libDispatch处理的.同理,GCD的dispatch_after在dispatch到main_queue时的timer机制才与RunLoop有关.

PerformSelector

NSObject的performSelecter:afterDelay:实际上其内部会创建一个Timer并添加到当前线程的Runloop中,所以如果当前线程么有RunLoop,则这个方法会失效.
NSObject的performSelector:onThread:实际上会传建一个Timer加到对应的线程中去,同样的,如果对应的线程没有RunLoop,那么该方法也会失效.
其实这种操作有种说法叫做创建常驻线程(内存),AFNetworking也用到这种方法,举个例子,如果把RunLoop去掉,那么test方法就不会执行:

Example13

网络请求

iOS中的网络请求接口自上而下有这么几层:

Example14

其中CFSocket和CFNetwork偏底层一些,比较知名的网络请求框架AFNetWorking的早先1.0、2.0版本都是基于NSURLConnection编写的,iOS7之后新增了URLSession,NSURLSession的底层用到了NSURLConnection的部分功能(e.g. com.apple.NSURLConnectionLoader线程),之后新版本的AFNetworking和Alamofire(Swift版本)就是基于这个封装的了.

Example15

通常使用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链接的,NSURLConnectionLoade中的RunLoop通过一些基于mach port的Source1接收来自底层CFSocket的通知.当收到通知后,其会在合适的时机向CFMultiplexerSource等Source0发送通知,同时唤醒delegate线程的RunLoop来让其处理这些通知,CFMultiplexerSource会在delegate线程的RunLoop对delegate执行实际的回调.

事件响应

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


当一个硬件事件发生后(触摸/锁屏/摇晃等),首先由IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接收,SpringBoard只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种Event,随后用mach port转发给需要的app进程.


触摸事件其实是Source1在接收系统事件后在回调_IOHIDEventSystemClientQueueCallback()内触发的Source0,Source0再触发的_UIApplicationHandleEventQueue().Source0一定是要唤醒RunLoop即时响应并执行的,如果RunLoop此时在休眠等待系统的mach_msg事件,那么就会通过Source1来唤醒RunLoop执行.


_UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等.

Example16

手势识别

当上边说到的_UIApplicationHandleEventQueue()识别到一个手势时,其首先会调用cancel将当前的touchesBegin/Move/End系列回调打断.随后系统将对应的UIGestureRecognizer标记为待处理.

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

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

UI更新

Core Animation在RunLoop中注册了一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop)事件.当在操作UI时,比如改变了Frame、更新了UIView/CALayer的层次时,或者手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就会被标记为待处理,并被提交到一个全局的容器中去. 当Observer监听的事件到来时,回调函数中会遍历所有的待处理的UIView/CALayer以执行实际的绘制和调整,并更新界面.

如果此处有动画,通过DisplayLink稳定的刷新机制会不断的唤醒RunLoop,使得不断地有机会执行Observer回调,从而根据时间来不断更新这个动画的属性值,并描绘出来.

函数内部调用栈

Example17

绘图和动画有两种处理方式:CPU(中央处理器)和GPU(图像处理器)
CPU: CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等
GPU: GPU进行变化、合成、渲染

关于CADisplayLink的描述有两种

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


CADisplayLink是一个执行频率(fps)和屏幕刷新频率相同(可以修改preferredFramesPerSecond改变刷新频率)的定时器,它也需要加入到RunLoop才能执行. 与NSTimer相似的,CADisplayLink同样是基于CFRunLoopTimerRef实现的,底层使用mk_timer(可以比较加入到RunLoop前后RunLoop中timer的变化). 和NSTimer相比它的精度更高(尽管NSTimer也可以修改精度),不过和NSTimer类似的是如果遇到大任务它仍然存在丢帧现象,通常情况下CADisplayLink用于构建帧动画,看起来相对更流畅,而NSTimer则有更广泛的用处.

不管怎么样CADisplayLink和NSTimer还是有很大不同,具体的详情可以参考这篇文章:CADisplayLink

ibireme根据CADisplayLink的特性写了一个FPS指示器YYFPSLabel,代码非常少.原理是这样的:既然CADisplayLink可以以屏幕刷新的频率调用指定的seletor,而且iOS系统中正常的屏幕刷新频率为60HX,所以使用CADisplayLink的timestamp属性,配合timer的执行次数计算得出FPS数.

参考资料

  1. 深入理解RunLoop
  2. iOS 事件处理机制与图像渲染过程
  3. RunLoop学习笔记(一) 基本原理介绍
  4. iOS刨根问底-深入理解RunLoop
  5. 【iOS程序启动与运转】- RunLoop个人小结
  6. RunLoop的前世今生
  7. Runloop知识树










文章目录
  1. 1. Runloop是什么
  2. 2. Runloop的组成
    1. 2.1. Runloop构成
    2. 2.2. RunLoop主要处理以下6类事件
  3. 3. RunLoop的Mode
  4. 4. RunLoop的运行机制
    1. 4.1. RunLoop的挂起和唤醒
      1. 4.1.1. RunLoop的挂起
      2. 4.1.2. RunLoop的唤醒
      3. 4.1.3. 处理事件
      4. 4.1.4. 事件处理完成进行判断
    2. 4.2. RunLoop的底层实现
  5. 5. RunLoop和线程
  6. 6. 苹果用RunLoop实现的功能
    1. 6.1. Autoreleasepool
    2. 6.2. NSTimer(timer触发)
    3. 6.3. 和GCD的关系
    4. 6.4. PerformSelector
    5. 6.5. 网络请求
    6. 6.6. 事件响应
    7. 6.7. 手势识别
    8. 6.8. UI更新
  7. 7. 参考资料