Ever wondered how exactly the system renders your app's UI? I don't care how you answered that. Keep reading.
The Main Run Loop
The main run loop is what is responsible for making a lot of things happen at each frame of your app’s existence. Since this is an environment where the user can interact with the system, there has to be a mechanism to collect, and then react to, events. The run loop’s goal is to do the app work, pass a CATransaction to the backend, and move to the “waiting” state as soon as possible.
If the RunLoop finishes its work before it's time to render the next frame, it will rest until more work comes in, but it will wake up and continue working if something comes in before the bottom of the current run loop cycle.
Running at 60 FPS
In the time each frame gets (~16 ms) there is App work and Render Server work to be done. When it’s all said and done, your app can really only use 5~10 ms of that time. If it turns out that the work you've scheduled can't be completed in that amount of time, your app will miss the next display refresh which means that that frame has been dropped.
The Work to be Done
During each frame, the run loop will perform any blocks that were dispatched to the main queue and any touches that occurred will be processed. Then, at the bottom of the run loop, if necessary, a CATransaction will be created and sent to the Render Server via IPC.
A transaction is created any time any change should happen on-screen. Say you set the bounds of a view; at the bottom of this iteration of the run loop, a transaction will be created and punted off to the render server for processing.
When a transaction is sent to the backend, the entire layer tree is analyzed and re-rendered. This is why subtree rasterization is so useful since you’re rendering one layer instead of however many sublayers you would have had.
Finally, if necessary, the GPU work is done. This involves things such as cornerRadius, borderWidth, shadowPath, maskLayer, all of which flood the GPU with work. Layer blending is also GPU bound and matters a lot on older devices. This is all known as “offscreen rendering” and also affects performance.
With all this in mind, it's interesting to know that animations use largely the same mechanism to get things moving.
The Way Client Side Animations Work
When an animation is created in Pop, UIKit Dynamics, or most commonly a UIScrollView, a CADisplayLink is set up so that the layer can be changed at a constant interval between the beginning state and ending state. The new bounds are calculated using whatever physics equation is being used and a CATransaction will be created and sent to the render server as if you had set the bounds manually.
The Way “Normal” Animations Work (Back-End Animations)
When a UIKit animation is created, it is itself, a CATransaction that is sent to the render server. Then all subsequent steps in the animation, as well as the physics calculations occur on the backend regardless of what’s going on in your app’s threads. This is why it’s hard to react to user input and change the course of one of these animations. There is IPC involved so naturally it can’t exactly be instant.
On the Render Server, a CADisplayLink is maintained so that it can keep track of your app’s refresh rate itself and calculate the new bounds at each refresh from there.
Fun Fact: At the point the main run loop goes to the “before waiting” state, if you’ve called -setneedsLayout, it will call -layoutSubviews for you. In the context of a collection or table view, -layoutSubviews is then what calls -cellForRowAtIndexPath: as new cells should be coming on screen.
You can't tell me that wasn't worth it!