iOS 内存调试 相关文档

来源:互联网 发布:linux查询用户密码命令 编辑:程序博客网 时间:2024/05/22 08:24
转自:http://liam.flookes.com/wp/2012/05/03/finding-ios-memory/
转自:http://www.friday.com/bbum/2010/10/17/when-is-a-leak-not-a-leak-using-heapshot-analysis-to-find-undesirable-memory-growth/



Finding iOS memory

As I get more familiar with iOS, one difference from my experience with consoles is the general mystery about where resources end up in memory.

For consoles a typical game allocates as much memory as it can from the OS, puts it into its own memory pools and then is as miserly as possible from then on.

On iOS, even figuring out how much memory you’re using is pretty difficult! There’s not too much information available at runtime, and it’s a bit off-putting when I count up the allocations my game has made, and the system reports that it is using 400% more than I expected. Ouch. The documentation seems a bit scattered, so most of this post is me documenting my discoveries as I go. I’m certainly not an expert here, so take all of it with a few grains of salt.

If you google for information about memory on iOS, the first recommendation is to look at the Allocations instrument. This is generally good advice.

Allocations

The allocations will show you in a reasonably nice way all of the calls your application makes to malloc. If you’re using C++ or Objective-C, calls to new or alloc end up falling through to… malloc! So any standard code side allocations will show up here with a callstack to help you track it down, which is pretty handy. As a reference in my game if I disable my internal allocator, this shows a pretty stable usage of around 16 MB over 12 thousand allocations.

But wait… where are my textures? I know I’m using some memory for my textures and buffer objects. And they are nowhere to be found. And if I swap in TCMalloc or dlmalloc as my allocator, all of a sudden I’m only using 4.3 MB. And if you’re using MonoTouch or Unity, you might get suspicious about not seeing the C# heap memory being taken into account. So off to other instruments we go!

Memory Monitor

The number that’s usually the most off-putting is the one reported by the Memory Monitor instrument under the Real Memory column. This seems to include EVERYTHING! Textures, executable, shared libraries, actual allocations. You name it, it’s yours and counted against you. Of course it’s pretty much impossible to figure out what’s using it from just the one number… but it’s a start. A side note is that this number is the same as what this call returns:

 kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);    

So it is available at runtime at least. That number also tends to match the number reported in out of memory crash reports, indeed if you dig around Mach you’ll see the number reported is the number of pages multiplied by the page size. One fun thing about the memory monitor is you can see other applications and how much memory they’re using. The biggest one I’ve found so far is Infinity Blade by Epic, which peaked in at a hefty 165 MB of memory, or around 1/4 of the available memory on my iPhone 4. My game when running pokes in at 28.62 MB, which we’ll note is significantly higher than the 15 MB that Allocations reported.

VM Tracker

Next down the list is the number reported by the VM Tracker (Virtual Memory Tracker) instrument. Things get interesting with VM Tracker, as it looks to be the most finely grained method for iOS to report an application’s memory usage.

In my game we can see that it has a *Resident* size of 97.69 MB, and a *Dirty* size of 36.43 MB. The *Resident* size is presumably all of the memory/pages that the system associates with my App. It is close to 3x what the Memory Monitor reports! So clearly the Memory Monitor is not assigning the blame for all of that memory to my game. The *Dirty* size is also bigger than what Memory Monitor reports, but it’s at least a little bit closer to what’s expected. From reading through the Apple developer forums it looks like “Dirty memory usage is the most important on iOS, as this represents memory that cannot be automatically discarded by the VM system (because there is no page file).” So let’s go through the categories I’m aware of:

IOKit

IOKit is… textures you’ve uploaded with OpenGL, i.e. all those calls to glTexImage2d. If you’re not using OpenGL, your image resources will likely go into a different label, “Core Animation” and “CGImage” come up, but don’t quote me on it.

VM_ALLOCATE

The VM_ALLOCATE label is a bit of a grab bag. I set a breakpoint on the call to vm_allocate() and noticed that my glBufferData calls are in the callstack, so VBOs allocate their memory from this region, (not all vertex arrays will end up in here though, some also end up hitting malloc!). In fact it seems like anything that uses mmap will also show up in here, presumably because mmap calls generate vm_allocate() under the hood. If you’re using dlmalloc, it will use mmap by default, so dlmalloc’s heap is actually accounted for in this label and not included in the malloc sections. Likewise it seems mono uses this for its memory, so if you have a Unity game and are wondering where the rest of the heap is… VM_ALLOCATE is your culprit!

