(部分内容来自my answer to a similar question。)
事实证明,运行循环很复杂,一个简单的问题,比如“drawRect: 是否发生在运行循环的末尾?”没有简单的答案。
CFRunLoop 是open-source CoreFoundation package 的一部分,所以我们可以看看它到底是什么。运行循环大致如下:
while (true) {
Call kCFRunLoopBeforeTimers observer callbacks;
Call kCFRunLoopBeforeSources observer callbacks;
Perform blocks queued by CFRunLoopPerformBlock;
Call the callback of each version 0 CFRunLoopSource that has been signaled;
// Touch events are a version 0 source in iOS 8.0.
// CFSocket is a version 0 source.
if (any version 0 source callbacks were called) {
Perform blocks newly queued by CFRunLoopPerformBlock;
}
if (I didn't drain the main queue on the last iteration
AND the main queue has any blocks waiting)
{
remove all blocks from the main queue
execute all the blocks just removed from the main queue
} else {
Call kCFRunLoopBeforeWaiting observer callbacks;
// Core Animation uses a BeforeWaiting observer to perform layout and drawing.
Wait for a CFRunLoopSource to be signalled
OR for a timer to fire
OR for a block to be added to the main queue;
Call kCFRunLoopAfterWaiting observer callbacks;
if (the event was a timer) {
call CFRunLoopTimer callbacks for timers that should have fired by now
} else if (event was a block arriving on the main queue) {
remove all blocks from the main queue
execute all the blocks just removed from the main queue
} else {
look up the version 1 CFRunLoopSource for the event
if (I found a version 1 source) {
call the source's callback
}
// Interface orientation changes are a version 1 source in iOS 8.0.
}
}
Perform blocks queued by CFRunLoopPerformBlock;
}
Core Animation 以 2000000 的顺序注册了一个 kCFRunLoopBeforeWaiting 观察者(尽管没有记录;您可以通过打印 [NSRunLoop mainRunLoop].description 来弄清楚)。这个观察者提交当前的CATransaction,它(如果需要)执行布局(updateConstraints 和layoutSubviews)然后绘制(drawRect:)。
请注意,在执行 BeforeWaiting 观察者之前,运行循环可以评估 while(true) 中的 true 两次。如果它调度计时器或版本 1 的源,并将块放在主队列中,则运行循环将在调用 BeforeWaiting 观察者之前循环两次(并且它会调度版本 0 的源两次)。
系统混合使用版本 0 源和版本 1 源。在我的测试中,触摸事件是使用版本 0 源传递的。 (你可以通过在触摸处理程序中放置一个断点来判断;堆栈跟踪包含__CFRunLoopDoSources0。)进入/离开前台之类的事件是通过CFRunLoopPerformBlock 调度的,所以我不知道真正提供它们的是哪种来源。界面方向更改通过版本 1 源提供。 CFSocket is documented to be a version 0 source.(很可能NSURLSession和NSURLConnection在内部使用CFSocket。)
请注意,运行循环是结构化的,因此每次迭代只会出现这些分支之一:
- 就绪计时器触发,或
- 阻止
dispatch_get_main_queue() 运行,或
-
单个版本 1 源被调度到其回调。
之后,任意数量的版本 0 源都可以调用它们的回调。
所以:
- 布局总是在绘制之前发生,如果在核心动画观察器运行时两者都处于未决状态。 CA 观察者在计时器、主队列块或外部事件回调运行后运行。
- 主 GCD 队列的优先级高于计时器和版本 1 源,如果运行循环在循环的前一回合没有耗尽主队列。
- 如果这三个都准备好了,计时器对主队列和版本 1 源具有优先权。
- 主队列的优先级高于版本 1 源,应该都准备好了。
还请记住,您可以随时使用 layoutIfNeeded 请求立即布局。