Developer Tools • iOS, OS X • 52:01
Efficient memory management is essential to making an app great. Inefficient memory usage wastes scarce resources, can degrade system performance, and can even cause your app to crash. At this must-attend session for Mac and iPhone developers, learn advanced techniques for tracking memory usage in your app. Come to a better understanding of the object life cycle, and discover how to improve your app's memory performance.
Speakers: Daniel Delwood, Victor Hernandez
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
[Daniel Delwood]
Howdy. I'm Daniel Delwood, engineer on the Performance Tools Team. And I'm happy to introduce you to Advanced Memory Analysis with Instruments. So let's get started. Well, what are we talking about today? Memory. And memory is critical to performance. It's an extremely important part of your program's execution and that's because it's such a limited resource. On Mac if you use too much memory, you'll start paging and really slow your app down.
And iPhone -- this is even more important because there's less memory and your app will get terminated. So today we're going to talk about Instruments. And Instruments offers you a lot of tools for tracking down those common memory issues and is really useful -- what exactly for? Well, first of all, it's useful to understand your app's memory usage and this is really a key point because with understanding comes the ability to change and the ability to change your application to do what you want it to do, not what you think it's doing and maybe isn't already.
So use Instruments to reduce wasted memory. We'll have a couple demos of this today. Diagnose memory-related crashes -- a demo of this as well. And I really want to encourage you from the start to be proactive about your memory usage: profile early, profile often, and don't just wait until you get those terminations or your app doesn't fare well in a multitasking environment.
So exactly what are we going to talk about? Well, 5 issues: first of all, eliminating leaks, which is an issue of wasted memory; abandoned memory -- also wasted memory; messages to the allocated objects; responding to memory warnings; and finally, a little blurb on how to use autorelease properly and track that down in Instruments. So let's get started.
What constitutes a leak? Well, leaks are allocated memory that can no longer be reached. This means that they're no longer useful to your program, you allocated them some time, and now there are no more pointers to it. So what can you do about it? Well, you can program more carefully but it's hard to find these sometimes and that's why we provide tools. So for example to make this concrete, say you have an object. My object and it has an instance variable name.
Well, in your init method, you might say something like name = [[NSString alloc] initWithFormat:...]. So that creates a string with a reference count of 1. And then later on, after your object's been used and it's about to go away, you'll be running your object's dealloc method. Well, what happens when you forget to write the [name release]? Well, your object still goes away and that pointer to your string object, but your string object doesn't go away because it still has a rough count of 1.
So all right, what do we do about this? Well, that's what the Leaks template is for. And Leaks, as its name suggests, identifies leaked memory on your behalf. It does this by doing a conservative memory analysis, scans your process's address space, and it looks for blocks in your heap that are no longer referenced. Now, when I say "conservative," I mean that it works really, really, really hard to make sure that it's reliable. So that means it may miss a few leaks but it is very reliable in the leaks that it does report.
So what I mean by all of this is that if you have a bunch of heap objects, what it does is it scans through your stack, looking for pointers to those heap objects, and marks those and also through some global data so this is statics that you may declare and then scans to other heap objects. From those it continually, recursively scans and anything left over that isn't reachable, well, those are your leaks. Well, great.
Now you know what the leaks are; how can you go about fixing them? Well, that's what Allocations instrument is for. And this is sort of the heavy lifter of our memory tools in that it tracks all malloc-type heap allocations. So this includes C's, malloc, calloc, and any other ways - objective-C, when you say alloc or new, C++'s new operators -- all of those things get tracked by Allocations. And then it provides you those malloc events, those free events at the end, and if you specify, you can also get the retain/release and the autorelease event from the middle and those will come in really useful as we'll see in some of the demos.
And the other thing, by tracking all of these allocations, you can type statistics. So how many strings have you created? How many of your object controller have you created? And you can find out when they're deallocated and that sort of useful information. And finally, the most important part is that it gathers backtraces for each one of these events.
And so with that backtrace, you can put it in the call tree and find out what parts of your code are allocating the most memory -- those probably are what you want to focus on. One final note, the Allocations instrument does incur a little bit of overhead with all this tracking. So it's perhaps not the best idea to use Time Profiler or Time Analysis at the same time.
All right, so let's go ahead and show a demo of finding and fixing leaks. All right, I'm going to launch my project and for the purposes of this, I'm going to be showing most of this technology in the iPhone simulator, although all of this applies to Mac, iPhone, iPad as well. So here we go.
I'll go ahead and launch my application. It's a really simple iPhone app. All it does is it displays a bunch of what we want to call "breadcrumb entries." And so these are places you've been, maybe you took a picture and left a description. But you can just go back and forth and there's even a latitude and a longitude. So this is geocoded.
The important part is: is this application leaking memory? Well, we can find that out directly from Xcode by running from this run menu, run with performance tool, and we'll just select the leaks template. So Instruments starts up in the background and starts recording. We'll deal with that later. All right.
So it starts recording in the background and I can start using my app. But the first thing you'll note is that there's some graphs in Instruments being displayed already. So this is the immediate mode where you can see your data as it's being recorded. So if I zoom in on the track view, you'll notice that Allocations right now is showing me where I've allocated the current memory in my application.
So at the beginning there was a spike -- is it launched? And then there was another spike, probably as I loaded in my SQLite database. And if I wanted to change the view, I could even change in the Style option in the inspector. But for now that's good. And leaks -- as I already discovered something -- and so its graph is showing me the number of leaks discovered in the total leak bytes.
So if I want to find out some more information and go ahead and click on the Leaks instrument. And let me scroll a little bit more just so I can find a few more leaks. But in the table view here, what we have is a bunch of what looks like leaked strings. So there's 13 leaked strings and 2 leaked strings.
And this first one says there's 600 bytes leaked -- well, not too much but we should really endeavor to fix everything. And if I turn this down, you'll see that what this is doing is this is taking 13 different leaks of strings and aggregating them into one place because usually code that leaks, leaks multiple times. And so by doing this, you can find that fixing one of these strings will fix all 13 -- in this case, possibly even 15.
So I'm interested in this object. What I'm going to do next is take a look at the backtrace. So at the top we have that view that I can bring in and it's the extended detail view. And you may recognize this form of stack trace from the Xcode 4 with the backtrace compression slider at the bottom. So immediately I can see that my code in black is calling NSString initWithFormat from my root view controller.
And I can just double-click to jump there, see it my source view. And I'm going to open it in Xcode just to make it a little bit easier to see. And so what we have here is that string, that subtitle text, the latitude and longitude that you see in the entry.
And since most leaks are pretty simple, I can just go ahead and look around this method and make sure I did things right. So I have an alloc initWithFormat -- that gives me a reference count of 1; there's no autorelease at the end. So I need to release this somewhere and if I follow it through in the function -- oh, there we go, we did release it properly. So it looks like this isn't going to be a really simple leak.
So going back to Instruments, the biggest thing to understand here is that leaks aren't always that simple and there aren't just the allocation point -- you need to know the full story and that's why we record the reference counts. So I'll just go ahead and select any one of these leaks and press the Focus button next to it. And it gives me the reference count list so I can see that there's a malloc, a retain, and a release, and the RIF count dropped to 1 but what we really want is for it to drop to zero before we have no references to it.
OK. So we looked at the malloc, let's look at the retain. So just jumping here we can see that this is in my breadcrumb cell set location string and that's displaying on the screen. And what we've got is a string coming in, we're copying it, which makes sense, and we're also doing some special work. This is probably why we didn't synthesize the setter.
And I'll even jump to the header just so we can make sure that we specified it as Copy and we did. And great, that looks probably right. And we go back to the history and look at the release. And this again is in that table view cellForRowAtIndexPath so that was the release we saw in the first function.
So obviously it's this retain that's unbalanced and that was in the cell. Well, perhaps we're leaking the cell, too. Or perhaps the cells aren't going away as we expect. So if we go back to the Allocations instrument, the view here is showing you the statistic. So like I was saying, all the -- telling you how many numbers you've created and giving you the number of bytes there.
And what we want to know is: for our breadcrumb cell, how many are still alive? Well, only 7. Well, that makes sense. We have ourselves an iPhone app and so we're probably doing cell reuse properly. Oh, so that's interesting. That means that our set location string really is leaking somewhere. Well, let's take a look at it one more time, now that we know it's at fault.
So we are setting an end location string and we're even releasing in dealloc. But the one thing we are forgetting is: if we call this over and over again, we're going to be leaking the instance variable -- while we properly copy the string, we're not releasing the location string.
And so it's a very simple fix, we can say NLocation string release just above it. But that's not quite right. What if MLocation string and string are the exact same object? Well, following the memory management guidelines, we really should check here and say if MLocation string is not equal to string, then do this. So all right.
Should we build? Go ahead and just Stop and Rerun. And I'll use the app again. And it doesn't look like we're finding any leaks. So there we go, that's just one leak fixed. Again, it's important to make sure that you look at the whole life cycle of the leak.
[ Applause ]
It's just looking at the allocation point. And the reason you can't do it is because it's not the whole story. You need to know what the object was assigned to, what instance variables, where it went. And it's also because framework-created objects can actually be leaked by your app code because if you get an object back from the framework and you stuff it away in an instance variable and, say, forget to release it in the dealloc, you're responsible for leaking the object.
Finally, just want to reiterate: focus on a single instance to investigate. Use that little Focus button and just look at one instance. Finally, memory management guide: it's a really useful document, there's a lot to it and, you know, I still learn great things from this thing. So that was eliminating leaks and that's one type of wasted memory. Another type of wasted memory -- it's even harder to track down -- is abandoned memory.
And we find that it's actually more times even more important than leaks. So what is it exactly? How do we define it? Well, leaked memory was memory that is allocated but can no longer be reached. So that means it's inaccessible and the tools -- just look for any blocks with no more pointers.
Now, abandoned memory is a little bit different from that; it's accessible allocated memory that is never used again. It's conceptually abandoned by you, the programmer, abandoned by your app; it's leftover and it's wasted or perhaps just forgotten. And so this memory is not nearly as easy to detect and more importantly, it also occurs in garbage-collected code. So you really have to be careful about abandoned memory and it's a big problem.
So let me give you a concrete example of what I mean by that. How can you abandon memory? Well, one way is extraneous information -- information that your app doesn't really need. So for example, let's say you have a tic-tack-toe game and you want to support one little feature: undo or redo the very last move, that's it.
Well, you could implement it pretty simply by saying, "Whenever a user makes a move, add that current game state before they make the move to the previous game states." And then when someone selects a new game, well, you'll add a blank state because you don't want to undo pass a new game.
And then you'll probably have a button action that says, "Undo or redo whatever the last move was." And so that just looks at the last game state and switches it. So where is the abandoned memory here? Well, the problem is that we've got a previous game state's array and every single time the user makes a move, we add more states to it.
Well, only the last one's useful for this simple undo and redo feature. And all of the rest of it is abandoned and will just grow without limit as they continue to use your application. Another common example is a faulty cache. Now we've seen this many times and caching itself is great. Caching makes your app more responsive, makes it faster, but a faulty cache is what I'm talking about and that's a cache that doesn't work as designed.
So take this example: it's just a quick little function to look for an image in a directory with an index. So if you've got 10 images, maybe you're looking for image 3, right? So it looks in the image cache first and says, "Hey, is there an object in there with this directory URL and this index?" And then if there's no index, it creates it alloc init autorelease, great.
And it sets the image in the cache for that stream key, using the URL and the index. Well, let's pay really close attention to this example here because the key that we looked up was using stringWithFormat%@. And that calls description on URL and gets the key we wanted, the URL followed by the index.
However, the key that we set the image as was actually using the %d. So this is just a typo. It's an easy mistake to make, but in this case it's a really hard one to find because it looks like your cache is working properly but every time it's actually allocating a new image and this is definitely not what you want.
OK. So if that's what abandoned memory is, how do we detect it? Well, there's a basic principle and that's: memory shouldn't grow without bound when repeating operation that returns the user to the same state. So conceptually this makes sense to programmers. If you say, "Push and pop a view controller," you'd expect the memory goes back to where it was. Or say maybe you open and close a window, right? The idea is that you think your application -- before using the window and after using the window -- will be the same thing.
Or maybe it's even a different, more subtle action such as just toggling an app preference on and off using a check box. Maybe it's setting user defaults behind the scenes. The point is, you'd expect it to go back where it was. Now this is where you would need to use Instruments to verify your expectation and making sure that you're not abandoning any memory. So there's three steps to it. Three steps that you as a programmer need to do: one, get your app into a starting state. So let's say you're doing that pushing and popping of view controller, you launch your app and get it to the first view.
Then you need to perform an action and return back to that state, that's your user scenario -- that's what you're going to repeat over and over again because then you take a snapshot of the heap using a new feature in the Allocations instrument. And as I said, it's really important that you repeat steps 2 and 3.
You need to perform that scenario over and over again and keep taking a snapshot. Because the first time there may be correct caching or maybe you're warming up something in your application. And so it's that over and over again. It shouldn't increase without bounds as you repeat that action. So let me go ahead and show you a demo and help make this make more sense.
So our application had no leaks as last time we checked it and this time I'm going to say, "run with performance tool allocations," to use that new feature as part of the Allocations instrument. So it comes up and it's showing me the statistics in the table view here. And so I've gotten my application to a starting state, great.
Well, now what I want to do is I want to take a snapshot and that's what this new heap-shot analysis category with the Mark Heap button does. And so when I press it, you'll notice in the Track View a little flag appears and in my Detail View, the view has been updated to show me the heap-shots, that's what we call these snapshots. And the one I just took was the baseline snapshot. And so it shows me there's about 3 megabytess of growth since the beginning of the application and then there's 21,000 objects still live. OK, great.
Now I need to repeat my scenario. So what I'm going to do is I'm just going to click on the first one and go back. And you can even see there's a bump in the Track View as I did that, probably as some caches were warmed or something like that. So now all I'm going to do is press the Mark Heap button again. So I'll do it a second time and even a third time.
And so what we notice here in the Detail View is that the first shot I took, there was a heap growth of 167 kilobytes with 2100 objects that were still living now from that snapshot. And then the other two were growing by about 400 objects. Well, is that going to continue forever? If I just click back and forth, is my app going to grow by 400 objects every single time? It sort of looks like it. So I've done it 6 times and now I'm done.
Oh, but wait, I actually wanted to take a snapshot at the end there because I forgot to before I stopped the app. Well, what's was very nice about this new feature is you can do analysis after the fact. So a few outtakes of the old screen or maybe you have an old trace document that you want to analyze. All you do is go to the Track View, drag the inspection head to where you want the snapshot, and press Mark Heap. And it drops another snapshot point there.
[ Applause ]
How do we fix this? Well, what we're going to do is we're going to find one of those that seems to be most representative of these snapshots, say this one -- 366 objects. So I'm going to use the Focus button, just focus in on that. And what I see is all those objects that were created in that time range that are still living at the end of the program's execution.
So these are the objects I'm really, really interested in because the fourth time I would expect none to be alive at the end of the program's execution. So I'm going to look for objects that are probably created by me and the first thing I see is that there's a breadcrumb entry. My app is called Breadcrumbs.
Each one of those rows represents an entry and there's one of those. And that's probably holding onto some instance variables and some other stuff. Is there anything else I recognize? Oh, yes, a composed view controller; I wrote that. Whoa, there's one view controller still live after every iteration but I didn't link it.
Well, to figure this out, what I need to do is just turn this down and see the address of the view controller and I can bring in the Extended Detail View and show me where it's allocating. Now I go ahead and jump to that and I'll open an Xcode here so you can see it. This is my "did select row at index" path method.
So user taps on the row and what we do is we say, "OK what's the entry that that row represents?" Once we have that, it looks like we're actually caching view controllers -- don't necessarily know why we need to cache before each entry but it looks like this is performance enhancement we made to the application.
Now the interesting thing here is that we notice that our snapshot was creating the object every single time. Now, why? Because I clicked on the first row but there's the same entry every single time. So why is that? Well, it's probably a faulty cache on our fault and so what we look is we see that the cache is looking up for the key entry, which is a breadcrumb entry type, it's an object. And that we're setting it for the entry as well. So that looks like these two match, that's good.
What about the entry? Is it performing properly? Well, to use it as a key in NSDictionary, the entry has to actually conform to NSCopying and that's because by default, NSMutableDictionaries will copy in their keys. The other things we need to make sure that we wrote correctly -- so that there's copy with zone to conform to NSCopying -- the other things we need to write are "hash" and "is equal" because the dictionary needs to know how to hash it and how to check if two different keys -- two different objects -- are equal. But we've clearly written hash and it's just the row ID because what these breadcrumb entries actually represent is an object from a SQLite database so that's the primary key, that's all we need.
But there isn't an "is equal." So the problem here is just sort of like comparing two cans of soup, right? If they're both tomato soup, well, they're different cans but we haven't actually taught our cans how to compare to each other to read the labels. And so what we need to do is write an "is equal" method. So I will go ahead. I've got one prewritten here. There we go.
And I'll just drop it in. And it's a really simple little "is equal." It says if the other object is the same class, then we just compare the row IDs. Great. So simple fix and let's see if it actually fixing our abandoned memory. So run with performance tool Allocations. Here we go, back in the same document. And I'll wait until it gets all ready and take a snapshot, a baseline snapshot. Let me see here.
Oops. And then I'm going to repeat my scenario over and over again. So do it the first time, do it the second time, and you'll notice that, you know, our change hasn't made much difference on the first time -- it's still about 2100 objects. The second time seems to be a little less, about 119. Well, let's keep doing it and see if there's any change. So the third time, the fourth.
And one thing we'll notice here in a minute is that the number of objects in those previous snapshots is actually going down from the original numbers. So I'll talk about that in a little bit. But the main thing we want to notice here is that for snapshots 4, 5, 6, and 7, there was no heap growth, none whatsoever when we pushed the view controller and when we popped the view controller. So now we've verified that our application behaves as we expected and that our cache is indeed working now.
So that is the demo.
[ Applause ]
So I want to talk a little bit more about the details of how those snapshots work. But the first thing, if you remember anything about abandoned memory, is that it will require some work on the part of the programmer: you need to follow the steps, you need to have a user scenario, and you need to verify that your application's memory growth isn't going up over time just because you keep stashing away memory. And very simple things, such as the typo I showed you can contribute to that.
Now those heapshots. Well, they're not snapshots in the classical sense of being immutable. So let's say for instance that your application starts up and you get it to that starting state. Well, once you get to the starting state, you take a baseline snapshot. Now, that doesn't mean that all of those objects in the snapshot will still live to the end of your program. And that's what you're interested in.
So that when you actually repeat your scenario for the first time and take a snapshot, perhaps there's some caching and you see 4 objects, this is sort of like what we saw in the demo. If you repeat it again, take a snapshot, maybe there's two the second time. But again, those objects can go away later and that is what you're looking for.
And so as you repeat it over and over again, hopefully you've coded well enough that those objects will go away and that the number in between those two snapshots -- the number of objects live at the of your program -- will drop to zero and that is when you actually fix the problem of abandoned memory for that scenario. Perhaps it's time to look at a different scenario. All right. That's enough for me. I'd like to pass this off to my colleague, Victor Hernandez.
[ Applause ]
[Victor Hernandez]
So thank you, Daniel. My name is Victor Hernandez. I'm also an engineer on the Performance Tools Team. So the next memory issue we're going to be discussing is messages to deallocated objects. And unlike the issues that Daniel talked about, this one can actually cause a crash.
You probably have seen plenty of crash reporter logs; here's yet another one. But what makes this one unique? Well, if we look at the crashing thread, you'll see that it's dying in objc_msgSend. These can be really hard crashes to debug because you don't know what the object was supposed to be at the time of the crash. So how can you get yourself into this situation? Well, it could be because of an overreleased object. In this example, you have an NSString that's being allocated and it gets a rough count of 1.
If at some point in the future, you release that string, its rough count goes to 0, causing it to be deallocated. And then later on, if you pass a message to that deallocated object, well, there's your crash. So how can you go about debugging this? Well, Instruments provides you with a very useful thing called the Zombies template.
And what happens here is a little bit different: you allocate your string, you have a rough count of 1; then you do the release and the rough count goes to 0. But instead of deallocating the object, you instead turn it into a zombie. And this is really useful because at the later point when you actually pass the string by appending format message to that string, you're actually passing it to the zombie, which Instruments records and it actually notifies you with this really, really great dialogue that comes up and you're one click away from finding out what all the information you need to actually fix this crash. So there's no better way to show you how this works than with an actual demo. So I'm going to launch Xcode and I'm going to bring up my program. It's right here. And -- all right.
So I'm going to quickly launch this application. So the demo application is a simple reader, it consists of a library of books, each book has an entry with a title and its author and you can go ahead and read the book. My users have been reporting that there's been intermittent crashes. Let me see if I can actually reproduce that. Sure enough, there's the crash.
And if I look at the debugger, it's actually dying in objc_msgSend. It is my crash, great. So but I'm actually going to look at this and find out if I can get much more information using Instruments. So let's launch Instruments. There's a whole variety of templates for me to choose from. I'm going to be running this in iPhone simulator. And this is the Zombies template that I'm interested in.
So what's interesting here is that the first thing you'll notice is that it looks just like I have the Allocations instrument and there's no zombies, that's the thing about it. Well, in fact, that's actually not the case. If we look at the inspector, you'll notice that there's two extra settings: set one, enable zombie detection turned on, that's great; the other one is right here, which is record reference counts.
So in this case, we'll not only get the memory history that consists of the allocation and the deallocation of the object, but we'll also get all of the retain releases and autoreleases in between. And that's exactly all the extra information that we really need to be able to debug this well. So let's go ahead and launch our program. OK. Let's see if we can reproduce the crash. Scrolling. Ah ha, there it is, great.
So Instruments tells us that a zombie has been messaged -- an Objective-C message was sent to your deallocated object at this address. And here's the one click away. You just go to the Focus button and you press that and down below you get the complete history -- the memory history -- for this object. The first thing to note is that we now know actually the type of the object; it's a string and it has 13 memory events.
The first one is its allocation, followed by a series of autorelease retains and releases that pump the rough count to positive numbers, eventually getting to the point where it gets a rough count of zero, which in this case turns it into a zombie and then any later memory event or message sent to this object will actually be reported as a zombie event -- that's really useful. So the place to start with debugging this is to actually look at the last zombie event and see what it tells us.
So I'm going to bring up the extended detail view. And it looks like it's happening on this line. So I'm going to go over here. And just to see this better, let's bring this up in Xcode. OK. OK, so what's going on in this code? OK, it's a method, it's our table view cell selector and so every time that we populate one of the cells in our application where this gets called. But what's so special about it -- oh, right, I added this special code so that when you're displaying the author, it actually checks to see if it has a last name because we don't want to ship any books that the authors are forgetting their last name.
So I'm putting this really ugly string, "X X incomplete author names" so that QA catches this before we ship. Well, this sounds like a good feature, but unfortunately it's causing a crash. Well, why is it causing a crash? Well, it looks like the author variable is being passed the stringByAppendingFormat message and this is the zombie right here. So let's go back and find out where we got the author's variable from -- it's the author field of the book class. Well, let's jump to that definition. Uh-oh. Ah, there we go.
And I want to go to the header because I'm interested in the declaration. Sure, it's a string -- we already knew that -- but looking down below, there's more important information, it's actually a property and it's been declared as nonatomic and retained. That means that every time that you set that property, its old value gets released and the new value gets retained. That might be useful information.
OK. Now that we know where we got this value from, let's go back to Instruments. And let's go back to the history and see how we should continue debugging this. So we got to ask ourselves a few questions: first of all, what could be going wrong? Well, the first thing I could think of is this is an overreleased object.
And if this is an overreleased object, the message might actually -- that it's crashing on -- should be released. That's not the case. So the next thing to think about is whether or not we're correctly protecting that object from being released by another reference to it. And the best way to find that out is to go through all the releases and see if any of them point to anything wrong. So I'm going to go to this release, I'm going to see where it's happening. Oh, that's not that interesting. It's just simply the synthesized setter for the author, although we do know already something about that.
And then -- uh-oh, this is actually happening on the previous line. Oh, right, here's the book. Whenever we set the author property, it ends up releasing its previous value, which is what the author variable's pointing to. So sure enough, this is the bug. So let me go ahead and fix that. I should protect my variable. So one way of fixing this would be to retain it before and after its use. If I could type. There we go.
But in fact, actually, this isn't even necessary because we know that the book.author is going to be set right here so why don't we just get rid of book.author.nil -- equal nil -- which is redundant and do all of this? Save that and try running it again. I'm going to build and I'm going to try running it again and see if I get a zombie again. So I'm going to follow my same steps.
[ Applause ]
So what have you seen there? You've seen that the Zombie template instrument is a great tool for being able to debug crashes associated with deallocated objects.
I want to point out a particular quote from the reference guide that Daniel referred to earlier, the "Memory Management Programming Guide" because it talks specifically about this scenario: "A received object is normally guaranteed to remain valid within the method it was received in (exceptions include multithreaded applications and in some Distributed Objects situations although you must also take care if you modify an object from which you received another object)," exactly the crash that I hit. We modified the book and that ended up modifying the author string that we got from it; we need to be really careful about this.
So what else do you need to know about the Zombies template? Well, first of all, it causes a lot of memory growth because those objects never get deallocated, they just stay around as zombies. So you need to use the iPhone and iPad simulator to debug this because the memory constraints on the other devices. Also, it's not suitable to use with leaks because everything will show up as a leak.
And finally, really important take-home message for this kind of crash: it's not always the fault of that last objc_msgSend that's causing the crash, it could be any of the releases beforehand and you need to know its full memory history to find out how to debug this crash. OK, moving along. The next issue we're going to be talking about is responding to memory warnings.
Memory warnings are a simple fact of life on iPhone OS. When the system needs memory, notifications go out to all the applications. This is even more common now that there's multitasking, so there's going to be more contention for the same memory. Your application needs to respond or be terminated. And there's a variety of ways of writing the codes to handle all these memory notifications. Here's two examples.
But one of the things that can be really tricky to find out is, well, what are you supposed to actually do during that function? Well, it's really simple: to decide what memory to free, you just simply need to know what pages in your virtual memory are resonant and dirty.
And Instruments helps you identify these pages. So first, let's be explicit about this: what are we talking about with resident dirty pages? Your application, it's going to display and edit this Tokyo photograph. It maps it in initially and the protection is read/write and it's been loaded copy-on-write. So if at any point you read it, that makes the memory resident. And then at a later point, when you decide to write it, the page that you wrote has now become dirty; that's resident dirty pages. So how do you find out what pages are resident and dirty? Well, you watch your virtual memory using the VM Tracker instrument.
The VM Tracker instrument takes snapshots of your virtual memory. You can think of it as a visual depiction of the vmmap command line tool. And it provides more granularity than Activity Monitor because it doesn't tell you the accumulated statistics for the whole program, but instead it gives you information for each region and each page of your virtual memory.
For each of those, it tells you the type of the memory, it identifies what kind of protection you have, and more importantly, it reports if it's resident and dirty. There's another really good thing about using VM Tracker, which is it helps you check your work. You need to be proactive about checking to see that your memory warning handler actually does its job.
And what you can do is you can use the simulator to manually trigger a memory warning and then watch your virtual memory usage using VM Tracker to see how your app responds. And it's time to show you a demo to see how to do this. OK, so we're back at the demo machine. Let me quit this. All right. So my -- here we go.
So in the same reader application, I have my delegate, which is where I'm supposed to implement my memory warning handler, but as you can see it's blank; I don't know what I'm supposed to release. So let's run it inside of Instruments to find out. The Allocations template's the one you're going to want to use because it also includes the VM Tracker instrument. So I'm going to choose that one. And you'll notice that it's initially not giving you any data.
Well, the reason is because it's set up to manually take snapshots or you can actually tell it to do that automatically and you can even change the interval at which it does that. Great. Look, it just populated that and it gave me this information. Well, this is the summary review.
It tells you for all the different types of memory how much of it is resident and how much of it is virtual and also its protection and more importantly, look right down here, it actually tells you how much of it is dirty. But this isn't the granularity you want.
You want even more granularity. So you go to the regions map and here, for every single region it tells you that information, specifically it tells you for each range how many bytes are dirty, how many bytes are resonant, what's the total size, and also once again, its protections. And it also tells you the associated file with each of those ranges. That's really, really useful.
You can see it starts off with your application, followed by all of its libraries. And if we keep on scrolling down, we know that this is a reader app so we should eventually see the text files associated with the books we're reading. Where are they? There they are, great.
So I'm going to highlight "History of the United States". You see that we have -- it's a memory-mapped file and that currently the dirty size is zero. That makes sense because all we've done is actually loaded it to be able to get the author and the title out of the text file but we actually haven't displayed it yet. And it turns out that our application is not just displaying it, but it's actually decrypting the encrypted version that we have on disk.
So once we actually display it, it's going to have write the decrypted part back to memory. So let's actually watch it do that. So I'm going to focus in on that file and it's on FBCC. Great there it is -- "History of the United States". And I'll watch what happens when I actually click on this.
Sure enough, I'll focus and you will see that at that address range, it changed from map to VM allocated because it was loaded on copy-on-write and all of it became dirty. This is exactly the sort of memory we should be cleaning up. But I haven't actually implemented anything -- the memory handler.
So I need to go ahead and do that. So let me stop this, quit it, and -- so what should I do here? Well, I basically need to tell the library to give back to the system all memory associated with the books that I'm not currently looking at. Well, sure enough, I've already written a convenience routine that does that and I have it right here and I'll just implement that.
Great. Let me build it. And I'm going to run it again in the exact same way. OK. So I switch over to the VM Tracker. I do a snapshot. There we go. I switch the region's map and I'm going to go back down to my text files. I'm going to follow all the same steps.
"History of the United States". This time it's a 104D. There it is. Once I click on it, I'm going to see that memory switch to VM allocated. And I'm now going to go back. At this point, if I receive a memory warning, I should give that memory back to the system.
Now, the next step is to actually simulate the memory warning. Well, it's right here in the iPhone simulator. I do that. And what I want to see, actually, is I want the memory to go and sure enough it has. That's how you can check your work. So VM Tracker is a great -- [applause] -- so VM Tracker is a great tool for helping you identify all the pages that are resident and dirty in your application and also check your work after the fact that you've actually done a correct job. And with that, I'd like to invite Daniel back on stage to talk about our last memory issue.
[ Applause ]
[Daniel Delwood]
Thank you very much, Victor. Last thing I want to talk about is using autorelease properly and give you some tips and tricks. So first of all, memory high-water mark really does matter in your application. If you're on a Mac, this means that you're going to cause paging, on iPhone we've already spoken about memory warnings.
So you can use the Allocations and VM Tracker instruments graphs -- those graphs that we showed -- to identify spikes in your memory usage. Now, what do they look like? Something like this. And so if you see it in really big spikes like this, you'll notice that maybe it's using 10.6 megabytes. Well, that's a lot of memory.
And many images loading and that sort of thing will actually cause it to spike even higher. So what you really want to do is lower those spikes as much as possible and keep your memory usage as steady as possible. Better would be a graph that looks like this.
Now, you can accomplish this by using more granular autorelease pools by nesting them and perhaps even avoid autorelease objects at some points. Now, there's definitely good places to use autorelease objects when you have to return values from methods and things like that. But the key is being smart about it. So let's talk about loops.
Loops are a really good place to be careful of this. So in this example, what we have is we have a database of employees and all we're doing is we're looping through and trying to separate out the employees by their group ID. Great, it's a simple idea. But you'll notice that the loop invariance, database.lastEmployee.number, well, that's actually calling a property on the object.
And lastEmployee could be a new autorelease object every time, especially if you're loading it from a SQLite database, you know, it's something you need to be aware of and be careful of. In this case, it's really easy to fix -- you just move that loop invariance up a line right before the loop and don't call it every single time.
Another one, some things like selectedRowIndexes on table view. Well, the table view can keep its selected indexes any way it wants. It can keep it in malloc buffer, it can keep them in immutable index array, that's an implementation detail. But the key is: however it does keep it, it probably will need to return you an autoreleased immutable copy so that you don't mess with its internal data. So in that case, calling selectedRowIndexes is returning you a new NSIndex set every single time you call it. Now, in this case, we're not even actually modifying the table's selected rows. So we can also move that up and out of the loop.
And finally, one very, very common thing that you can do and very easy thing that you can do is to avoid some of these convenience methods inside of loops. So numberWithInt, date, those sort of things off the class will return you an autoreleased value and that will grow your pool. If you run this loop 10,000 times, that's 10,000 NSNumbers.
And so since we're just using it as key here, what we could do, the line before say number equals number alloc init with group ID and the right below our use of it in objectForKey, just say "number release." So it's taking a little bit more control of our memory management based on the spikes that we see in those graphs and even the statistics you can see in the Object Allocations instrument.
Finally, I want to point out that there's no magic here; this is just a delayed release. Autorelease isn't doing anything under the covers that's special and you can even see this in Instruments when you look at the rough counting histories. So, highlighted here is an autorelease event but you'll notice that just in the bottom right, there's a release event two down and that's where the autorelease pool gets drained. And so this is how you can sort of see when your autorelease pools pop and fine-tune your usage better.
All right to summarize: memory's a limited resource -- you know this but it's also very, very, very important that you pay attention to it; Instruments can help you and is a great tool, lots of different facilities inside there; and you should really work to avoid wasting and misusing memory and even in the case of abandoned memory, you're going to have to actually put in some work to get those scenarios and make sure what you think is happening actually is.
And finally, please be proactive -- profile your app before the problems happen. Now, before you tune out, I'd like to direct you to our Developer Tools Evangelist for more information, the Instruments documentation, and really plug the developer forum -- this is a lot people who have gone through the same problems, faced the same challenges you have, and they're very, very helpful and give advice on these forums, and we also hang out there when we have time.
And finally, for related sessions -- you might need a time machine for some of these -- but the last ones, the performance optimization on iPhone OS and the automated user testing are coming up, well, actually the last one isn't. That is all. Thank you very much for coming.
[ Applause ]