跳转至

15 Optimizing Auto Layout Performance

App performance is critical for delivering a great end-user experience. A sluggish and unresponsive app tends to frustrate its users to the point where they delete the app. On the other hand, a fast and responsive app helps its users achieve their core tasks.

With Auto Layout, you have some handy tools available at your disposal to help fine-tune your app’s performance. In this chapter, you’ll learn about the following Auto Layout performance optimization topics:

  • Betting safe on performance with Interface Builder.
  • Factoring in the render loop.
  • Understanding constraints churning.
  • Utilizing static and dynamic constraints.
  • Batching and updating constraint changes.
  • Understanding the cost of using Auto Layout.
  • Making performance gains using best practices.

Betting safe on performance with Interface Builder

One of the benefits of using Interface Builder is that it reduces the room for errors when handling Auto Layout constraints. You show Interface Builder how you want your layout to look using constraints, and you put the rest into the hands of Apple.

In other words, Interface Builder handles the behind-the-scenes code.

However, many of today’s production apps implement Auto Layout partially or entirely using code. When you implement Auto Layout programmatically, you allow for greater customization — but you do so at the cost of widening the error margin for performance degradation. In short: The more you’re able to do, the more things can go wrong.

When using code for Auto Layout, it’s not only important to know what to do. It’s also important to know what not to do. For example, overriding and adding suboptimal code in updateConstraints() can cause disastrous performance or even a crash.

So, what’s better? It all depends. If you want Apple’s “it just works” experience with regard to optimizing Auto Layout performance, then use Interface Builder. If, however, you want finer control over how your Auto Layout is implemented, then the programmatic approach has more merit.

Factoring in the render loop

The render loop is not often talked about, yet it’s the backbone for laying out your user interface at every frame. Understanding how the render loop works will help you see the larger picture of how Auto Layout works with other system components. At the core, the render loop’s purpose is to ensure all of your views are presented as intended for every frame.

The render loop can be utilized to mitigate extraneous layout work. However, if it’s used incorrectly, the render loop can cause your app to take a performance hit. After all, the render loop has the potential to run 60 or even 120 times per second, depending on the device’s refresh rate (e.g., iPhone 11 Pro and iPad Pro 12.9” respectively).

Apple exposes the render loop API methods for developers to optimize layout performance. The goal is to let the system do the minimum amount of work to produce the desired user interface for every frame. The render loop’s methods can be segregated into three phases: update constraints, layout and display.

In order, each phase does the followings:

  1. Update constraints: Layout calculation.
  2. Layout: Input layout values from the previous step into the view.
  3. Display: Render the user interface of the view within the layout bounds.

Each render loop phase has its own set of methods:

  1. Update constraints: updateConstraints(), setNeedsUpdateConstraints(), updateConstraintsIfNeeded()
  2. Layout: layoutSubviews(), setNeedsLayout(), layoutIfNeeded()
  3. Display: draw(_:), setNeedsDisplay()

In the update constraints phase, its operations start from the leaf views and travel up the view hierarchy. You can use this to think about the order in which constraints are calculated. For the layout and display phases, the operations start at the root of the view hierarchy and travel down to the child views. The latter phases consist of transferring layout information from the layout engine and visualizing a view from the given rectangle.

You may have noticed that in each render loop phase, there are two or more operations. For the most part, you don’t need to and shouldn’t touch them. However, you may be able to find room to reduce repetitive layout work and fine-tune performance to give your layout speed an edge.

More important than improving the layout speed, however, is to avoid the pitfalls, which will do you a lot more harm than good. By understanding the components of the render loop, you’ll be less susceptible to fall into the possible pitfalls, such as layout churning. Layout churning is when the layout engine repeatedly and unnecessarily computes layout variables. Here’s a breakdown of the functions within the render loop:

  • updateConstraints(): Make constraints changes.
  • setNeedsUpdateConstraints(): Invoke constraint changes in the next render loop.
  • updateConstraintsIfNeeded(): Ensures the latest constraint changes are applied to the view and its subviews.
  • layoutSubviews(): Set the size and position of the view using constraints information.
  • setNeedsLayout(): Invoke layout changes in the next render loop.
  • layoutIfNeeded(): Ensures the latest layout updates are applied to the view and its subviews.
  • draw(_:): Draw view’s content within the view’s bounds.
  • setNeedsDisplay: Invoke view redrawing in the next render loop.

Although these methods are accessible to developers, take great care when using them because they are sensitive code. You should minimize your interaction with them, and only touch them when you’re able to reduce the overall layout work.

For example, say you have a UITextView: The intrinsic size of the text view depends upon the font size, font style, text, padding and so on. One thing you can do is to re-compute the text view’s size each time there’s an update to one of the properties. This is inefficient since these updates operate consecutively. In other words, everything that’s computed before the final update is extraneous work. Therefore, instead of calling a method like updateConstraints(), you can call setNeedsUpdateConstraints() after updating a property. This ensures that you only call updateConstraints() at the end of the render loop before the frame gets sent onto the screen.

