Using Xcode's visual debugger and Instruments' modules to prevent memory overuse Aug 27 2019

An essential step before you deliver your application to your users is to make sure that your app is not overusing your user’s resources. In this post, I’ll show how to use Xcode’s visual debugger and the command-line counterparts to check for common memory problems and also how to use Instrument’s to debug memory leaks.

First, let’s see which are the most common memory-related problems.

Memory issues

There are two common types of memory problems.

If your app starts using too much memory, it can be shut down by the OS. The app will trigger applicationDidReceiveMemoryWarning(_:) and you can handle low memory scenarios by freeing some of your caches before it gets terminated. Freeing memory in applicationDidReceiveMemoryWarning works if you know that your app will use a lot of memory. If you have a memory leak or a retention cycle, it needs to be fixed.

Let’s have a look at how to detect if you have a memory problem.

How to detect memory issues?

It is important to verify if our app is memory efficient. If our app has memory problems, it can be shut down by the OS, or it can generating weird bugs. At the very least, memory problems will slow down your app and make it feel sluggish. That is why it is always a good idea to verify if your app is behaving correctly.

Luckily Xcode provides an interface to some very helpful tools for debugging memory issues.

First, I would get the memory leaks out of the way. Because I’m using Swift, this shouldn’t be a problem but don’t assume, always test.

How to do this?

Using leaks

There are a few handy command-line tools at your disposal for debugging macOS and iOS apps, in no particular order:

To detect leaks on your app, run your app in the simulator, get the PID and run leaks for that PID.

Let’s say I have an app called Testing, this would be the workflow:

  1. Run my app on the simulator

  2. Get the process id (PID), you can use Activity Monitor, I’ll use the command-line.

1
2
3
$ ps aux | grep Testing.app
# derik 3281   0.0  0.5  5320400  77792   ??  SXs   7:11PM   0:01.04 /Users/derik/Library/Developer/CoreSimulator/Devices/.../Testing.app/Testing
# derik 4245   0.0  0.0  4293640    848 s002  S+    4:40AM   0:00.01 grep Testing.app
  1. Run leaks for that PID
1
$ leaks 3281
  1. Check the exit code of the command.
1
2
3
 0   -   No leaks found
 1   -   One or more leaks detected
>1   -   An error occurred 

to check for the exit code of the previous command check the $? variable after running the command:

1
$ echo $?

If you get a 0, that’s great, you can move forward. If you get 1, a memory leak was detected.

Let’s imagine you got a leak detected, how can we check what is going on?

Checking the memgraph

As you can see we can use leaks on any running process, we only need the PID. Not only our iOS apps running on the simulator but any app. If your computer is feeling sluggish, maybe there’s an app with leaks. You can try and run the leaks command on the app’s PID. If you find any leaks send some feedback to the developer :).

Ok, back to our problem we found a memory leak. You probably got a lot of information on your screen about the leak. Option one is read through that wall of text and find what the problem might be, or you could generate a memgraph and open it in Xcode’s visual debugger. Let’s do that (we’ll assume the APP running in the simulator has the PID 3281):

1
2
3
$ leaks 3281 --outputGraph=testingApp
# This generates testingApp.memgraph
$ open testingApp.memgraph

This will open up Xcode and show you the visual debugger for the memgraph file. You should see an interface like the one in the following image.

Xcode memgraph visual debugger

Now we can start debugging.

Visual debugger

In Xcode, you’ll see that there are some objects with a purple icon containing an exclamation mark next to them. Those are the objects detected as having memory issues. If you click on any of the objects, you’ll see its dependency graph (Who is referencing them). You’ll notice that there are a lot of objects that are part of Apple’s frameworks. We can filter them out because they are probably just a side effect. So let’s remove any objects directly related to the current workspace. We do this by clicking on the icon that looks like a dog-eared page:

Show only content from workspace

That should clear up the interface. You can also only show leaked objects by clicking on the icon next to it, the one that looks like a square with an exclamation mark inside.

Show only leaked blocks

Ok, now you should play with all the options on the interface to hide and show information that you think might give you a clue to what caused the memory leak. An essential piece of information is the object’s backtrace. The backtrace shows the list of function calls that brought that object into existence.

