iOS之RunLoop底层原理探究

RunLoop顾名思义也就是运行循环,在程序运行过程中循环执行某些任务。RunLoop的应用范畴包括以下几个方面:

  • 定时器(Timer)、PerformSelector
  • GCD Async Main Queue
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • AutoreleasePool

如果没有RunLoop的话,程序执行完之后就会立即退出。而使用RunLoop后,程序执行完成后并不会马上退出,而是保持运行状态。比如iOS项目中的main函数中UIApplicationMain函数内部就实现了RunLoop。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

UIApplicationMain内部逻辑的伪代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
//休眠中等待消息
int message = sleep_and_wait();
//RunLoop唤醒线程,处理消息
retVal = process_message(message);
} while (0 == retVal);
return 0;
}
}

综上所述,RunLoop的基本作用是:

  • 保持程序的持续运行;
  • 处理App中的各种事件(比如触摸事件、定时器事件等);
  • 节省CPU资源,提高程序性能:有待执行任务时执行任务,不执行任务时休眠。

RunLoop对象

iOS中有2套API来访问和使用RunLoop对象。其中一套基于OC的Foundation:NSRunLoop,另一套是基于C语言的Core Foundation:CFRunLoopRef。其中的NSRunLoop和CFRunLoopRef都代表着RunLoop对象。NSRunLoop是基于CFRunLoopRef的一层OC包装(这也是使用OC方式和C语言方式打印出来的主线程不同的原因,主线程真正的地址是使用C语言打印出来的地址)。CFRunLoopRef是开源的。CFRunLoopRef下载链接

获取当前线程的RunLoop对象:

1
2
3
4
//OC方法,获取当前线程的RunLoop对象
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//C语言
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();

获取主线程的RunLoop对象:

1
NSRunLoop *runloop = [NSRunLoop mainRunLoop];

RunLoop与线程的关系

  • 每条线程都有唯一的一个与之对应的RunLoop对象。
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取该线程时创建与之对应的RunLoop对象。
  • RunLoop会在线程结束时销毁。
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。

RunLoop底层实现

Core Foundation中关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

RunLoop在源码中的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;//线程对象
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

我们将主要的成员变量挑选出来,简化一下:

1
2
3
4
5
6
7
struct __CFRunLoop {
pthread_t _pthread;//线程对象
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
  • CFMutableSetRef _modes:_modes是一个类似数组的集合,但是其所储存的数据类型是CFRunLoopModeRef,而且是无序的。
  • CFRunLoopModeRef _currentMode:当前模式。

CFRunLoopModeRef的定义如下:

1
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

那么__CFRunLoopMode又是怎么定义的呢?

1
2
3
4
5
6
7
8
struct __CFRunLoopMode {
//部分成员变量
CFStringRef _name;//mode名称
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
  • CFMutableSetRef _sources0 和 CFMutableSetRef _sources1:这两个集合中存储的是 CFRunLoopSourceRef类型的数据。
  • CFMutableArrayRef _observers:该集合中存储的是CFRunLoopObserverRef类型的数据。
  • CFMutableArrayRef _timers:存储的是CFRunLoopTimerRef类型的数据。

RunLoop相关的类之间的逻辑关系.png

综上所述可知,开头列出的5个类它们之间的关系是:CFRunLoopRef(__CFRunLoopMode)中存储着CFRunLoopModeRef类型的数据,CFRunLoopModeRef中存储着CFRunLoopSourceRef、 CFRunLoopTimerRef、CFRunLoopObserverRef类型的数据。

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式。一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。RunLoop启动时只能选择其中的一个Mode作为CurrentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,不同Mode中的Source0/Source1/Timer/Observer彼此分隔,互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出该Mode。

CFRunLoopModeRef的5种Mode

CFRunLoopModeRef中一共有5种Mode,分别是:

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
  • kCFRunLoopCommonModes:通用模式,一旦设置此模式表示同时监听kCFRunLoopDefaultMode和UITrackingRunLoopMode这两种模式。其实kCFRunLoopMode并不是一种真的模式,它只是一个标记。将定时器的RunLoop模式设置为kCFRunLoopCommonModes意味着定时器(NSTimer)可以在“CFMutableSetRef commonModes数组中存放的模式”下运行,而且kCFRunLoopDefaultMode和UITrackingRunLoopMode都存放在CFMutableSetRef commonModes数组中。

  • UIInitializationRunLoopMode:在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。

  • GSEventReceiveRunLoopMode:用来接受系统事件的内部 Mode,通常用不到。

CFRunLoopModeRef的5种Mode中最常见常用的模式是kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode这2种。

添加Observer监听RunLoop的所有状态

RunLoop的所有状态包括:

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//即将从休眠中唤醒
kCFRunLoopExit = (1UL << 7),//即将推出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

添加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
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;

case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;

case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;

default:
break;
}
});
//添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
//释放observer
CFRelease(observer);

RunLoop的运行逻辑

RunLoop的大体上的运行逻辑其实就是循环处理某种模式下的Source0、Source1、Timers、Observers。那么Source0、Source1、Timers、Observers具体表示什么呢?

  • Source0:

  (1)触摸事件处理;

  (2)performSelector:onThread:方法

  • Source1:

  (1)基于Port的线程间通信;

  (2)系统事件捕捉

  • Timers:

  (1)NSTimers

  (2)performSelector:WithObject:afterDelay:

  • Observers:

  (1)用于监听RunLoop的状态

  (2)UI刷新(BeforeWaiting在休眠之前刷新UI)

  (3)Autoreleasepool(BeforeWaiting)

