iOS开发-RunLoop

今天,来讲一个iOS开发中比较基础的东西,RunLoop

RunLoop 是什么

RunLoop,可以分解成两个简单的英文单词 “Run” 和 “Loop”,在程序中解释这两个单词就是运行和循环的意思,所以可以把RunLoop理解成循环运行,直接解释就是不停的跑圈。

那么用代码来说的话,RunLoop实际上就是一个do-while循环,在这个循环中不断处理各种事件,比如UI事件,触摸事件等等,如果在没有事情做得时候就会进入休眠模式。

先放一张苹果官方文档中的图,后边我们也会介绍这个图中所表达的意思。

logo

RunLoop 有什么用

那知道了 RunLoop 就是一直跑圈的一个东西,那他到底有什么用呢?根据他的特性,RunLoop主要的作用有4点。

  • 1.使程序一直处于运行状态。
  • 2.决定什么时候处理什么Event。
  • 3.调用解耦。
  • 4.节省CPU时间。

RunLoop 与线程

RunLoop和线程是一一对应的,我们不能自己创建RunLoop对象,但是我们可以获取系统提供的RunLoop对象。当我们获取RunLoop时,系统就会创建他。

主线程的RunLoop会在应用启动的时候完成启动,而其他线程在刚创建的时候并没有RunLoop,如果你不主动获取,那他一直都不会有,当你第一次获取他的时候,RunLoop才会被创建。而当线程结束的时候,这个线程中的RunLoop也会被销毁掉。

说了这么多,那么你可能会问,我在开发的时候怎么样能知道什么时候启动了RunLoop呢?那RunLoop在真正开发的时候出现在哪里呢?

别急,我们可以通过一个小例子来看看。比如我现在在ViewController里添加一个按钮,然后跑起来执行给按钮的点击事件响应函数clickedBtnClicked添加一个断点,然后我们来看调用的堆栈信息:

logo

我们从下往上看:

19、18 start 是dyld调用的,让我们程序跑起来。

然后17运行了main函数,main函数中的方法

1
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

调用了UIApplicationMain

16行就是UIApplicationMain的内容。

然后15行是一个发出点击事件的类,这里我们不多介绍。

14~10行就是我们今天说的主角RunLoop。

再往上就是一些业务层的调用,直到最后的ViewController调用clickedBtnClicked方法。

那么这几个RunLoop的具体是怎么回事儿?我们接着往下看。

RunLoop 的对外接口

RunLoop 在Foundation层和Core Foundation中都有对应的接口。分别是:

  • Foundation -> NSRunLoop
  • Core Foundation -> CFRunLoop

但是NSRunLoop完全是CFRunLoop面向对象的封装,但是线程不安全,所以这里我们就仅对CFRunLoop进行说明,但是需要注意的一点是,NSRunLoop中运行RunLoop的三个方法中,前两个方法是没有办法使用CFRunLoop中的方法停止的。

1
2
3
4
5
6
//运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制,不可以使用CFRunLoopStop(runloopRef);停止。
- (void)run;
//运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式,不可以使用CFRunLoopStop(runloopRef);停止。
- (void)runUntilDate:(NSDate *)limitDate;
//运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的,可以使用CFRunLoopStop(runloopRef);停止。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

那接下来我们来说CFRunLoop。CFRunLoop下有5个相关的类:

  • CFRunLoopRef:代表RunLoop对象
  • CFRunLoopModeRef:代表RunLoop对象的模式
  • CFRunLoopSourceRef:代表RunLoop对象的输入源
  • CFRunLoopTimerRef:代表RunLoop对象的定时源
  • CFRunLoopObserverRef:观察者,监听RunLoop的状态变化

用一张图表明这五个类的关系就是:

logo

其中每个RunLoop中对应多个Mode,每个Mode下又包含多个Source/Timer/Observe。

每次调用RunLoop的主函数时都会指定其中一个Mode,这个Mode被称为CurrentMode。

如果想要切换Mode时必须先退出Loop,再重新指定一个Mode进入,这样做主要是为了隔离开不同组的Source/Timer/Observe,使其不会互相发生影响。

CFRunLoopRef

CFRunLoopRef就是RunLoop对象类。

获取RunLoop的函数:

1
2
3
4
//获取当前线程中的RunLoop
CFRunLoopGetCurrent(void);
//获取主线程中的RunLoop
CFRunLoopGetMain(void);

操作RunLoop的函数:

1
2
3
4
5
6
7
8
9
10
//运行RunLoop
void CFRunLoopRun(void);
//运行RunLoop,指定运行Mode,时间和是否在处理输入源退出标志,返回值为exit的原因
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
//判断RunLoop是否在等待
Boolean CFRunLoopIsWaiting(CFRunLoopRef rl);
//唤醒RunLoop
CF_EXPORT void CFRunLoopWakeUp(CFRunLoopRef rl);
//停止RunLoop
CF_EXPORT void CFRunLoopStop(CFRunLoopRef rl);

比如这个Demo:

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
- (void)demoNo1
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(demoNo1Timer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
CFRunLoopRunResult result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
switch (result)
{
case kCFRunLoopRunFinished:
NSLog(@"kCFRunLoopRunFinished");
break;
case kCFRunLoopRunStopped:
NSLog(@"kCFRunLoopRunStopped");
break;
case kCFRunLoopRunTimedOut:
NSLog(@"kCFRunLoopRunTimedOut");
break;
case kCFRunLoopRunHandledSource:
NSLog(@"kCFRunLoopRunHandledSource");
break;
default:
break;
}
});
}
- (void)demoNo1Timer:(NSTimer *)timer
{
//输出 kCFRunLoopRunStopped
//主动停止了runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopStop(runloop);
//输出 kCFRunLoopRunFinished
//runloop中没有事件了,runloop结束
[timer invalidate];
}

CFRunLoopModeRef

系统一共提供了5中Mode:

  • 1.kCFRunLoopDefaultMode:系统中默认的运行模式。
  • 2.UITrackingRunLoopMode:用户交互事件模式。比如ScrollView滑动时,为了保证用户体验流畅,会切换到这个模式。
  • 3.UIInitializationRunLoopMode:刚启动的时候进入的第一个Mode,启动完成后就不再使用了。
  • 4.GSEventReceiveRunLoopMode:接受内部事件,通常用不到。
  • 5.kCFRunLoopCommonModes:这个不是一个真正的mode。

所以真正我们平时用到的Mode就只有两种,kCFRunLoopDefaultMode和UITrackingRunLoopMode。

而另外一种kCFRunLoopCommonModes主要是用来标记,每当RunLoop的内容发生变化时,RunLoop都会自动将_commonModeItems 里的Source/Timer/Observer同步到具有Common标记的Mode里。

比如说我们应该都会遇到过的一个问题,就是在添加NSTimer时,如果将它加入到kCFRunLoopDefaultMode时,滑动页面中的ScrollView就会导致计时器失效,这就是因为滑动时runloop会自动改变到UITrackingRunLoopMode下,而在kCFRunLoopDefaultMode下的timer就失效了,那如果将它加入到kCFRunLoopCommonModes中的话,就可以修复这个问题,就是因为kCFRunLoopCommonModes中标记了
kCFRunLoopDefaultMode和UITrackingRunLoopMode这两种Mode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)demoNo2
{
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(demoNo2Timer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, WIDTH, HEIGHT/2)];
scrollView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
scrollView.contentSize = CGSizeMake(0, HEIGHT*2);
[self.view addSubview:scrollView];
}
- (void)demoNo2Timer:(NSTimer *)timer
{
NSLog(@"Tik Tok");
}

CFRunLoop中也提供了管理Mode的接口:

1
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);

CFRunLoopSourceRef

这个类是事件产生的地方,Source有两种版本,Source0和Source1。

Source0 只包含了一个回调,并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop) 来唤醒这个RunLoop,让他处理事件。

Source1 包含了一个mach_port和一个回调,被用于内核和其他线程互相发送消息。这种Source能主动唤醒RunLoop的线程。

比如说之前的点击事件的堆栈信息中就可以看到,这个RunLoop的Source就是Source0类型的。

logo

CFRunLoopTimerRef

这个类是基于时间的触发器,在之前的官方RunLoop模型中也有他的存在,常用的计时器NSTimer即使基于他来封装的。

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerDo:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

比如这段代码加入到runloop中之后,runloop就会注册一个对应的时间点(2s),当时间点到时,runloop就会唤醒这个回调方法(timerDo:)。