SBRK

If you see an sbrk region, it means you are using software designed for systems that expect the sbrk() to do something reasonable. iOS (and OS X!) are not those platforms. They just get the lovely call foundhere, which shows it calling vm_allocate under the hood and with a hardcoded 4 MB. Oof. If you’re using TCMalloc make sure you modify the config.h to not use sbrk, as the autoconfig will find the header and assume it’s a good idea to use it.

__TEXT

The __TEXT section is using quite a bit of memory in my example:  584 KB for the application itself, and 27.21 MB total! As one might fear, unfortunately a big chunk of this is outside my control. __TEXT contains your actual executable code, as well as other things marked read only, like literal strings and other constant data. It also contains those sections for any dynamic libraries in use by running applications, so even if you cut down on the libraries you’re using, there’s no guarantee your __TEXT section will shrink.  What is under your control is the size of your executable, including constant data, as well as the libraries you use/load, although the latter may not be much help.

__DATA

The __DATA section contains static sections of your executable that are writable. So those big global buffers you shouldn’t be using will show up here. If you look at the map file for your executable, you can see which parts of the __DATA section in particular are taking up space. The bss would be your application’s static data. If you’re unfamiliar with what that that might mean, in C/ObjC/C++ it’s typically made of the following:

  1. Non-const variables that are declared static in your classes or functions.
  2. Global variables.

You can read in more detail here what the executable sections mean, including our __DATA section and __TEXT section friends.

MALLOC_TINY, MALLOC_SMALL, MALLOC_LARGE

Then we get to the malloc regions. There are three of them, and their sum will be the closest number to what Allocations reports: all our ‘standard’ allocations will end up somewhere in one of these three pools. From this division, and by browsing through the Libc code, we can see that the mach allocator is partially inspired by Hoard, and has some small thread caching behaviour to help threaded applications.

Assuming the iOS malloc is using that source file, we can see that ‘MALLOC_TINY’ means up to 496 bytes, ‘MALLOC_SMALL’ means up to 15 KB, and ‘MALLOC_LARGE’ for everything bigger than that. I did some quick tests with iOS 5.1 and can confirm those sizes make sense. Leaking 400 bytes goes to tiny, 600 bytes go to small, and 16000 bytes end up in large. It seems to be a generally spiffy allocator actually, so carefully consider the downsides of using your own!

A useful rundown of OS X’s allocator (which seems to be the same!) tells us there used to be a ‘huge’ malloc, but it was removed when Snow Leopard hit, somewhere around iOS 4. Besides the standard debug calls available in malloc.h, there’s a useful undocumented function called scalable_zone_statistics and it can work well for getting statistics about how you’re hitting the different malloc sections at runtime, I use it like so:

extern "C" boolean_t scalable_zone_statistics(malloc_zone_t *zone, malloc_statistics_t *stats, unsigned subzone);malloc_statistics_t stats;scalable_zone_statistics(malloc_default_zone(), &stats, 0); //or 1 or 2 for the small/large

As it’s not public, I would recommend against using it in anything you hope to release.

Miscellaneous

TCMalloc – this seems to be WebKit using TC Malloc internally. Not using webkit you say? Neither am I. Seems to top out around 250 KB for me.

Memory Tag 70 – This wonderfully named section seems to be related to UIImage or other UIKit calls. For me it’s only 32 KB, but I’ve seen some apps report much higher usage. Make sure you’re loading your images with UIImage with the correct method!

LINKEDIT – The ‘link edit’ (I always read it first as Linked-It…) is for the dynamic linker to patch up calls to dynamic libraries. I think. Most of this will be in under the dyld_shared_cache, which presumably means it’s for shared libraries.

WAT?

I’ll keep updating this post as I learn more about how things work on iOS, but hopefully this will be useful to other people who start to wonder where all that memory is going. If I have anything wrong or there’s things that should be added here, please let me know!

This entry was posted in iOS, Programming and tagged memory, optimization,vm tracker byLiam. Bookmark thepermalink.







     资料2

