余额不足.

深入浅出RunLoop

字数统计: 2.8k阅读时长: 11 min
2019/08/05 Share

楔子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

DispatchQueue.global().async {
print("1")
self.perform(#selector(self.aaa), with: nil)
self.perform(#selector(self.abc), with: nil, afterDelay: 0)
print("4")
}
}

@objc func aaa() {
print("2")
}

@objc func abc() {
print("3")
}
}

在全局并发队列分别执行四个输出指令,问输出结果会是怎样的?会是1234或者1243吗?
run一下,让编译器告诉我们结果。让人大跌眼镜的是输出结果是124,为什么abc方法没有执行?而aaa凭什么又执行了?
带着疑问我们去溯源,首先是func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged<AnyObject>!,该方法隶属于NSObjectProtocol中的协议方法,相类似的还有:

1
2
3
4
5
func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>!

func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged<AnyObject>!

func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged<AnyObject>!

NSObjectProtocol中却没有perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)这个方法,它归属于RunLoopNSObject的拓展中:

1
2
3
4
5
6
7
8
9
extension NSObject {
open func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval, inModes modes: [RunLoop.Mode])

open func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)

open class func cancelPreviousPerformRequests(withTarget aTarget: Any, selector aSelector: Selector, object anArgument: Any?)

open class func cancelPreviousPerformRequests(withTarget aTarget: Any)
}

看着相似的两个方法结果却大相径庭。

那要怎样操作才能让3也被输出出来呢?如果这四条语句是挂在主线程下执行的话是可以正常输出1243的,而在全局并发队列中却不能正常执行是因为子线程的RunLoop是默认没有被启动的,等于是你跟一个睡着的人说:“喂,记得打扫卫生!”,然后你就出门了,发现回家之后家里还是一团乱糟糟。所以要想让他干活,先把他叫醒:

1
2
3
4
5
6
7
DispatchQueue.global().async {
print("1")
self.perform(#selector(self.aaa), with: nil)
self.perform(#selector(self.abc), with: nil, afterDelay: 0)
print("4")
RunLoop.current.run()
}

通过run()方法即可将子线程的runloop启动,block内的操作即可被正常执行。
这里的afterDelay为0的情况下并不会让程序立马执行,而是在线程的RunLoop中排队,详见performSelector:withObject:afterDelay:

概念

RunLoop是一个协调接收事件并安排事件能妥当处理的循环,目的是为了让当有任务需要执行时时使线程处于干活的状态,没有任务的时候就睡觉去。

剖析

RunLoop结构及事件源

左边是RunLoop的循环结构,在循环中接收事件并安排合理的人员去处理该事件。
右边则是事件源,输入源传递异步事件,通常这些事件来自另一个线程或者不同的应用。定时器输入源传递同步事件,发生在预定时间或重复间隔。

当然,仅仅能处理这些事件源还是不够强大的,RunLoop还生成了运行循环的行为通知,注册一个观察者接收这些通知在合适的位置即可轻松去处理一些你想做的事情,例如在beforeSourcesbeforeWating这些个时间段你可以监测到App时候有发生卡顿等功能。

RunLoopModes

1
2
3
4
5
6
7
8
9
10
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
...
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
}

一个RunLoop的Mode包含了输入源、定时器以及观察者,即所谓的Source,Timer,Observer,每次RunLoop执行时都指定了一个特定的Mode,在RunLoop接收和处理事件的过程中,Observer仅仅观察和处理该Mode下的事件。

比如一个TableView上集成了自动轮播控件,而当TableView滑动时自动轮播图就不会自动轮播了,原因是因为当滑动时当前RunLoop的Mode为TrackingMode,而自动轮播控件处于DefaultMode,当自动轮播图的Timer向RunLoop发送事件的时候DefaultMode处于无人接听的状态,即通知没有到位任务就没有人执行了。

1
2
3
CF_INLINE void __CFRunLoopModeLock(CFRunLoopModeRef rlm) {
pthread_mutex_lock(&(rlm->_lock));
}

在CFRunLoop的源码中发现在FindMode、DoBlocks、DoObserver、DoSource0、DoSource1、DoTimer、CFRunLoopRun等方法中都有对当前Mode进行加锁。即可认为当RunLoop正在执行任务时是不可切换Mode的。

当你想对事件进行过滤的时候可自定义设定一个Mode用于采集你想要的事件埋点,也可在子线程中使用自定义Mode来防止低优先级的事件源在关键的时刻抢了资源。

Mode 名称 描述
Default
NSDefaultRunLoopMode (Cocoa)
kCFRunLoopDefaultMode (Core Foundation)
默认的Mode,大多数情况下应该使用该Mode开始 RunLoop并配置 input source
Connection NSConnectionReplyMode (Cocoa) Cocoa将此Mode与NSConnection对象结合使用以监视回调。应该几乎利用不到这个Mode
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa用此Mode来标识用于模态面板的事件
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa使用此Mode在屏幕滑动期间和其它用户界面跟踪期间限制传入事件
Common modes
NSRunLoopCommonModes (Cocoa)
kCFRunLoopCommonModes (Core Foundation)
这是一组可配置的常用Mode。将输入源与此Mode相关联也会将其与组中的每个Mode相关联。对于Cocoa应用程序,此集合默认包括Default,Modal和Event tracking模式。Core Foundation最初只包含默认Mode。您可以使用该CFRunLoopAddCommonMode功能将自定义Mode添加到集合中。

Quesetion

Q:这个CommonModes到底是个啥?很多文章都说他仅仅是个占位用的Mode,所以它到底属不属于一个真正的Mode?
A:在__CFRunLoopCreate(pthread_t t)中有这样的一段代码

1
2
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);