说到计时器,除了NSTimer之外,平时还可能会用到GCD的计时器,或者CADisplayLink,他们与基于runloop的NSTimer还是有一些差别的。

比如GCD的计时器:

1
2
3
4
5
6
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatchQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, intervalInSeconds * NSEC_PER_SEC, leewayInSeconds * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
code to be executed when timer fires
});
dispatch_resume(timer);

可以用精确的参数,不用依赖于runloop的mode,性能消耗更小。

而CADisplayLink主要是跟随屏幕的刷新频率保持一致(1s/60次)。

CFRunLoopObserverRef

这个类是一个观察者,每个观察者都包含了一个回调,每当runloop的状态发生变化时,观察者就能通过这个回调得知信息。回调的状态有:

1
2
3
4
5
6
7
8
9
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 // 监听全部状态改变
};

比如我们添加一个观察者,观察主线程中的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
- (void)demoNo3
{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 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;
case kCFRunLoopAllActivities:
NSLog(@"kCFRunLoopAllActivities");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(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
2018-03-30 17:21:20.040588+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.040753+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.040945+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.041031+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.041164+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.041264+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.041839+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.042523+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.044866+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.047332+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.048702+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.048944+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.051433+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.052578+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.052819+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.054099+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.056095+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.057081+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.058002+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.058749+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.061503+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting
2018-03-30 17:21:20.502012+0800 runloopDemo[71763:4374788] kCFRunLoopAfterWaiting
2018-03-30 17:21:20.502446+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:20.502533+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:20.502628+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting
2018-03-30 17:21:21.457896+0800 runloopDemo[71763:4374788] kCFRunLoopAfterWaiting
2018-03-30 17:21:21.459550+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:21:21.459788+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:21:21.459969+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting
2018-03-30 17:22:00.010817+0800 runloopDemo[71763:4374788] kCFRunLoopAfterWaiting
2018-03-30 17:22:00.114676+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:22:00.114791+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:22:00.115048+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:22:00.115179+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:22:00.115747+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting
2018-03-30 17:23:00.007480+0800 runloopDemo[71763:4374788] kCFRunLoopAfterWaiting
2018-03-30 17:23:00.011030+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:23:00.011167+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:23:00.011278+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting
2018-03-30 17:24:00.002525+0800 runloopDemo[71763:4374788] kCFRunLoopAfterWaiting
2018-03-30 17:24:00.005001+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:24:00.005130+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:24:00.005315+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting
2018-03-30 17:25:00.002704+0800 runloopDemo[71763:4374788] kCFRunLoopAfterWaiting
2018-03-30 17:25:00.004012+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeTimers
2018-03-30 17:25:00.005443+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeSources
2018-03-30 17:25:00.007229+0800 runloopDemo[71763:4374788] kCFRunLoopBeforeWaiting

到最后停在了 即将进入休眠的状态。

如果这个时候runloop中一个Source/Timer/Observer都没有了的话,runloop就会退出。

RunLoop 的内部实现

先上一张YY大神整理的图:

logo

这张图说明了一切,再放一个加了注释的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
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);
}

相信看完了这两个YY大神整理的内容之后,runloop的实现原理就已经讲的很清楚了。总结一下就是runloop内部和之前说的一样,就是一个循环,只要有Timer/Source/Observer的时候循环就会一直执行,当操作结束之后,runloop就会停下来,进入休眠状态等待唤醒。

RunLoop 的本质

runloop的本质就是 mach port 和 mach_msg()。

Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,RunLoop是通过mach_msg()函数发送消息,如果没有port消息,内核就会将线程置于等待状态,如果有消息,就会判断消息类型处理事件,并通过modeItem的callback回调。

logo

RunLoop 在苹果中的使用

Autorelease Pool

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

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

第二个Observer监听了两个事件:

  • BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()来释放旧的池并创建新的池。
  • Exit(即将退出)时调用_objc_autoreleasePoolPop()来释放池。这个Observer的优先级最低,保证释放池在所有的操作之后。

事件响应

当一个硬件事件(触摸/锁屏/摇晃/加速)发生后,首先有IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接受,之后由mach port转发给需要的App进程。

苹果注册了一个Source1来接受系统事件,通过回调函数触发Source0(所以Event实际上是基于Source0)的,调用_UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。