When is a Leak not a Leak? Using Heapshot Analysis to Find Undesirable Memory Growth

The other day, I was in need of a Cocoa application that launches quickly that has a standard document model. At random, I chose the rather awesomeHex Fiend. As I often do, I also hadtop -u -o pid running in a Terminal window.

And I noticed something odd. As expected, the RPRVT of Hex Fiend was growing on eachcmd-n. However,the RPRVT was not decreasing the same amount every time I hit cmd-w.

That ain’t right. Or it might be. Beyond evidence that a memory use problem may exist,top is a horrible tool for determining if a problem is real or what the actual problem might be.

In this case, the issue looks like a simple memory leak. Hex Fiend is allocating and retaining some set of objects, but not releasing them. The easiest first step is to use theleaks command line tool:

% leaks "Hex Fiend"leaks Report Version:  2.0Process:         Hex Fiend [3435]Path:            /Volumes/Data/Applications/Hex Fiend.app/Contents/MacOS/Hex FiendLoad Address:    0x100000000Identifier:      com.ridiculousfish.HexFiendVersion:         2.0.0 (200)Code Type:       X86-64 (Native)Parent Process:  launchd [122]Date/Time:       2010-10-16 20:47:09.935 -0700OS Version:      Mac OS X 10.6.4Report Version:  7Process 3435: 22980 nodes malloced for 2600 KBProcess 3435: 0 leaks for 0 total leaked bytes.

OK; whatever the problem is, it isn’t “leaked” memory in the traditional definition of “leaked memory”.

That is, whatever memory is being allocated and never released is still being referenced somewhere. Maybe a circular retain. Maybe something that has a weak reference from the rest of the App’s object graph such that leaks can’t detect.

In other words, this isn’t just a simple memory leak and it will require more advanced tools to fix.

Fortunately, the Allocations Instrument provides exactly the tool we need. It is called Heapshot Analysis and it is brutally effective at deducing these kinds of problems.

To use:

  • Launch Instruments and select the Allocations template under theMemory category.
  • Target the application you want to analyze. It can already be running or you can launch it from Instruments. As well, you can launch it from Xcode via theRun With Performance Tools menu.
  • (10) Do something in the application where you return to the starting state. ForHex Fiend, this particular case is as simple as creating a new document and then closing it. For an optimal application, this activity should effectively cause no memory growth (or very little).
  • In the Allocations Instrument, press the Mark Heap button
  • Goto 10 (and repeat 5 or 6 times).
Step 1.png

I ended up with the data as seen to the left. Each “Heapshot” iteration represents opening, then closing, an untitled window with no data in it.

The “Heap Growth” and “Still Alive” columns provide a summation of all of the objectsin that heapshot that still exist in all subsequent samplings of the heap. That is, inHeapshot 3, there were 36.08K of allocations spread across 260 allocation events that continued to exist throughout the rest of the run.

Specifically: the values in those columns represent permanent heap growth.

When creating, then closing, an untitled document there should be, ideally, no heap growth. That there is ~35K per document of permanent heap growth indicates that a leak does exist (regardless of what leaks said above).

Note that as you continue to iterate, you might see the values of previous Heapshot samples decrease. That is because objects allocated in that sample — at that heap mark — have been released. That is,every single object listed in that table — all ~25,000 or so in that screenshot alone — are very much in memory, using resources, and sticking around.

Instruments lets us dive deeper.

Step 2.png

Not only can we expand any given Heapshot iteration to see the inventory of permanent objects allocated in that iteration, but we can dive all the way down to the individual allocations and see a backtrace of exactly where it was allocated.

Looking at many of the allocations, they all seem to be during either the initialize of an instance ofMyDocument or during loading of the user interface related with the same.

No surprises there; the inventory of objects is clearly related to the document.

What is surprising, though, is that an instance of MyDocument doesn’t show up on that list! It looks like the instance ofMyDocument is correctly being deallocated on window close,but much of the user interface related to the document isnot!

While we clearly have enough evidence to suspect MyDocument as being the source of the issue, we can confirm this further within Instruments.