RunLoop具体的运行逻辑是这样的:

RunLoop的运行逻辑.png

  • (1)通知Observers:进入RunLoop;
  • (2)通知Observers:即将处理Timers;
  • (3)通知Observers:即将处理Sources;
  • (4)处理Blocks(比如通过CFRunLoopPerformBlock函数添加的Block);
  • (5)处理Source0(可能会再次处理Blocks);
  • (6)如果存在Source1,就跳转到第8步,处理Source1;
  • (7)通知Observers:开始休眠,不再占用CPU资源(等待消息唤醒);
  • (8)通知Observers:结束休眠(被某个消息唤醒);

  ✅处理Source1

  ✅处理Timer

  ✅处理GCD Async To Main Queue

  • (9)处理Blocks;

  • (10)根据前面的执行结果,决定如何操作:有可能不退出当前的RunLoop,而是回到第2步继续执行;也有可能结束当前RunLoop,切换到其他的模式。如果切换到其他模式的话,则会执行(11)步;

  • (11)通知Observers:退出RunLoop。

RunLoop休眠的实现原理

RunLoop休眠的实现原理.png

RunLoop休眠(线程阻塞)的实现原理:平时执行应用层面的代码时处于用户态,当在用户态调用mach_msg()函数时会自动转化到内核态,并调用内核态的mach_msg()函数,此时如果没有消息需要处理就让线程休眠,如果有消息需要处理就唤醒线程,回归到用户态调用应用层面的API去处理消息。

RunLoop在实际开发中的应用

  • (1)控制线程生命周期(线程保活)

线程保活的应用场景:频繁执行一个任务或者多个串行(非并发)任务,可以采用线程保活的方式。线程保活的方式比传统的“创建线程-销毁线程-再创建线程-再销毁…”更节省CPU资源且更高效。比如AFNetworking中后台网络请求就使用了线程保活这种技术。

  • (2)解决NSTimer在滑动时停止工作(失效)的问题

  NSTimer在滑动时失效的原因是NSTimer默认是工作在NSDefaultRunLoopMode模式下,而当我们滑动时,RunLoop会退出NSDefaultRunLoopMode模式,并进入UITrackingRunLoopMode模式,所有NSTimer失效。

  • (3)监控应用卡顿
  • (4)性能优化

RunLoop总结

1.讲讲RunLoop,项目中用过吗?

  • 控制线程生命周期(线程保活)
  • 解决NSTimer在滑动时停止工作(失效)的问题

2.RunLoop内部实现逻辑是怎样的?

答:参照RunLoop具体的运行逻辑示意图

3.RunLoop和线程的关系是怎样的?

  • 每条线程都有唯一的一个与之对应的RunLoop对象。
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取该线程时创建与之对应的RunLoop对象。
  • RunLoop会在线程结束时销毁。
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。

4.timer和RunLoop是怎样的关系?

  • 从底层数据结构来看,RunLoop的CFRunLoop结构体中存储着 CFMutableSetRef _modes,_modes是一个类似数组的集合,其所储存的数据类型是CFRunLoopModeRef, CFRunLoopModeRef结构体中存放着CFMutableArrayRef _timers。此外,如果timer被设置为 kCFRunLoopCommonModes模式,那么timer也将被存放在 CFRunLoop结构体中的 CFMutableSetRef _commonModeItems数组中。
  • 从RunLoop的运行逻辑来讲,timer会通知Observers结束休眠,唤醒线程来处理timer消息。

5.程序中添加每3秒响应一次的NSTimer,当拖动tableView时,timer可能无法响应要怎么解决?

NSTimer在滑动时失效的原因是NSTimer默认是工作在NSDefaultRunLoopMode模式下,而当我们滑动时,RunLoop会退出NSDefaultRunLoopMode模式,并进入UITrackingRunLoopMode模式,所有NSTimer失效。

【注意】[NSTimer scheduledTimerWithTimeInterval: repeats:block:]方法会自动将定时器添加到主线程的NSDefaultRunLoopMode模式下,如果要自定义RunLoop模式的话,可以使用timerWithTimeInterval方法创建定时器对象,并将定时器添加到当前线程的NSRunLoopCommonModes模式下(实际上是将timer定时器添加到了NSRunLoopCommonModes 模式下的CFMutableSetRef _commonModeItems数组中),这样就能解决timer失效的问题。代码如下:

1
2
3
NSTimer *timer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

6.RunLoop是怎么响应用户操作的,具体流程是什么样的?

当用户点击屏幕时,RunLoop内部的Source1会捕捉到该触屏事件,并将该事件包装成事件队列eventQueue交给Source0中进行处理。

7.说说RunLoop的几种状态

kCFRunLoopEntry = (1UL << 0),   //即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//即将从休眠中唤醒
kCFRunLoopExit = (1UL << 7),//即将推出RunLoop

8.RunLoop的mode作用是什么?

RunLoop常见的mode有2种:kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode。kCFRunLoopDefaultMode是默认模式,通常主线程是在这个模式下运行;UITrackingRunLoopMode用于ScrollView追踪触摸滑动,保证界面滑动时不受其他mode影响。

mode的作用是将不同模式下的Source0/Source1/Timer/Observer隔离开来,互不影响,这样就提高了执行效率和滑动流畅性。