DEV Community

Cover image for Debugging Memory Leaks With Instruments in XCode
Raphael Martin
Raphael Martin

Posted on

Debugging Memory Leaks With Instruments in XCode

Introduction

Recently, we explored Memory Leaks and Retain Cycles in Swift, diving into their causes and how to prevent them. But sometimes, apps are shipped to production with some of this issues. As engineers, it's our job to find and fix these flaws. Today, we’ll learn how to identify, analyze, and fix memory leaks in existing apps using Xcode Instruments.


Using deinit

When ARC deallocate an object, the deinit lifecycle function of the object is called. If you’re unfamiliar with ARC, I recommend you reading my previous post about it here first.

If deinit is never being called, you know that the object is also never being deallocated:

class MyClass {
   init() {
      print("MyClass object was initialized")
   }

   deinit() {
      print("MyClass object was deallocated")
   }
}

var obj: MyClass? = MyClass() // Output: "MyClass object was initialized"
obj = nil                     // Output: "MyClass object was deallocated"
Enter fullscreen mode Exit fullscreen mode

In the example above, you see in Xcode's console that the deinit is being correctly called, meaning the object was successfully deallocated.

Let's see an example with a retain cycle:

class Car {
    var name: String
    var driver: Driver?

    init(name: String, driver: Driver? = nil) {
        self.name = name
        self.driver = driver
    }

    deinit {
      print("`Car` was deallocated")
    }
}


class Driver {
    var name: String
    var car: Car?

    init(name: String, car: Car? = nil) {
        self.name = name
        self.car = car
    }

    deinit {
      print("`Driver` was deallocated")
    }
}

func createCarAndDriver() {
   var car: Car? = Car(name: "Ferrari", driver: nil)
   var driver: Driver? = Driver(name: "Enzo", car: nil)

   car?.driver = driver
   driver?.car = car
}

createCarAndDriver()
Enter fullscreen mode Exit fullscreen mode

In the above's example, no messages will appear in Xcode’s console, which indicates that both objects are failing to being deallocated.

How to use it

In your current project, if the app gets laggy or crashes when navigating to a specific screen, for example, you can take a look in which objects are instantiated when presenting this view. Suspect that an object isn’t being deallocated? Add (if it hasn't yet) a deinit to the class definition, and log or breakpoint its call. This way you can find it there's really an issue with this object allocation.

Pros/Cons

This isn't the most pragmatic way of debugging memory leaks, and depends on trial and error. But, for simple cases (small projects or small code bases) it can be enough.


Use Instruments to Find Retain Cycles

While tracking deinit calls works for simpler cases, Instruments offers a more sophisticated way to identify memory leaks. It's a debugging tool with several templates to you find complex issues with you application, such as memory, network, and even specific tools for games. Let's see how to use Instruments to detect some leaks:

  1. Creating a new Project: Firstly, we're going to create a new iOS project in Xcode to be our example. Ensure that on Platform you are selecting iOS and on Application App: Creating a new iOS project on XCode
  2. Project properties: Set the project's name and company identifier. Let's call it "DebugginRetainCycles", select "SwiftUI" for interface: Setting project properties. Name, Organization and Interface Framework
  3. Adding the flawed objects: Go to the app's entrypoint file, that should be named DebuggingRetainCyclesApp.swift. Here you'll have a SwiftUI object that implements the App protocol. At the end of the file, copy and paste these Car and Driver objects from the previous example, and also the createCarAndDriver() function declaration. Adding classes with retain cycle implemented
  4. Instantiating the objects: Add an initializer to the DebuggingRetainCyclesApp struct, and inside it call the createCarAndDriver() function. That will make our app allocate both objects at its first launch, creating a retain cycle. Calling the function that instantiate the objects, creating a cycle
  5. Profile the app: In XCode select the menu Product > Profile, or just press cmd+I. Xxode will build your project and open the Instruments Template window. Pay attention that it can appear unfocused in your desktop: How to find the
  6. Selecting the template: In this new window, select the Leaks template: Leaks template in Instruments Template Selection window
  7. Record and Analyze: After selecting it, a simulator will be launched, and an Instruments window with the profiling result will appear. If Xcode already finished building the app, click on the "Record" button at the Instruments window, otherwise wait for Xcode finish the building process first, and then click in that button. This will make the simulator launch the app, and Instruments start tracking and recording the memory usage of the app. Record Memory button in Instruments window
  8. Finding Leaks Existence: Now, you just should wait for some time and Instruments will give you a result of leaks found. In our case, we added the Retain Cycle direct to the launch of the app, but in a real scenario you may need to do some interaction with your app to simulate it (as navigating to a problematic screen). After performing the actions that you suspect that is causing a leak, wait for some time and the result should appear in the Leaks row, just below the Allocations one. In our case, just launching the app and waiting for about 20 seconds did the trick: Leaks being shown in Instruments window
  9. Detect Location: Select the Leaks row, and you'll see two leaked objects: Car and Driver. You can now stop recording the memory. Selecting one of them will display the Stack Trace panel in the right side, that tells you where in the code this object is being created. In Xcode, navigate to the indicated file, investigate the cause and fix it. In our case, we're fixing it by making the Driver.car reference weak. More about that in How To Prevent Retain Cycles?. Making the  raw `car` endraw  property weak
  10. Testing the Fix: Profile the app again, by pressing cmd+I. After Xcode finishing the build process, start recording again in the Instruments window and wait for the Leaks result. You should see a successful message: Instruments showing no leaks

Repeating these same steps in a real-world project will show you a report of found leaks, with the proper Stack Trace, which can help you find the location, and consequently, the cause of the leak.


Write Test Cases

You can also use Unit Tests to validate the inexistence of retain cycles, and also guarantee that they will not be added in future, if you run tests periodically in your development proccess:

func testCarReferenceIsWeak() {  
    var person: Person? = Person(name: "Alice")  
    let car = Car(name: "Ferrari", driver: person)  
    XCTAssertEqual(car.driver, person) // Asserts that the driver is correctly set
    person = nil  
    XCTAssertNil(car.driver)           // Asserts that the object is correctly deallocated 
}
Enter fullscreen mode Exit fullscreen mode

In this test case, we validate that the Person object successfully sets a Person as driver, and also successfully deallocate it when needed, ensuring that this class will not bring a retain cycle to your app.


Conclusion

Debugging memory leaks can be challenging, but using the right tools makes the process manageable. Today, we explored how to identify leaks using deinit, Instruments, and Unit Tests, ensuring your app maintains optimal memory management.

Bookmark this guide as a reference for the future. While I hope you don’t encounter memory leaks often, these steps will guide you when you do.

Top comments (0)