NEW! Check out my latest book, Testing Swift! >>

How to find and fix memory leaks using Instruments

Paul Hudson       @twostraws

Part 2 in a series of tutorials on Instruments:

  1. How to find and fix slow drawing using Instruments
  2. How to find and fix memory leaks using Instruments
  3. How to find and fix slow code using Instruments

Yesterday I looked at how to find and fix slow drawing using Instruments using a project specifically designed to highlight issues.

Note: you don’t need to have read yesterday’s article to continue here, but I do want to repeat the warning that this project is is specifically written to be bad and should not be used as a learning exercise beyond just using Instruments to identify problems.

Although yesterday we did some work improve rendering performance, our app still isn’t smooth – at least not on my 2nd-gen iPad Pro. To fix this, and also identify another issue, I want to introduce you to the allocations instrument, which is designed to identify where and when memory is allocated.

If you don’t already have the example project to hand, please download it and open it in Xcode. Now press Cmd+I to build and run the project for Instruments, select the allocations instrument, then press Choose.

Just like with the Core Animation instrument, you need to press the record button in the top-left of the window in order to have instruments begin capturing. So, press that now, and exercise the app – that means scroll around a lot, read some stories, and so on.

When you’ve touched all parts of the (admittedly small) app, press Stop. Let’s take a look at what Instruments is telling us…

Problem 1: Collection view cell re-use

iOS has been reusing table view cells since before it was even called iOS, so it’s no surprise that collection views use the same technology. The concept is simple: as our collection view scrolls, we can automatically re-use collection view cells rather than creating them from scratch.

Instruments can show us this process in action. When you ran the allocations instrument you should have seen lots of data fill your Instruments window - it was telling you every time some object was created on the memory heap. That means all the labels, all the images, all the views, and more, all shown at the same time.

Rather than try to look at everything at the same time, it’s better to filter Instruments to show something specific. In this case, look for the Instrument Detail text field at the bottom of Instruments and type “collectionviewcell” in there to show only classes matching that name.

What you should see is one row containing UICollectionViewCell. To the right of that you’ll see two important headers amongst others: Persistent and Transient. The first tells you how many collection were cells were created and stayed around for the length of the program, and the second reports how many were created and destroyed while Instruments was recording.

If everything has gone to plan, you should several persistent instances of UICollectionViewCell after filtering, along with 0 transient instances. This is more or less what we would expect: even if you scroll up and down wildly for a while, the system should never need to create more than a certain number of collection view cells.

What that number is depends on your device – on my 12.9-inch iPad Pro it was 24 because the system can display 5 rows of 4 collection cells at a time, with another row ready to slide in as scrolling happens. However, if you’re using a smaller iPad your persistent number will be lower.

Regardless of what that number is, it isn’t enough: I’ve made this app slow enough that scrolling struggles even with cell re-use happening. iOS is re-using cells automatically, which is awesome, but it’s having to do that on the fly as users are scrolling. This is hard, and will cause our app to drop frames.

Fortunately, UICollectionView is capable of prefetching cells for us so that scrolling happens more smoothly. It does this intelligently: if the user is scrolling down then cells below their position will be prefetched. If they then change their mind and start scrolling upwards, the previous cache will be cleared and new cells above will be loaded from above their position.

Even better, prefetching is trivial to enable. In code you need to add this inside the viewDidLoad() method of ViewController.swift:

collectionView?.isPrefetchingEnabled = true

If you prefer using Interface Builder, select the collection view controller in Main.storyboard and check Prefetching Enabled.

Prefetching is actually enabled by default when you create a new collection view, but many developers turn it off because it can cause problems. In particular, the hugely popular IGListKit says this:

In iOS 10, a new cell prefetching API was introduced. At Instagram, enabling this feature substantially degraded scrolling performance. We recommend setting isPrefetchingEnabled to false in Swift.

So: use prefetching if you can, but be prepared that it might have been disabled for you by a library, or it might actually cause performance problems depending on your usage.

Try running the allocations instrument again to get a clean recording session – all being well you should now see more persistent collection view cells, showing that UIKit is keeping more in RAM. Make sure you exercise the app fully: scroll around a lot, open and close various stories, and so on.

Problem 2: Watching for leaks

Using the allocations instrument is great for examining caches, but it’s also useful for finding leaks. In this instance, our code has a leak, and it’s one that Instruments can actually identify for us.

Enter “detail” into the Instrument Detail text field, and you should see several persistent instances of DetailViewController depending on how many times you tried reading a story. This is a leak: when we dismiss the story view controller we expect its memory to be released, but Instruments is telling us those view controllers are persistent.

If you look inside DetailViewController.swift, you’ll find the following property:

var delegate: ViewController?

This view controller has a strong reference to the delegate – could that be the problem? To find out, change the property declaration to this:

weak var delegate: ViewController?

We’ve made a change, so go ahead and run the allocations instrument again to see if it worked. Go ahead and use the app thoroughly once again – scroll around a lot, open and close various stories, and so on.

What you’ll find is that the detail view controllers still leak – using a weak delegate didn’t fix it. This makes sense if you think about it: there’s no retain cycle because the main view controller doesn’t own the detail view controller. Sure, it presents the detail view controller, but that doesn’t mean it owns it.

Instead, the problem occurs here:

self.timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { timer in
    self.webView.scrollView.contentOffset.y += 0.3
}

That creates a timer to make the web view animate downwards, creating the automatic scrolling effect. This is where the retain cycle occurs: the view controller owns the timer, and the timer directly references the view controller – so it owns the view controller.

The solution here is to make the time hold self weakly, breaking the retain cycle, like this:

self.timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] timer in
    self?.webView.scrollView.contentOffset.y += 0.3
}

Now that we’ve made another change, make sure you run Instruments again so that you can check that it’s worked. All being well, you should find the detail view controllers being destroyed when they are dismissed – a big improvement!

Now what?

Even after two sets of instrumenting, this app still has problems so we’re going to come back to it one last time in part three: how to find and fix slow code using Instruments. However, at this point we’ve used Instruments to find several display performance issues and monitor memory allocations to detect leaks, which is definitely an improvement.

Once again, I hope you’re getting into the flow of re-testing your changes with Instruments regularly. Each time we made a change, we ran it back through Instruments to see whether the results matched what we expected – this is smart practice, and worth sticking to as you continue to work!

 

MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Was this page useful? Let me know!

Click here to visit the Hacking with Swift store >>