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.
Table of Contents
Memory issues
There are two common types of memory problems.
- Memory leak: this occurs when a part of memory is allocated but never de-allocated, and it can no longer be referenced by any of the application objects. In Swift, this would be very rare unless you are doing some objective-c interoperability or a bug in some framework object. This will be rare in Swift because Swift uses ARC (Automatic Reference Counting) to handle the allocation and release of memory. What this means is that Swift keeps a count of how many objects are pointing (referencing) to our object. If the count reaches zero, the object can be safely de-allocated because no-one is "using" it.
- Retention cycles: this happens when we no longer use an object, and the object should be de-allocated, but the object is held in memory because of a hard reference. This type of memory problems are sometimes harder to detect because the object is still valid, there is someone still referencing it. In contrast, if no-one references a space in memory that we allocated, it is "easy" to detect. What we have to evaluate here is our own logic and the workflow of the app to see which objects are still in memory that should have been de-allocated.
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:
leaks
- detects memory leaks on a running processheap
- shows the heap of a running processstringdups
- identify duplicate strings or other objects in the malloc blocks of a running process-
vmmap
- displays allocated virtual memory regions on a running process
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:
Run my app on the simulator
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
- Run
leaks
for that PID
1
$ leaks 3281
- 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.
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:
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.
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
.
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).
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.
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.
- Check your leaked object's backtrace. This will give you an idea of who created and maybe you'll find the culprit of releasing.
- Follow your app's workflow and stop to analyze state changes. Start the app, get a feel of the current state, if everything makes sense move to the next step, check, move, etcetera.
- Remove the unessential. Don't try to debug your app with many things going on at once if you can simplify the states do it. This also applies for logs and the visual debugger. Don't clutter your interface.
- Learn to use breakpoints and
os_log
. I mostly use print statements to debug, but breakpoints andos_log
help when the bugs are not trivial.
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
.
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:
- We start the app, let it complete its initialization and hit
Mark Generation
. This will show you how many items have been created. - 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.
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:
- Display the Main Screen, with a user and password (The controller name will be
WelcomeViewController
). - When the login button is tapped, it should navigate to the logged user screen (The controller here will be
LoggedUserViewController
) - We create a delegate protocol,
UserInfoDelegate
, that allows us to send data to the logged user screen. - The
WelcomeViewController
has a reference to theLoggedUserViewController
as aUserInfoDelegate
(which creates the retention cycle)
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
:
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
.
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.
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
.
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.
Well, that wasn't that useful, it only shows that it was created. Let's see what can we glimpse from WelcomeViewController
.
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.
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.
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
- Apple's Instruments documentation.
- WWDC Visual Debugging with Xcode - 2016
- Stack Overflow answer - on identifying Reference cycles
- A nice article on Finding memory leaks with Instruments, read the tips on filtering the information Instruments should record.
- Apple's Debugging Tools documentation. Good reference for checking what each icon means and some info on the Debugging Gauges.
- LLDB and Xcode
- Stack overflow on the difference between memory leak and abandoned memory. Heapshot is what now we call
Mark Generation
. - The article on heap shot analysis linked by the previous stack overflow reference
- iOS simulator command line tricks
- iOS memory deep dive - WWDC 2018
- Apple reference to Malloc Debugging Features
- Stack overflow answer - Understanding memory stack trace
- Debugging Chromium on macOS