手势识别

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

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

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

界面刷新

当UI发生改变时(Frame变化,UIView/CALayer的结构变化)时,或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理。

苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在他的回调函数里会遍历所有待处理的UIView/CALayer来执行实际的绘制和调整,并更新UI界面。

定时器

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

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

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

GCD任务

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

网络请求

关于网络请求的接口:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLSession 是 iOS7 中新增的接口,也用到NSURLConnection的loader线程。所以还是以NSURLConnection为例。

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

logo

RunLoop 的应用

说了这么多,那我们在平时的开发中最有可能用到的RunLoop应用是什么时候呢,比如之前说的NSTimer的问题,还有就是比如AFN中用到的开辟一条常驻的线程。

接下来我们就来做了一个使用runloop常驻线程的例子。

首先我们创建一条线程并让他跑起来:

1
2
3
4
5
- (void)demoNo4
{
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(demoNo4Thread) object:nil];
[self.myThread start];
}
1
2
3
4
5
6
7
- (void)demoNo4Thread
{
NSLog(@"demo No 4 running...");
//代表线程已经结束
NSLog(@"demo No 4 thread is over");
}

接下来我们添加一个按钮,当点击 按钮的时候在刚刚我们建立的线程中调用方法demoNo4Thread2

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
- (void)demoNo4
{
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(demoNo4Thread) object:nil];
[self.myThread start];
UIButton *demo4Btn = [UIButton buttonWithType:UIButtonTypeCustom];
demo4Btn.frame = CGRectMake(0, HEIGHT/2, WIDTH, 50);
[demo4Btn setTitle:@"click me" forState:UIControlStateNormal];
[demo4Btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[demo4Btn addTarget:self action:@selector(demo4BtnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:demo4Btn];
}
- (void)demo4BtnClicked
{
[self performSelector:@selector(demoNo4Thread2) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)demoNo4Thread
{
NSLog(@"demo No 4 running...");
//代表线程已经结束
NSLog(@"demo No 4 thread is over");
}
- (void)demoNo4Thread2
{
NSLog(@"I can run");
}

这时候我们会发现,程序运行起来只会打印:

1
2
2018-03-31 17:19:23.870449+0800 runloopDemo[77990:4833887] demo No 4 running...
2018-03-31 17:19:23.870730+0800 runloopDemo[77990:4833887] demo No 4 thread is over

因为runloop中并没有事情,导致myThread已经结束了,所以点击按钮时,并没有办法唤起方法。

这时候,我们在demoNo4Thread中给当前的runloop添加一个port,并让他跑起来。

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
- (void)demoNo4
{
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(demoNo4Thread) object:nil];
[self.myThread start];
UIButton *demo4Btn = [UIButton buttonWithType:UIButtonTypeCustom];
demo4Btn.frame = CGRectMake(0, HEIGHT/2, WIDTH, 50);
[demo4Btn setTitle:@"click me" forState:UIControlStateNormal];
[demo4Btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[demo4Btn addTarget:self action:@selector(demo4BtnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:demo4Btn];
}
- (void)demo4BtnClicked
{
[self performSelector:@selector(demoNo4Thread2) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)demoNo4Thread
{
NSLog(@"demo No 4 running...");
//添加一个port,并让runloop跑起来
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
//代表线程已经结束
NSLog(@"demo No 4 thread is over");
}
- (void)demoNo4Thread2
{
NSLog(@"I can run");
}

这时候再运行程序,就可以看到,demo No 4 thread is over没有打印,点击按钮的时候,也能响应对应的方法。

1
2
2018-03-31 17:22:29.415023+0800 runloopDemo[78117:4840152] demo No 4 running...
2018-03-31 17:22:41.341009+0800 runloopDemo[78117:4840152] I can run

这样一条常驻的线程就已经开辟好了。

最后

以上就是这次文章的全部内容了,其中大部分内容都来自于YY大神的博客中,仅供个人学习使用,如果有什么不对的地方还请各位大佬多多批评。

Demo地址在这里

参考文档

基于runloop的线程保活、销毁与通信

iOS多线程–彻底学会多线程之『RunLoop』

Run Loops

深入理解RunLoop

【iOS程序启动与运转】- RunLoop个人小结

iOS线下分享《RunLoop》by 孙源@sunnyxx