【iOS】OC底层系列十二 - RunLoop

RunLoop保证了程序不会在最后一行代码执行结束后立即退出程序,它保证了程序的持续运行,因为只有程序在运行阶段才可以接收和处理各种事件。

一、什么是RunLoop?

RunLoop,顾名思义,运行循环的意思,在程序运行过程中循环做一些事情。

应用范畴:

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

如果没有RunLoop,执行完NSLog代码后,会即将退出程序。

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}

如果有了RunLoop,程序并不会马上退出,而是保持运行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 正常iOS程序的入口
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

// 上面代码的伪代码实现(RunLoop)
int main(int argc, const char * argv[]) {
@autoreleasepool {
int retVal 0;
do {
// 睡眠中等待消息
int message = sleep_and_wait();
// 处理消息
retVal = process_message(message);
} while (0 == retVal);
}
return 0;
}

UIApplicationMain()函数内部创建了RunLoop对象,保证了程序的持续运行。

RunLoop的基本作用:

  • 保持程序的持续运行
  • 处理App中的各种事件(比如触摸事件、定时器事件等)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  • ……

二、RunLoop对象

iOS中有2套API来访问和使用RunLoop:

  • Foundation框架:NSRunLoop(OC)
1
2
3
4
5
// 获得当前线程的RunLoop对象
[NSRunLoop currentRunLoop];

// 获得主线程的RunLoop对象
[NSRunLoop mainRunLoop];
  • Core Foundation框架:CFRunLoopRef(C)
1
2
3
4
5
// 获得当前线程的RunLoop对象
CFRunLoopGetCurrent();

// 获得主线程的RunLoop对象
CFRunLoopGetMain();

NSRunLoop和CFRunLoopRef都代表着RunLoop对象。NSRunLoop是基于CFRunLoopRef的一层OC包装(两个框架打印RunLoop对象的地址是不一样的),CFRunLoopRef是开源的(https://opensource.apple.com/tarballs/CF/)。

RunLoop与线程的关系:

  1. 每条线程都有唯一的一个与之对应的RunLoop对象。

  2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。

  3. 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建,RunLoop会在线程结束时销毁。

  4. 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。

三、RunLoop相关的类

Core Foundation中关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

以下是CFRunLoop的源码(实际内容有很多,仅摘取了有用部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread; // 当前RunLoop对应的线程
CFMutableSetRef _commonModes; // 通用模式数组(包含了默认和滑动模式),被标记通用模式后会同时支持两种模式
CFMutableSetRef _commonModeItems; // 被标记为通用模式的监听器、定时器、sources0、sources1会放到这里面(否则会放到_modes中)
CFRunLoopModeRef _currentMode; // 当前RunLoop模式(_modes中的一个)
CFMutableSetRef _modes; // 模式集合(存储着CFRunLoopModeRef类型),一个RunLoop对象可能会有多种模式
};

typedef struct __CFRunLoopMode * CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name; // Mode名称
CFMutableSetRef _sources0; // 集合(存储CFRunLoopSourceRef类型)
CFMutableSetRef _sources1; // 集合(存储CFRunLoopSourceRef类型)
CFMutableArrayRef _observers; // 监听器集合(存储CFRunLoopObserverRef类型)
CFMutableArrayRef _timers; // 定时器集合(存储CFRunLoopTimerRef类型)
};

3.1. CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式。一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。

RunLoop启动时只能选择其中一个Mode,作为currentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。

不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。