It would look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
malloc_history Report Version:  2.0
ALLOC 0x7f9372ea1580-0x7f9372ea18ff [size=896]:
0   libsystem_malloc.dylib                0x10f28a2ba stack_logging_lite_calloc + 107
1   libsystem_malloc.dylib                0x10f28ec87 malloc_zone_calloc + 99
2   libsystem_malloc.dylib                0x10f28f317 calloc + 30
3   libobjc.A.dylib                       0x10ba5b350 class_createInstance + 73
4   libobjc.A.dylib                       0x10ba6395f _objc_rootAlloc + 42
5   com.apple.UIKitCore                   0x11153d81b -[UIClassSwapper initWithCoder:] + 176
6   com.apple.UIFoundation                0x1158da852 UINibDecoderDecodeObjectForValue + 753
7   com.apple.UIFoundation                0x1158da554 -[UINibDecoder decodeObjectForKey:] + 251
8   com.apple.UIKitCore                   0x111541f1d -[UIRuntimeConnection initWithCoder:] + 178
9   com.apple.UIFoundation                0x1158da852 UINibDecoderDecodeObjectForValue + 753
10  com.apple.UIFoundation                0x1158daaf9 UINibDecoderDecodeObjectForValue + 1432
11  com.apple.UIFoundation                0x1158da554 -[UINibDecoder decodeObjectForKey:] + 251
12  com.apple.UIKitCore                   0x11153f7cd -[UINib instantiateWithOwner:options:] + 1216
13  com.apple.UIKitCore                   0x111a5b594 -[UIStoryboard instantiateViewControllerWithIdentifier:] + 181
14  com.apple.UIKitCore                   0x1118bf809 -[UIApplication _loadMainStoryboardFileNamed:bundle:] + 111
15  com.apple.UIKitCore                   0x1118bfcb1 -[UIApplication _loadMainInterfaceFile] + 274
16  com.apple.UIKitCore                   0x1118be3e5 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1360
17  com.apple.UIKitCore                   0x111102a4e __111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 904
18  com.apple.UIKitCore                   0x11110b346 +[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 153
19  com.apple.UIKitCore                   0x111102664 -[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 236
20  com.apple.UIKitCore                   0x111102fc0 -[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 1091
21  com.apple.UIKitCore                   0x111101332 __82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 782
22  com.apple.UIKitCore                   0x111100fe9 -[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 433
23  com.apple.UIKitCore                   0x111105d2e __125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 576
24  com.apple.UIKitCore                   0x111106988 _performActionsWithDelayForTransitionContext + 100
25  com.apple.UIKitCore                   0x111105a95 -[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 223
26  com.apple.UIKitCore                   0x11110aa48 -[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 392
27  com.apple.UIKitCore                   0x1118bcdc8 -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 514
28  com.apple.UIKitCore                   0x11147402f -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 361
29  com.apple.FrontBoardServices          0x118608d25 -[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 448
30  com.apple.FrontBoardServices          0x118612ad6 __56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 283
31  com.apple.FrontBoardServices          0x118612300 __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke + 53
32  libdispatch.dylib                     0x10efeedb5 _dispatch_client_callout + 8
33  libdispatch.dylib                     0x10eff22ba _dispatch_block_invoke_direct + 300
34  com.apple.FrontBoardServices          0x1186440da __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 30
35  com.apple.FrontBoardServices          0x118643d92 -[FBSSerialQueue _performNext] + 451
36  com.apple.FrontBoardServices          0x118644327 -[FBSSerialQueue _performNextFromRunLoopSource] + 42
37  com.apple.CoreFoundation              0x10dbbddb1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
38  com.apple.CoreFoundation              0x10dbbd633 __CFRunLoopDoSources0 + 243
39  com.apple.CoreFoundation              0x10dbb7cef __CFRunLoopRun + 1231
40  com.apple.CoreFoundation              0x10dbb74d2 CFRunLoopRunSpecific + 626
41  com.apple.GraphicsServices            0x11557e2fe GSEventRunModal + 65
42  com.apple.UIKitCore                   0x1118bffc2 UIApplicationMain + 140
43  com.banobal.Testing                   0x10b15e83b main + 75  AppDelegate.swift:12
44  libdyld.dylib                         0x10f063541 start + 1

As you can see it is inverted, at the bottom you see the start, then the main App with its AppDelegate and it goes from there.

but how do we obtain the stack backtrace?

To obtain the backtrace you need to run the application we are inspecting with the environment variable MallocStackLogging enabled. For example, if we are going to analyze the Terminal.app we would run it in one shell:

1
$ env MallocStackLogging=1 /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal 

And in another shell, we can call leaks to generate the memory graph file.

1
2
3
4
5
6
7
$ ps aux | grep Terminal.app
ps aux | grep Terminal.app
derik 6326   0.0  0.0  4287496    864 s023  S+    5:55AM   0:00.00 grep Terminal.app
derik 5356   0.0  0.6  4500860  93104 s002  S+    5:54AM   0:03.44 /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
# now we run leaks and generate our memgraph file
$leaks 5356 --outputGraph=terminal
Output graph successfully written to 'terminal.memgraph' 

Now when we open the terminal.memgraph and navigate to any object, we can see its backtrace on the Memory Inspector Panel.

Xcode memory inspector pane

That is great if we run the command from a shell, but how do we do it from Xcode?

We can do it on our Schema Editor. Just open the schema editor, select Run then Logging check Malloc stack and Live allocations Only (if you want to keep the log smaller).

Set malloc stack on Xcode schema

Now when you generate the memgraph, you will be able to capture the stack trace.

You could also use the command-line tools to view the backtrace of an object, that is how I obtained the previous backtrace I posted for my ViewController.

You can see the memory address on the memgraph in Xcode, or you can obtain it by using leaks again. let’s do a quick demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ leaks testingApp.memgraph | grep "ViewController"
# 35 (6.30K) __strong _rootViewController --> <ViewController 0x7f9372ea1580> [896]
# With the memory address now I can use `malloc_history`
$ malloc_history testingApp.memgraph --fullStacks 0x7f9372ea1580
#malloc_history testingApp.memgraph --fullStacks 0x7f9372ea1580
#malloc_history Report Version:  2.0
#ALLOC 0x7f9372ea1580-0x7f9372ea18ff [size=896]:
#0   libsystem_malloc.dylib                0x10f28a2ba stack_logging_lite_calloc + 107
#1   libsystem_malloc.dylib                0x10f28ec87 malloc_zone_calloc + 99
#2   libsystem_malloc.dylib                0x10f28f317 calloc + 30
#3   libobjc.A.dylib                       0x10ba5b350 class_createInstance + 73
#4   libobjc.A.dylib                       0x10ba6395f _objc_rootAlloc + 42
#5   com.apple.UIKitCore                   0x11153d81b -[UIClassSwapper initWithCoder:] + 176
#6   com.apple.UIFoundation                0x1158da852 UINibDecoderDecodeObjectForValue + 753
#7   com.apple.UIFoundation                0x1158da554 -[UINibDecoder decodeObjectForKey:] + 251
#8   com.apple.UIKitCore                   0x111541f1d -[UIRuntimeConnection initWithCoder:] + 178
#9   com.apple.UIFoundation                0x1158da852 UINibDecoderDecodeObjectForValue + 753
#10  com.apple.UIFoundation                0x1158daaf9 UINibDecoderDecodeObjectForValue + 1432
#11  com.apple.UIFoundation                0x1158da554 -[UINibDecoder decodeObjectForKey:] + 251
#12  com.apple.UIKitCore                   0x11153f7cd -[UINib instantiateWithOwner:options:] + 1216
#13  com.apple.UIKitCore                   0x111a5b594 -[UIStoryboard instantiateViewControllerWithIdentifier:] + 181
#14  com.apple.UIKitCore                   0x1118bf809 -[UIApplication _loadMainStoryboardFileNamed:bundle:] + 111
#15  com.apple.UIKitCore                   0x1118bfcb1 -[UIApplication _loadMainInterfaceFile] + 274
#16  com.apple.UIKitCore                   0x1118be3e5 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1360
#17  com.apple.UIKitCore                   0x111102a4e __111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 904
#18  com.apple.UIKitCore                   0x11110b346 +[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 153
#19  com.apple.UIKitCore                   0x111102664 -[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 236
#20  com.apple.UIKitCore                   0x111102fc0 -[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 1091
#21  com.apple.UIKitCore                   0x111101332 __82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 782
#22  com.apple.UIKitCore                   0x111100fe9 -[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 433
#23  com.apple.UIKitCore                   0x111105d2e __125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 576
#24  com.apple.UIKitCore                   0x111106988 _performActionsWithDelayForTransitionContext + 100
#25  com.apple.UIKitCore                   0x111105a95 -[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 223
#26  com.apple.UIKitCore                   0x11110aa48 -[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 392
#27  com.apple.UIKitCore                   0x1118bcdc8 -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 514
#28  com.apple.UIKitCore                   0x11147402f -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 361
#29  com.apple.FrontBoardServices          0x118608d25 -[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 448
#30  com.apple.FrontBoardServices          0x118612ad6 __56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 283
#31  com.apple.FrontBoardServices          0x118612300 __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke + 53
#32  libdispatch.dylib                     0x10efeedb5 _dispatch_client_callout + 8
#33  libdispatch.dylib                     0x10eff22ba _dispatch_block_invoke_direct + 300
#34  com.apple.FrontBoardServices          0x1186440da __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 30
#35  com.apple.FrontBoardServices          0x118643d92 -[FBSSerialQueue _performNext] + 451
#36  com.apple.FrontBoardServices          0x118644327 -[FBSSerialQueue _performNextFromRunLoopSource] + 42
#37  com.apple.CoreFoundation              0x10dbbddb1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
#38  com.apple.CoreFoundation              0x10dbbd633 __CFRunLoopDoSources0 + 243
#39  com.apple.CoreFoundation              0x10dbb7cef __CFRunLoopRun + 1231
#40  com.apple.CoreFoundation              0x10dbb74d2 CFRunLoopRunSpecific + 626
#41  com.apple.GraphicsServices            0x11557e2fe GSEventRunModal + 65
#42  com.apple.UIKitCore                   0x1118bffc2 UIApplicationMain + 140
#43  com.banobal.Testing                   0x10b15e83b main + 75  AppDelegate.swift:12
#44  libdyld.dylib                         0x10f063541 start + 1

I’m showing you how to use the command-line tools because they are easier to script. You can save your memgraph file and then you can do any text processing you wish on them to obtain the data you want.

But if you are still working on your command-line skills and feel more comfortable on the GUI, you can do all this in Xcode.

Using Xcode’s GUI for memgraph

When you run your app on Xcode, you can generate a memory graph by clicking the “memory graph” icon on your debug area. It freezes the app, so you can get a snapshot and debug it.

Xcode debug memory graph button

You can also export that memory graph by going to File > Export Memory Graph ... , you can save it and analyze it later if you want.

Ok, by now you should feel more familiar with the tools. I encourage you to play more with these tools, so you get comfortable using them. Click all the buttons, create memgraphs, try to understand the backtrace, etcetera. You don’t want to start exploring the tools when you have a critical error, and you have users expecting your app. Experiment, so when you need to use these tools, they feel second nature for you.

Here is where the real work starts, now we can start debugging.

Some thoughts on debugging

First of all, debugging is an art. Debugging depends on your specif application. When you debug, you have to make sense of what should be happening and what is happening. I can’t tell you how to debug your app because I don’t know the structure of your app, and I don’t know which objects make sense to exist and which don’t. But we can explore some general tips.

Once you find the culprit, fix and rerun leaks until there are no memory leaks.

Now let’s move to the second problem, retention cycles.

How to detect retention cycles?

Retention cycles are harder to detect. In contrast to memory leaks, the issue is more abstract, the compiler can not review your program’s logic, so it is up to you. But we can make use of Instrument’s to check for retention cycles.

Instruments can be executed from Xcode, by pressing Command+I. Now let’s set our profiler to our taste:

Select Allocations as the profile, long-press the recording button until you see the contextual menu appear and select Recording Options.

Instruments recording options

Make sure that Record reference counts is enabled.

I’ll go straight to detecting the retention cycles. I won’t explain Instrument’s interface because it’ll be repeating what Apple already did on its documentation. You should read it it will only take you about 20 minutes to skim through everything and it will give you a good idea of how to use Instruments.

First, we are going to keep an eye on memory usage. Is the footprint of your app making sense based on what you know of its behaviour?

For example, imagine we have an app that uses a navigation controller and switches from a list of items to the details of the items. If we go from the list of items to a specific item detail, our memory should grow. Once I go back to the list view the detail view controller should be released. If that is not the case, then we might see our memory footprint start to go up when we go back and forth between the two view controllers.

We can verify if our persisted objects are growing or being released propery using the Mark Generation option in Instruments.

The process is something like this:

  1. We start the app, let it complete its initialization and hit Mark Generation. This will show you how many items have been created.
  2. If you hit Mark Generation again, it’ll take another snapshot and show you the difference in the number of objects between the previous “Generation” and the new one.

If nothing has changed the Persistent objects should be zero.

Instruments mark generation

Using Mark Generation we can navigate on our app from one state to another and try to make sense of when should objects should be released. Let’s see a “demo”.

Dependency Retention Example:

Let’s imagine an App that uses the coordinator pattern. This is the workflow:

This is our Coordinator. You can see that in userLoggedin function we set the loginDelegate to be the new LoggedUserViewController.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import UIKit

class AppCoordinator: Coordinator {
    var navigationController: UINavigationController

    lazy var appDelegate: AppDelegate  = {
        return UIApplication.shared.delegate as! AppDelegate
    }()

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let vc = WelcomeViewController.instantiate()

        vc.coordinator = self

        navigationController.pushViewController(vc, animated: false)
    }

    func userLoggedin() {
        let vc = LoggedUserViewController.instantiate()

        vc.coordinator = self

        if let rc = navigationController.visibleViewController as? WelcomeViewController {
            vc.loginDelegate = rc
            rc.userInfoDelegate = vc
        }

        navigationController.pushViewController(vc, animated: true)
    }

    func dismissScreen() {
        navigationController.popViewController(animated: true)
    }

}

Let’s have a look at our two ViewControllers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import UIKit

class WelcomeViewController: UIViewController, Storyboarded, LoginDelegate {

    @IBOutlet var username: UITextField!
    @IBOutlet var loginMessageLabel: UILabel!
    var coordinator: AppCoordinator?
    var userInfoDelegate: UserInfoDelegate?

    @IBAction func loginTapped(_ sender: Any) {
        coordinator?.userLoggedin()
        userInfoDelegate?.setUsername("Anonymous")
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func displayMessage(_ message: String) {
        loginMessageLabel.text = message
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import UIKit

class LoggedUserViewController: UIViewController, Storyboarded,UserInfoDelegate {



    var coordinator: AppCoordinator?
    var loginDelegate: LoginDelegate?
    var username: String?

    @IBOutlet var welcomeMessage: UILabel!


    @IBAction func logoutTapped(_ sender: Any) {
        loginDelegate?.displayMessage("Logged out")
        coordinator?.dismissScreen()
    }

    func setUsername(_ username: String) {
        self.username = username
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        welcomeMessage.text = "Welcome \(username ?? "!")"
        // Do any additional setup after loading the view.
    }
}

Alright, now let’s run instruments and have a look at the results.

First lets make our first Mark Generation:

Demo 01 mark generation

Now in our app we click the button and visit the LoggedUserViewController, let’s see the Mark Generation it should have more objects because we loaded into memory the new LoggedUserViewController.

Demo 02 mark generation

Ok, that was expected. Now we hit back to go to our WelcomeViewController and hit Mark Generation, and it should be back to zero. It should be zero because all of the objects from LoggedUserViewController should be de-referenced.

Demo 03 mark generation

There’s a problem, that should be zero. Let’s see what more detail we can get by navigating inside that Generation (click on the arrow when you hover over the name of the Generation). We will see lots of objects including NS and CF objects, we can hide them by clicking on Call Tree and select Hide System Libraries.

Demo 04 generation detail

We can see that we have a LoggedUserViewController that shouldn’t be there. We can now check the Call Tree and check where was it created.

Change to Call Tree in the Detail Pane. Then let’s go into more detail for LoggedUserViewController by clicking on the arrow in the row.

Demo 05 call tree detail

Well, that wasn’t that useful, it only shows that it was created. Let’s see what can we glimpse from WelcomeViewController.

Demo 06 call tree detail welcome controller

That was better, it shows that it has a pointer to LoggedUserViewController from the AppCoordinator. Interesting let’s see the code, just click on the row for more detail.

We can open the source-code on Xcode if we need to.

In our case, we don’t need to open Xcode. We can see from the detail screen that we are assigning the userInfoDelegate to be the LoggedUserViewController. This is what creates a strong reference.

Demo 07 open coordinator code in xcode

Good, now we can fix the code in WelcomeViewController:

1
var userInfoDelegate: UserInfoDelegate? //If not weak creates retention cycle

Fix by making the reference weak.

1
weak var userInfoDelegate: UserInfoDelegate? //If not weak creates retention cycle

When we test again, we notice that the LoggedUserViewController is correctly de-referenced when we hit back on the navigation controller.

We could have done something similar using the memory graph inside Xcode.

Using Xcode memory Graph

Sometimes the memory graph can show more info at a glance.

Demo 08 memory graph in xcode

You can see that the reference to the LoggedUserViewController is through userInfoDelegate, and infer that that’s the problem.

Final Thoughts

As you can see, there is a lot to explore in Xcode, Instruments and all the command-line tools provided by Apple for memory analysis. The memgraph files give a lot of information that you can use at a later time to do additional analysis.

I put more emphasis on the command-line tool, so you get a better sense of what is going on behind the scenes. Also, command-line tools are easier to integrate with other tools or automate, more than GUIs.

You can, for example, add a script that verifies that your app doesn’t contain any memory leaks and add it to your CI pipeline.

Even Instruments has a command-line tool, where you can specify the PID or the device where your app is running.

1
$ man instruments

I encourage you to play with these tools. Get familiar with them. It takes time to master the tools, but once you get comfortable with them, you’ll be able to debug and profile your apps with ease.

Let me know if you have any questions, and feedback is always welcomed too.

Related topics/notes of interest


** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.