Discover how to render smoother animations in your app by troubleshooting the commit phase of your render loop. Dive into the mechanics of this phase, and learn how to use Instruments to uncover the source of hitches in your app, eliminate them, and avoid them outright.
Hi. My name is Charles Circlaeys, and in this talk, we will dive into scrolling in animation hitches in the commit phase of the Render Loop.
iOS uses the Render Loop to display your views. Touch events are sent to your app, it responds by changing its views, and those views are rendered onto the display by iOS.
We will focus on finding and fixing animation hitches in the commit phase of the Render Loop.
To learn about the entire Render Loop and what is a hitch, please watch the video "Explore UI Animation Hitches and The Render Loop." We will first look into what defines a commit transaction...
use Instruments to find hitches...
and we will share our recommendations to avoid commit hitches. Let's start by defining a commit transaction. Here, we have an example of an apps view hierarchy that is currently waiting for events. After it receives a touch event, a view responds to it and processes the event by changing the background color or the frame for some of its subviews.
The system records that these subviews will require a layout or display during the next commit transaction.
During the commit transaction, the views that need a display or a layout will be updated accordingly by calling drawRect or layoutSubviews.
Let's take a look at the different phases involved during a commit transaction. There are four steps: the layout phase, the display phase, the prepare phase and, finally, the commit phase.
During the layout phase, layoutSubviews will be called for every view that requires layout. You can mark a layout needed by changing a views position, adding or removing views or explicitly call setNeedsLayout on the view.
During the display phase, drawRect will be called for every view that requires display. You can indicate that display is needed by adding views to the view hierarchy that override drawRect or by calling setNeedsDisplay explicitly. During the prepare phase, images that haven't been decoded yet will be decoded during this step. This kind of operation can take considerable time for large images.
Also, if an image is in a color format that the GPU cannot directly work with, it will be converted during this step. This will require the image to be copied instead of sending a pointer to the original, which will cost additional time and memory. To learn more about optimizing the images in your app, watch the "Image and Graphics Best Practices" video.
Finally, during the commit phase, the view hierarchy will be packaged up recursively and sent to the render server. Note that deep view hierarchies will take longer to be packaged up.
Now that we have described the details of a commit transaction, let's move to our second topic: find hitches with Instruments. In Xcode 12, we released a new Instrument template to profile hitches in your apps. This will help you to visualize and investigate the Render Loop for the hitching frames detected.
Let's look at some hitches in our example app. We will record a trace in Instruments as we scroll in our application.
So here we have a recorded trace of our scrolling performance, and we can see all the hitches detected.
Let's take a closer look at the hitch 16.
We can see on the left all the tracks corresponding to the Render Loop phases required to compose the frames.
The Hitches track shows the hitches and their durations.
The User Events track shows the user events as receipted with the hitching frame.
The Commits track shows the commit phases and the processes that committed during this phase.
Patrick will talk more about the Renders and GPU tracks in "Demystify and Eliminate Hitches in the Render Phase." The Frame Lifetimes track shows the entire duration to compose the hitching frame. The Built-in Display track shows all the frames that appeared on display along with the VSYNC events.
You can compare the frame lifetime with the beginning of the hitch duration to visualize the expected interval that the frames should have been ready for display.
This interval is called the Acceptable Latency. All time after that is the hitch duration.
Below the tracks we can see detailed metrics for the hitch when the Hitches track is selected. There are many hitches in our demo app, but we have been focusing on the hitch 16 here.
We can see the hitch duration...
the acceptable latency...
and the hitch type. Hitch type is useful to get a hint during which phase the frame was delayed and where to start investigating.
For this example, we can see that the selected hitching frame was caused by the commit and the GPU phase.
I'd like to find what code is taking too long in the commit phase, and thankfully, the animation hitches template includes Time Profiler, so we can see what code is running when this hitch is occurring. From here, I can select the interval I want to investigate and search for the process that was committing. I can select the main thread of this process...
and display its call tree.
Now we are able to analyze which calls might be expensive. We can see that this call tree is originated from a commit transaction, and it shows that we spent about ten milliseconds inside a method called updateTags in our QSTEM CollectionViewCell.
Let's look at what's in this app.
It is composed of a common CollectionView, and each cell shows a photo thumbnail using a UIImageView, some text using UILabel and some tags using a custom TagLabel view. Let's look at the implementation of our QSTEM CollectionViewCell class and, more particularly, where this method is being called. Here we see that we have a property observer on the menuItem.
This property observer will call updateTags in two scenarios: A valid menuItem was set, or it was set to nil.
In the first scenario, we parse an array of tags that we want to display. In the second scenario, we parse an empty array to remove any remaining tags. Let's take a look inside the updateTags method implementation now. As expected, we remove all the views from the view hierarchy in case of an empty array of tags. Otherwise, we create a StackView if needed.
Then we create or reuse existing TagLabels for each tag.
After that, we remove any existing unused TagLabels in case that the previous usage of this cell had more tags than the new one. Let's go back to our calling scope and let's look at potential problems now that we have a better knowledge of how things are implemented. The QSTEM CollectionViewCell overrides the prepareForReuse method that is called when a cell is dequeued for reuse. And inside this method, we set the menuItem to nil. Doing so will cause a second scenario to happen, which removes all the previous TagLabels from the cell view hierarchy without taking advantage of our reuse implementation logic.
This means that for every dequeued cell, we will remove all the previous label subviews and reinstantiate every single view needed to display all labels.
This is not optimal, and this might be causing the hitch. The solution for this is quite simple. We don't need to clear our menuItem, and we can simply remove the prepareForReuse method from the implementation. Now when we set the new cells menuItem, we can take advantage of the reusable logic and avoid expensive view hierarchy operations. If we record a new trace after our fix, we notice that we drastically improve the number of hitches detected compared to the first trace. The Time Profiler in Instruments was very helpful for finding what code was taking too long and causing a hitch. To find out more about Time Profiler, watch the "Using Time Profiler in Instruments" video.
After learning how to profile applications with Instruments, let's discuss some recommendations to avoid hitches in the commit phase.
Rule number one is to keep your views lightweight. To do so, try to take advantage as much as you can from the properties available on CALayer that are GPU accelerated and avoid CPU custom drawing. In case that it is justified, make sure to measure the performance of it. Avoid any empty implementations of drawRect as the system must do extra work, which will require additional time and memory usage during the coming transaction. Try to reuse views as much as possible to avoid expensive view hierarchy operations as adding and removing. Related to removing views, try to take advantage of the view property "hidden" if you need to stop showing a specific view during an animation. This is a lot cheaper. Rule number two is to reduce expensive and redundant layout. Try to only rely on setNeedsLayout when you need to update your layout. layoutIfNeeded will expend the current transaction lifetime and can cause hitches. Most of the time, you can wait for the next run loop to update your layout. Try to use the minimum number of constraints to avoid increasing the complexity for solving this. Finally, a view shall only invalidate itself or its children, but not its siblings or its parent view. Otherwise, the view's layout will be recursively invalidated again. I recommend you to watch the two WWDC talks mentioned below to learn more details about performance layout and image and graphics best practices.
Now that you understand the commit transaction pipeline, you can avoid expensive commits, you can use the new Animation Hitches template in Instruments to detect and investigate hitches. You've learned some strategies for preventing commit hitches such as ensure prepareForReuse does not incur additional work, keep your view hierarchy shallow and lightweight and avoid expensive and redundant layout. Also, be sure to learn about the next phase of the Render Loop in the video "Demystify and Eliminate Hitches in the Render Phase." Thank you.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.