Why update constraints

As introduced earlier, making use of updateConstraints() can be a recipe for disaster. So, why use it? The title of this chapter may give it away: It’s for performance. There are two ways to make constraint changes when using code: in place or in batches by using updateConstraints().

In place constraint changes are found in places such as viewDidLoad(), view initializers or user interaction. These constraint changes have the layout engine activate and deactivate constraints individually.

On the other hand, updateConstraints() gets the layout engine to batch constraint changes. Imagine activating and deactivating all of your constraints at once. To schedule a change in constraints, call setNeedsUpdateConstraints().

You use updateConstraints() when making constraint changes in place is slow, or you can mitigate redundant work for the layout engine. However, because this is sensitive code, it has the possibility of making your app susceptible to constraints churn.

Constraints churn

Constraints churn happens when constraints are repeatedly added and removed. Although this may not happen frequently, it’s essential to know what it looks like to avoid any misstep.

To avoid constraints churn, ensure the following:

  • Static constraints are set once.
  • Dynamic constraints only activate and deactivate when necessary.

Static constraints are constraints that never change throughout the lifetime of a view. Dynamic constraints are constraints that can change. They may be used in one scenario and discarded in another. You’ll see examples of this as you work on the sample project.

Before running the sample project, it’s important to note that the simulator uses your Mac’s hardware, which can be magnitudes faster than your iOS device. By using an iOS device, you’ll be better able to see performance dents if there are any. You might even consider using an older iOS device to benchmark performance.

Open AutoLayoutPerformance.xcodeproj in the starter folder. Build and run.

img

img

Go ahead and scroll up and down on your device. You’ll see the console log prints Update Constraints every time a cell is dequeued from the table view. You may notice that the scroll frame rate is a bit choppy; yet, there are only eight views per cell. Just imagine the performance dent for views with more subviews. It’s time to look inside to see what’s wrong and what you can do to smooth out the frames of your app.

Open TableViewCell.swift from the Views group. Look inside updateConstraints(), and you’ll see the following:

// 1
contentView.subviews.forEach { $0.removeFromSuperview() }
// 2
contentView.addSubview(usernameLabel)
contentView.addSubview(titleLabel)
contentView.addSubview(profileImageView)
contentView.addSubview(badgeImageView)
contentView.addSubview(descriptionLabel)
contentView.addSubview(postImageView)
contentView.addSubview(cakeImageView)
contentView.addSubview(isHighCalorieLabel)
// 3
NSLayoutConstraint.deactivate(layoutConstraints)
// 4
layoutConstraints =
  usernameLabelConstraints
  + titleLabelConstraints
  + profileConstraints
  + descriptionLabelConstraints
  + postImageViewConstraints
// 5
if beverage?.isHighCalorie ?? false {
  layoutConstraints += highCalorieConstraints
} else {
  layoutConstraints += lowCalorieConstraints
}
// 6
if beverage?.isFrequentUser ?? false {
  layoutConstraints += badgeImageViewConstraints
}
// 7
NSLayoutConstraint.activate(layoutConstraints)

Here’s the code breakdown:

  1. Content view will remove its subviews by calling removeFromSuperview() on each subview.
  2. Content view will add the re-add the subviews.
  3. The layout engine will deactivate all constraints inside of layoutConstraints.
  4. layoutConstraints is set with title label, profile, description label and page image view constraints.
  5. Then, depending on if the beverage is high-calorie, add a constraint to layoutConstraints.
  6. Then, depending on if the beverage belongs to a frequent user, add a constraint to layoutConstraints.
  7. Activate the constraints.

At first, the code may not seem problematic — but it is. Although you may not be able to call this method 60 times per second due to the size of the table view cell and how fast you can scroll, the layout engine does extraneous work that can be mitigated, which you’ll fix next.

Adding and removing subviews

Notably, there’s no need to add and remove subviews constantly. Instead, you only need to do this once when the view initializes.

First, remove the following code from updateConstraints():

contentView.subviews.forEach { $0.removeFromSuperview() }

Second, move the following code from updateConstraints() to commonInit():

contentView.addSubview(usernameLabel)
contentView.addSubview(titleLabel)
contentView.addSubview(profileImageView)
contentView.addSubview(badgeImageView)
contentView.addSubview(descriptionLabel)
contentView.addSubview(postImageView)
contentView.addSubview(cakeImageView)
contentView.addSubview(isHighCalorieLabel)

You’ve removed the extraneous workload for adding and removing subviews inside updateConstraints(). Any time TableViewCell calls updateConstraints(), it no longer unnecessarily rips off all its subviews and adds back the same subviews.