Turn on Record reference counts in the Allocations instrument. When the app isn’t running, hit the little (i) in the Allocations track in Instruments and click the check box. Then run the application and do the same set of samples (or turn this on from the beginning if you didn’t forget like I did).

Now, when you click through any allocation, you’ll see a list of events that includes the point of allocation and all subsequent retain/release events. Clicking through many of the random objects in any givenHeapshot sample shows two things. First, all of the objects ended with a retain count of 1 — not 2, not 5, but 1 — this indicates that whatever the problem is, it is likely pretty consistent and we can fix it once and be done with it. Secondly, the non-UI related objects likeDataInspector or NSBigMutableString have very few events and they largely come from[MyDocument -init], further confirming our suspicions.

Now it is time to turn to the source.

Which I need to download; all of the above was done against the app without the source.

OK — got it — now — start with MyDocument‘s init method (which doesn’t do theself = [super init]; if (self) {...} return self; dance. Boo.) and compare it todealloc

The init method looks fairly straightforward; allocate a bunch of stuff, glue it together, return self:

- init {    [super init];    lineCountingRepresenter = [[HFLineCountingRepresenter alloc] init];    hexRepresenter = [[HFHexTextRepresenter alloc] init];    asciiRepresenter = [[HFStringEncodingTextRepresenter alloc] init];    scrollRepresenter = [[HFVerticalScrollerRepresenter alloc] init];    layoutRepresenter = [[HFLayoutRepresenter alloc] init];    statusBarRepresenter = [[HFStatusBarRepresenter alloc] init];    dataInspectorRepresenter = [[DataInspectorRepresenter alloc] init];    ... etc ...

As a matter of fact, pretty much all of those representers are showing up as still in memory per each Heapshot mark!

And, diving into the individual retain/releases, we see that some of the representers are connected to other representers. So, if anyone of those representers is sticking around, it might likely keep others alive with it, too! Or it could be a circular retain issue. But, before we start trying to deduce hard problems, we should exhaust the simple causes first and look at thedealloc method.

The first thing that jumps out at me is that the dealloc method is trying to do something clever. Instead of line-by-line callingrelease on every object allocated ininit, it does:

    [[self representers] makeObjectsPerformSelector:@selector(release)];

OK. So, what does representers look like?

- (NSArray *)representers {    return [NSArray arrayWithObjects:lineCountingRepresenter, hexRepresenter,               asciiRepresenter, scrollRepresenter,               dataInspectorRepresenter, statusBarRepresenter, nil];}

Waitaminute there…. comparing that array’s contents to the items allocated ininit, we see that thelayoutRepresenter is missing. Nor is it explicitly released anywhere else indealloc!

That’d be the leak right there! And leaks won’t detect it because there are enough non-retained relationships that the objects look like they are still reachable from the application’s core object graph!

The naive fix would be to add layoutRepresenter to the array returned by-representers. However, thelayoutRepresenter seems to be kinda special in its role and, frankly, I really hate trickydealloc games like making an array of objects performrelease.

So, I replaced the performSelector: in dealloc with:

    [lineCountingRepresenter release];    [hexRepresenter release];    [asciiRepresenter release];    [scrollRepresenter release];    [layoutRepresenter release];    [statusBarRepresenter release];    [dataInspectorRepresenter release];
Step 3.png

Running the heapshot analysis again shows that the # of permanent allocations per iteration has dropped from ~250 to a consistent 8. Vast improvement, but still not perfect.

Looking at the remaining allocations, every single one is allocated in drawLineNumbersWithClipStringDrawing.

In particular, it is the drawing call here that is the source of the remaining leaks (except one):

    NSString *string = [[NSString alloc] initWithBytesNoCopy:bufflength:newStringLengthencoding:NSASCIIStringEncoding freeWhenDone:NO];            [string drawInRect:textRect withAttributes:textAttributes];            [string release];

But, wait, how can that be? Well, it could be a bug in the AppKit. Or it could be some kind of a weird cache. As a matter of fact, if you create about 30 documents inHex Fiend, then close them all, you will see the previous heap marks drop to7 objects remaining. So, clearly, there is some kind of a size limited cache that is eventually being pruned. Obviously, not a very efficient cache if it is filling with copies of the same objects. I like to call these kinds of cacheswrite only caches. All the benefits of high memory use combined with all the efficiencies of a 100% cache miss rate! FTW!

The one other leak, though, is that the dealloc method in HFLineCountingView is not releasing thetextContainer. First, I’ll fix that and re-measure. Done. That removes one object from each Heapshot iteration.

OK — so, looking at the remaining objects, we have a set of objects that look an awful lot like a set of attributes for text; a paragraph style, color, etc…

Sure enough, textAttributes is not being released indealloc.

Step 4.png

So, where do we stand?

See for yourself!

Not bad! No leaks on many iterations. That 1 4KB malloc seen in some iterations is likely some internal cache in the AppKit. That it doesn’t always appear and eventually goes away indicates that it is both behaving correctly and can be ignored.

The next step would be to do the same kind of testing, only with documents that contain actual data. Then do the testing after making a set of edits and undoing them.


Heapshot analysis has proven to be an incredibly effective tool for identifying and fixing memory use issues in applications. The above style of use where you pick a task that should return state back to the same as it was when you started is the most effective form of this analysis.

However, this same approach can be used for applications that build up or change state over time (think Mail, which has new messages coming in all the time or an application with an accretion of logs or undo state).

Fire up your application under Instruments and periodically hit “Mark Heap” when your app is in a reasonable state. The more Heap Shots you capture, the easier it is to analyze. Look at any given iteration and ask yourselfWhy do these objects created way back when still exist in memory and is their use of resources justified?. The follow up question isWhat can I do to make the permanent memory accretion smaller?.



13 Responses to “When is a Leak not a Leak? Using Heapshot Analysis to Find Undesirable Memory Growth”

  1. Marshall says:

    Nice writeup, Bill.
    Thanks for the tips.

  2. Mike Llewellyn says:

    One of e best Instruments walkthroughs I have seen, thanks for sharing that. Instruments for me tomorrow morning now:) .

  3. Tom Harrington says:

    Thanks, I’ve been working on leveling up with Instruments and this really helps. Using record reference counts seems like a good suggestion for people who mistakenly expect the -retainCount method to be useful.

  4. ridiculous_fish says:

    Yikes! Thanks for tracking this down, Bill! leaks is not foolproof.

  5. bbum says:

    @Tom Heh — I should add to the article that if you are calling retainCount, you are doing it wrong.

    @Fish — it was a fun puzzle. Hex Fiend is really an awesome, useful, app. Thanks for going through the trouble of open sourcing it!

  6. Iain Delaney says:

    Is that something the static analyzer could catch, or is the coding error too sneaky? I’ve been amazed at the things the analyzer does find.

  7. Core Intuition » Episode 36: I Completely Messed It Up says:

    [...] Using heapshot analysis — Bill Bumgarner’s blog post [...]

  8. bbum says:

    @Iain The analyzer won’t catch issues that require deep, cross-correlative, analysis of your code. It is limited to a single compilation unit — a single .m file — and won’t catch problems that span compilation units.

  9. iPhone development highlights: January 2011 at Ole Begemann: iOS Development says:

    [...] a post from October 2010, Bill Bumgarner introduces the Heapshot Analysis tool in Instruments and how to use it to find memory leaks in your app. If you want to see it in action, I also highly [...]

  10. Michael Tsai - Blog - Heapshot Analysis says:

    [...] Bill Bumgarner on using Instruments to find memory leaks: Heapshot analysis has proven to be an incredibly effective tool for identifying and fixing memory use issues in applications. The above style of use where you pick a task that should return state back to the same as it was when you started is the most effective form of this analysis. [...]

  11. --Nick/ says:

    Thanks — I’ve come across this quite a while after you wrote it, but I’m glad to find it and it’s helping me this evening.

  12. Debugging memory leaks with Instruments and Heapshots | Finalize.com: My journey with iOS and other code adventures says:

    [...] When is a Leak not a Leak? Using Heapshot Analysis to Find Undesirable Memory Growth [...]

  13. devUA says:

    hey i am using ARC in my project. when i do heapshots then Heaps have some which are not released. And some of these are local NSArray which contains NSNumber with int. so i am unable to find why these NSArray which contains NSNumber unable to release in Heapshots even i exit game ( or class which contains these array is released)…

Leave a Reply

Line and paragraph breaks automatic.
XHTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>