UIKit rendering - The run loop
On the previous post, we discovered that the commit of the current implicit transaction is responsible of our initial interrupted layout phase. So we only have one question left: when does it take place?
The answer is related to an important concept on iOS: the run loop.
I will refer to those three publications in the following article:
- Run Loop Programming Guide
- Nicolas Bouilleaud - Run, RunLoop, Run!
- Rob on StackOverflow - How do you schedule a block to run on the next run loop iteration?
Role of a run loop
The run loop is the mechanism that differentiates a command line program from an interactive application.
A run loop can be vizualised as an infinite loop attached to a thread waiting perpetually for an event. When an event comes, the run loop executes the block code associated to it (if any) on the thread and put the thread back to sleep once the work is done.
On iOS, a run loop can be attached to a NSThread
. Its role is to ensure that its NSThread
is busy when there is work to do and at rest when there is none.
The main thread automatically launches its run loop at the application launch.
Implementing a run loop
This pseudo code comes from Nicolas’ article. It highlights the two main actions of a run loop:
- A run loop waits for an event
- It dispatches it once received
The run loop on iOS
On iOS, there are of two types of event: timers and sources.
The sources basically correspond to events that an application can handle: a touch, a network call that ends, etc. They are provided by the system.
Rob Mayoff, on his StackOverflow post describes each of the steps performed by a run loop on iOS:
We find our initial representation of the run loop as a simple infinite loop alongisde all the different operations and checks that it performs.
Thanks to the Core Foundation
developer teams, anytime a block is executed during the cycle of a run loop, it is always called by a function with very distinctive prototypes:
It helps debugging. We know where we are in the run loop pass. To get the full detailed list, please refer to Nicolas’ article.
The run loop observers
In the Rob’s post, you may have noticed those “observers callbacks”. It is indeed possible to observe the run loop, alongside scheduling blocks, to be notified at a desired time.
The observer blocks are always performed by the debugging function:
CoreAnimation and Run Loop
What is the link between a run loop and a CATransaction
?
As stated by Rob Mayoff, CoreAnimation
is an active observer of the run loop. It waits for the kCFRunLoopBeforeWaiting
event. This latter informs CoreAnimation
of the standby of the run loop. Thus at that moment, CoreAnimation
is sure that all the modifications made during the current run loop pass have been scheduled. It can commit the implicit transaction it started, if any, when a first change was made during the loop to display the changes.
We can verify this statement by looking again at the bottom of our stacks of our breakpoints A and B of the first part of the article.
In A, a touch (a source) woke up the run loop. The touch has been dispatched and triggered the action of our button.
At that moment, the modification of the sample view height constraint forced CoreAnimation
to start an implicit transaction.
The loop continued, triggering its various observers along its way. CoreAnimation
, as a skillful observer, committed the implicit transaction it started at the action call just before the run loop finished its loop.
We can generalize this behavior by observing the symbolic C++ breakpoint:
Summary
CoreAnimation
renders our app view hierarchy at each run loop pass and not at each view hierarchy modification. To do so, it observes each modification made to each view and begins a CATransaction
if one does not already exist. As an observer of the run loop, CoreAnimation
finally commits this transaction just before the thread is put to sleep. The interest is to gather all the modifications made since the wake-up and not refreshing the screen excessively. Just before making the rendering, CoreAnimation
makes sure each view is laid out by triggering a layout phase.
The events of a UIViewController
An interest in discovering the run loop and its interaction with CoreAnimation
is to have a new point of view on the events that trigger the code of our applications. Apple has taken care to expose us a high level and accessible API: viewWillAppear
, viewDidAppear
, viewWillLayoutSubviews
etc. We can now try to give them a new meaning.
viewWillAppear
When presenting a UIViewController
, viewWillAppear
is the first method called by UIKit. According to this breakpoint, it is called in the same stack as the commit of the implicit transaction and therefore as a layout phase. The UIViewController
view is about to be sent to the render server. However, the layout has not yet taken place: the layoutSubviews
methods of our views have not yet been called.
viewWillLayoutSubviews and viewDidLayoutSubviews
viewWillLayoutSubviews
announces the start of layout of the view controller root view. It is called in the same viewWillAppear
stack but this time the commit has taken place (see #6). Successive calls to the layoutSubviews
methods of the UIViewController
views are about to start. viewDidLayoutSubviews
is called just after the execution of the layoutSubviews
of the root view of the view controller.
viewDidAppear
viewDidAppear
is called a few seconds later - the time for the presentation animation to finish. It appears in the stack of a block of code sent to the main thread. This block was probably defined using the setCompletionBlock
method at the beginning of the UIViewController
presentation, so viewDidAppear
is called once all the presentation animations are finished.
Conclusion
In this journey, we discovered the main components of the iOS rendering process: a transaction, a run loop and an observer pattern.
However, we cannot claim to have fully answered the question. We went a bit beyong the intelligible world of the Apple’s documentation but the reality escapes us. From one version of iOS to another, the elements described above may change and many of the statements are actually simple observations.
We also made a few shortcuts. If you try to put the C++ breakpoint given above, you may encounter commits made by CoreAnimation
in blocks other than those triggered by an observer. In particular, if you break too long on a breakpoint, CoreAnimation
seems to trigger a layout phase as soon as the current stack ends.
We have nevertheless managed to break through the layer, represented by UIKit, which hides the heart of an iOS application. The events exposed by UIKit allow us to always execute our code in respect of the infinite loops of the run loop that drives the screen refreshs. We highlighted the application’s life cycle and the importance of scrupulously respecting the UIKit events.