Activating and deactivating static constraints

Most of the constraints are static constraints, which don’t change throughout the lifetime of the view. They are there for good as long as the view doesn’t get deallocated. For static constraints, you need to ensure that they’re only going to activate once.

First, add the following property to TableViewCell:

private var staticConstraints: [NSLayoutConstraint] = []

To mitigate the extraneous workload for the layout engine, you’ll use this property to reference and keep track of activated static constraints.

Second, replace the following code in updateConstraints():

NSLayoutConstraint.deactivate(layoutConstraints)
layoutConstraints =
  usernameLabelConstraints
  + titleLabelConstraints
  + profileConstraints
  + descriptionLabelConstraints
  + postImageViewConstraints

With the following:

if staticConstraints.isEmpty {
  staticConstraints =
    usernameLabelConstraints
    + titleLabelConstraints
    + profileConstraints
    + descriptionLabelConstraints
    + postImageViewConstraints
  NSLayoutConstraint.activate(staticConstraints)
}

TableViewCell no longer deactivates and activates the same constraints repeatedly. The app checks if there are static constraints. If there are none, the app sets the static constraints and activates them. For subsequent render loops, static constraint changes are out of the equation.

Activating and deactivating dynamic constraints

Dynamic constraints are constraints that may change throughout the lifetime of a view. Part of the table view cell’s layout is dependent on the beverage properties.

A high-calorie beverage lays out an additional image and label:

img

A beverage post from a frequent user lays out an orange badge:

img

These constraints are dynamically activated and deactivated based on the beverage properties. Once again, you should aim to mitigate any unnecessary work for the layout engine.

First, add the following property to TableViewCell:

private var dynamicConstraints: [NSLayoutConstraint] = []

Here, similar to staticConstraints, you create an empty array of NSLayoutConstraint. With this collection, you’ll use it to reference activated dynamic constraints.

Then, add the following helper method to TableViewCell:

private func updateDynamicConstraints(isHighCalorie: Bool) {
  NSLayoutConstraint.deactivate(dynamicConstraints)
  dynamicConstraints = isHighCalorie
    ? highCalorieConstraints : lowCalorieConstraints
  NSLayoutConstraint.activate(dynamicConstraints)
}

You’ll use this method to deactivate old dynamic constraints and set the new dynamic constraints to either the high-calorie or low-calorie constraints. Afterward, you’ll activate the new dynamic constraints.

Finally, replace the following code in TableViewCell:

if beverage?.isHighCalorie ?? false {
  layoutConstraints += highCalorieConstraints
} else {
  layoutConstraints += lowCalorieConstraints
}

With the following:

// 1
if beverage?.isHighCalorie ?? false
  && dynamicConstraints != highCalorieConstraints {
  updateDynamicConstraints(isHighCalorie: true)
// 2
} else if dynamicConstraints != lowCalorieConstraints {
  updateDynamicConstraints(isHighCalorie: false)
}

Here’s how it works:

  1. The decision to compute the high-calorie constraints is predicated on two conditions: First, the beverage is high-calorie. Second, the current dynamic constraints referenced within TableViewCell differ from the high-calorie constraints. Only when these two conditions are true do you pull the Auto Layout engine out of bed.
  2. Similarly, the decision to update dynamic constraints is predicated on the beverage’s high-calorie status and if the previous dynamic constraints differ from the low-calorie constraints. Otherwise, you let the Auto Layout engine rest.

Avoiding unnecessary constraints activation and deactivation

Similar to the beverage’s high-calorie status, whether the beverage post belongs to a frequent user also has a role in TableViewCell user interface. You can activate and deactivate the badge image view’s constraints depending on isFrequentUser. However, this is extraneous work on the layout engine. Whenever possible, choose to activate the constraints once, and you can use isHidden to show or hide a view, which is less taxing on the system.

Remove the following code from updateConstraints():

if beverage?.isFrequentUser ?? false {
  layoutConstraints += badgeImageViewConstraints
}

Replace profileConstraints in TableViewCell with:

private var profileConstraints: [NSLayoutConstraint] {
  profileImageViewConstraints + badgeImageViewConstraints
}

Now, TableViewCell no longer unnecessarily activates and deactivates badgeImageViewConstraints. Instead, the operation only runs inside of updateConstraints() when staticConstraints is empty inside a TableViewCell.

Lastly, remove the following code from updateConstraints():

NSLayoutConstraint.activate(layoutConstraints)

Also, remove layoutConstraints from TableViewCell. The code above and the associated property are no longer needed anywhere in the view.

Build and run. You’ll now have a much smoother scrolling experience. Cheers!

Each constraint computation isn’t computationally significant in any way. However, when these computations repeatedly compound beside other operations within your app, the overall computation is a significant drain on your device — possibly translating to a rather poor user experience.