常见的2种Mode:

  • kCFRunLoopDefaultMode(对应OC的NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。
  • UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响(界面滑动时自动切换到该Mode,滑动结束后会退出该Mode到默认的Mode)。

注意:NSDefaultRunLoopModeUITrackingRunLoopMode才是真正存在的模式。NSRunLoopCommonModes并不是一个真的模式,它只是一个标记。被标记为通用模式的RunLoop,默认和滑动两种模式都支持。

3.2. CFRunLoopObserverRef

RunLoop的状态:

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

添加Observer监听RunLoop的所有状态(添加RunLoop的Observer只有C语言实现,没有OC版本):

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
// 创建Observer
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(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
// 释放Observer
CFRelease(observer);

添加Observer时传入的Mode如果是kCFRunLoopCommonModes,会同时监听普通模式和滚动模式。

四、RunLoop的运行逻辑

  • Source0:

    • 触摸事件处理
    • performSelector:onThread:
  • Source1

    • 基于Port的线程间通信
    • 系统事件捕捉(注意:触摸事件的捕捉是在Source1,处理是在Source0)
  • Timers

    • NSTimer
    • performSelector:withObject:afterDelay:(底层实现就是一个定时器)
  • Observers

    • 用于监听RunLoop的状态
    • UI刷新(BeforeWaiting,RunLoop进入休眠前执行UI刷新操作)
    • Autorelease pool(BeforeWaiting)

RunLoop休眠的实现原理:

用户态如果需要休眠,会进入到内核态的mach_msg()函数,在这个函数内部会自动让内核进入休眠状态(等待消息:没有消息就让线程休眠,有消息就唤醒线程)。当有消息唤醒线程时,会自动切换到用户态,让RunLoop继续处理消息。

五、RunLoop在实际开中的应用

5.1. 控制线程生命周期(线程保活)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@implementation DBThread

- (void)dealloc {
NSLog(@"%s", __func__);
}

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

DBThread *thread = [[DBThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}

- (void)run {
NSLog(@"%s", __func__);
}

@end

打印结果:

1
2
-[ViewController run]
-[DBThread dealloc]

线程启动后,run方法执行完线程就结束了。有什么办法能够让run方法执行完线程不死呢?可以使用RunLoop。

往RunLoop里面添加Source/Timer/Observer可以让线程方法不死。

1
2
3
4
5
6
7
- (void)run {
NSLog(@"%s", __func__);
// sources1用于线程间通信,port就是添加到sources1中
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"end");
}

线程执行后的输出结果:-[ViewController run]。线程没有死掉,end也没有输出,程序会一直卡在[[NSRunLoop currentRunLoop] run];,在这里RunLoop是等待唤醒的状态。[[NSRunLoop currentRunLoop] run]的API解释说开启run方法后RunLoop是无法停止的,它专门用于开启一个永不销毁的线程,run方法底层就是在不断地调用runMode:beforeDate:方法(开启一个while循环,不断地调用,相当于开启了很多个runMode:beforeDate:方法)。CFRunLoopStop(CFRunLoopGetCurrent());仅仅是停止一个runMode:beforeDate:,所以线程不会死掉,也不会执行run方法后面的代码。

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
@interface ViewController ()

@property (nonatomic, strong) DBThread *thread;
@property (nonatomic, assign) BOOL isStoped;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 如果使用initWithTarget:selector:object:方法会发生强引用,导致控制器不会执行dealloc
NSLog(@"进入控制器");
self.isStoped = NO;
__weak typeof(self) weakSelf = self;
self.thread = [[DBThread alloc] initWithBlock:^{
NSLog(@"begin:%@", [NSThread currentThread]);
// 往RunLoop里面添加Source/Timer/Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

// CFRunLoopStop(CFRunLoopGetCurrent());执行后会再次进入while,因为相当于把上次的runMode:beforeDate:停掉
// 如果在dealloc中执行stop,weakSelf会是nil的,因为控制器会被释放
while (weakSelf && !weakSelf.isStoped) {
NSLog(@"开启RunLoop");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"关闭RunLoop");
}
NSLog(@"end:%@", [NSThread currentThread]);
}];
[self.thread start];
}

// 停止子线程的RunLoop
- (void)stopThread {
// 停止标记
self.isStoped = YES;
// 停掉RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)dealloc {
NSLog(@"退出控制器");
NSLog(@"%s", __func__);
// 在子线程调用stop(不能在主线程调用)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
NSLog(@"ViewController End");
}

@end

执行结果:

1
2
3
4
5
6
7
8
9
10
进入控制器
begin:<DBThread: 0x60000033a000>{number = 8, name = (null)}
开启RunLoop
退出控制器
-[ViewController dealloc]
-[ViewController stopThread] <DBThread: 0x60000033a000>{number = 8, name = (null)}
ViewController End
关闭RunLoop
end:<DBThread: 0x60000033a000>{number = 8, name = (null)}
-[DBThread dealloc]

注意:如果使用C语言RunLoop的API把线程添加到RunLoop中,是不需要做while循环的。CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);最后一个参数代表执行完source后是否退出当前loop,传false就不需要stoped标记和while循环。

5.2. 解决NSTimer在滑动时停止工作的问题

RunLoop只能在一种模式下工作,滑动时是在TrackingMode模式下工作,滑动结束后会自动切换到默认模式,所以会发生滑动时定时器停止工作的问题。

要想解决这个问题,可以把Timer放在RunLoop的通用模式下执行,因为通用模式是可以在默认模式和滑动模式下都工作的。

1
2
3
4
5
static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", count++);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意:定时器要使用timerWithTimeInterval:方法获取Timer对象,然后把Timer放到指定模式的RunLoop中。scheduledTimerWithTimeInterval:是定制方法,会自动把Timer添加到默认模式中。

5.3. 监控应用卡顿

5.4. 性能优化


面试题1:讲讲 RunLoop,项目中有用到吗?

解答:参考章节一。

面试题2:runloop内部实现逻辑?

解答:参考章节四。

面试题3:runloop和线程的关系?

解答:参考章节二。

面试题4:timer与runloop 的关系?

解答:参考章节三中的结构图。

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

解答:把timer添加到RunLoop中,并指定模式为通用模式(包含了默认和滑动两种情况)。

面试题6:runloop 是怎么响应用户操作的, 具体流程是什么样的?

解答:在Source1里面捕捉系统消息后,把消息事件封装成EventQueue,最后放到Source0中进行处理。

面试题7:说说runLoop的几种状态

解答:参考章节3.2。

面试题8:runloop的mode作用是什么?

解答:参考章节3.1。