loop->_commonModes是一个Set,里面可以存放各种Mode,在runloop的创建过程中已经把defaultMode存进了CommonMode中了。

输入源

基于上图我们可以得知事件源分为基于Port的源,自定义源以及Cocoa中的方法执行源(楔子中的例子)

Timer

就是我们所认知的定时器,在预设的时间将事件传递给线程。当然这个时间不一定是实时的,当所在线程卡顿时或者在自动轮播图那个例子的情况下Timer的事件源都可能会出现问题。

Observer

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), // RunLoop即将处理定时器
kCFRunLoopBeforeSources = (1UL << 2), // RunLoop即将处理输入源
kCFRunLoopBeforeWaiting = (1UL << 5), // RunLoop即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // RunLoop被唤醒,但在处理唤醒它的事件之前
kCFRunLoopExit = (1UL << 7), // 退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

倘若你需要对RunLoop的这些状态进行监听,调用CFRunLoopObserverCreate()并用CFRunLoopObserverRef接收返回值即可

1
2
3
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);

CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

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
CFRunLoopRunSpecific() {
...
// 通知观察者将要进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 即将退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
...
}

__CFRunLoopRun() {
...
do {
// 通知观察者将要处理Timer
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知观察者将要处理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(rl, rlm);
// 如果有Source0,处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

// 如果有Source1则跳转执行handle_msg()
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}

// 即将进入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);

...

// RunLoop被唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 处理消息
handle_msg:
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
} else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// 收到Timer唤起RunLoop
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 开始处理Timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}
...
// 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
...
else {
// 收到Source唤起RunLoop
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// 开始处理Source1
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 处理加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);

if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部调用者强制停止了
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
}

source0:非port的事件源
source1:基于port的事件源
其实记住0和1的Bool值就能方便的记住两个source的区别了,更通俗的解释:
source0:自定义事件或者是performSelector等事件
source1:来自系统内核或者其他进程或线程的事件

Question

Q:当APP处于静止(休眠)状态,这时候去点击屏幕,事件的响应流程是怎么样的?
A:IOKit封装IOHIDEvent将event传递给SpringBoard,再通过mach_port传递给当前APP进程,这时候触发RunLoop的source1,然后source1的回调触发source0,之后就脱离RunLoop的范畴就不往下分析了,感兴趣的同学可前往iOS触摸事件全家桶

Q:RunLoop正在休息时处于什么状态?
A:也就是图中所示的第7个步骤,当事件处理完后,系统会去调用mach_msg_trap()触发内核状态的切换,使程序属于休眠状态。其实当我们的APP静止时,直接在Xcode中进行暂停,可以看见几乎所有线程都处于mach_msg_trap状态。

通过这个问题是不是对Source0和Source1有了比较深刻的理解了?当然带个0和1只是为了方便记忆,实际上我们应该对source的认知应该是他是否是基于port的事件源。

怎么用RunLoop?

通过上面的一些分析,对RunLoop应该有了一些认知,但日常开发中它能给予我们哪些帮助?如果您计划执行以下任何操作,则需要启动RunLoop:

  1. 使用端口或自定义输入源与其他线程通信。
  2. 在线程上使用定时器。
  3. 使用performSelector。
  4. 保持线程以执行定期任务。

主线程卡顿监测

根据上面的RunLoop的逻辑处理流程我们可以推测出RunLoop的事件处理事件主要在kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之间,而在这两个状态下如果发生了超时操作,那么RunLoop必然会保持kCFRunLoopBeforeSources这个状态。

假设事件A是一个会影响性能的卡顿操作,那么事件A到RunLoop的时候会先触发kCFRunLoopBeforeSources,然后RunLoop状态到达kCFRunLoopAfterWaiting,在这个状态下开辟一个子线程,设定一个阈值,让子线程先睡一会,如果子线程睡醒了而主线程依然在执行事件A,则认为事件A是个卡顿的操作。

Show me the Code:

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
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
[YTAppFluencyMonitor sharedMonitor].currentActivity = activity;
dispatch_semaphore_signal([YTAppFluencyMonitor sharedMonitor].semphore);
}

- (void)startMonitoring {
if (_isMonitoring) { return; }
_isMonitoring = YES;
CFRunLoopObserverContext context = {
0,
(__bridge void *)self,
NULL,
NULL
};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

dispatch_async(yt_event_monitor_queue(), ^{
while ([YTAppFluencyMonitor sharedMonitor].isMonitoring) {
if ([YTAppFluencyMonitor sharedMonitor].currentActivity == kCFRunLoopBeforeWaiting) {
__block BOOL timeOut = YES;
dispatch_async(dispatch_get_main_queue(), ^{
timeOut = NO;
dispatch_semaphore_signal([YTAppFluencyMonitor sharedMonitor].eventSemphore);
});
[NSThread sleepForTimeInterval: time_out_interval];
if (timeOut) {
[YTBacktraceLogger yt_logMain];
}
dispatch_wait([YTAppFluencyMonitor sharedMonitor].eventSemphore, DISPATCH_TIME_FOREVER);
}
}
});
}

- (void)stopMonitoring {
if (!_isMonitoring) { return; }
_isMonitoring = NO;

CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = nil;
}

End。

CATALOG
  1. 1. 楔子
  2. 2. 概念
  3. 3. 剖析
    1. 3.1. RunLoopModes
      1. 3.1.1. Quesetion
    2. 3.2. 输入源
    3. 3.3. Timer
    4. 3.4. Observer
    5. 3.5. RunLoop内部逻辑
      1. 3.5.1. Question
  4. 4. 怎么用RunLoop?
    1. 4.1. 主线程卡顿监测