Unsatisfiable constraints

It’s good to know that unsatisfiable constraints can cause performance issues. Not only that, other problems may stem from unsatisfiable constraints. When there are unsatisfiable constraints, the layout engine will need to go through the process of figuring out which constraints to break in the hopes of giving you the desired layout.

This is unnecessary work for the layout engine, and you risk getting a layout that isn’t what you want. As a result, other parts of your app that interconnect with the constraints can have problems now too, which can be difficult to debug since unsatisfiable constraints mask the root of the problem. As a rule of thumb, ensure that there are no unsatisfiable constraints for optimal app performance with Auto Layout.

Constraints dependencies

Now, you’ll look at how layout dependencies affect Auto Layout’s performance. Look at the following diagram:

img

The top two views are independent of each other. They are constrained to the root view. As a result, you’d have linear time complexity for both layout operations.

The bottom two views have a dependency. Both views are constrained to be a specific distance apart. When there’s a dependency, the layout engine will do additional substitution work.

Perhaps, the first thing that comes to a lot of people’s minds is to make the views independent of each other as much as possible. However, the layout engine caches layout and tracks dependencies. As long as the dependencies are correlated to the layout you want, it’s highly unlikely that avoiding constraints and using other methods such as manual calculation is worthwhile. The key takeaway here is not to be afraid to add constraints and avoid doing a bunch of manual calculations for Auto Layout.

However, there’s a fine line between having the constraints to display a particular layout versus having too many constraints or constraints settings to accommodate for additional layouts. In the latter case, there will be false dependencies where constraints don’t need to be dependent on each other. In addition to higher computation power requirements, debugging becomes difficult. The solution is to put two different layouts in two different views and don’t use constraints to make it work in a single view for performance and debugging sanity.

Advanced Auto Layout features and cost

You may wonder how expensive it is to set layout constraint inequalities, constants and priorities. When given an inequality relation, the layout engine sees it as a single additional variable to solve. Inequalities are lightweight for the layout engine.

Setting a layout constraint constant makes use of the layout engine’s dependency tracker. Because the layout engine tracks dependencies, even having a constraint react directly to the user’s swipe gesture is performant. Apple uses the dependency tracker to optimize Auto Layout’s set of constraint operation performance.

Unlike constraint inequalities and constants, constraint priorities take additional work to run the simplex algorithm for error minimization. This is something to be mindful of and not be afraid of using it. Instead, you should use it only when you need to.

In a view that tries to accommodate for layouts that should be separated into multiple views, you may find many constraints and constraint priorities settings all over the place. When this is the case, it’s time to separate the layouts into their compartmental views.

You may have seen this in some legacy apps. When this nightmare shows itself, it’s an excellent idea to use performance and debugging clarity as talking points for a view refactor.

Apple optimizing UIKit

Apple is continuously optimizing UIKit ’s performance. Every year at WWWDC, Apple introduces a more performant way to integrate your data, behind the scene optimizations, new frameworks to make building dynamic and responsive layouts easy and so much more.

To take advantage of the latest Apple technologies, upgrading to the project’s latest deployment target can sometimes do the trick. Other times, you’d need to do some more manual work. For example, in WWDC18, Apple introduced UIKit layout improvements for iOS 12. The improvements shown are no laughable matter. They included more performant ways to help developers achieve the layout they want, which includes improvements to the OS system, core of UIKit, client code and more. Plus, for devices running on iOS 12 and above, these improvements are free.

Although you can do more with Auto Layout in code, it’s easier to get Auto Layout right with Interface Builder. You can optimize Auto Layout by helping the layout engine do the least amount of work for your desired layout. As long as that is your north star, you have the correct mental model and are on the right path.

Key points

  • Auto Layout done in the Interface Builder gives you automatic layout performance optimization. It’s a great place to start and stick to it for performance if you can.
  • When creating Auto Layout constraints in code, you can do it in place or in batches.
  • When you find a need to optimize Auto Layout performance, update the constraints inside of updateConstraints().
  • Beware when working with sensitive code like updateConstraints(). You use it to make layout changes and defer extraneous work.
  • Get rid of any unsatisfiable constraints to mitigate the risks of performance dent and problems masked from it.
  • Additional view dependency means an additional substitution for the layout engine when solving constraints.
  • Typically, letting the layout engine compute view frames is faster than the computation you can manually do in the client code for your view(s).
  • Inequalities aren’t expensive. They’re an additional variable for the layout engine to solve.
  • Constraint priorities utilizes the simplex algorithm for error minimization. This takes more work than inequalities. It’s good to know that it does take more work, but you should use it when you need to.
  • When Apple optimizes UIKit, this can give your app a performance boost. Devices running on the latest iOS version should deliver the fastest Auto Layout performances.