Crash with Homepwner_LowMemory


#1

Hi all,

This crash occurs with the author’s stock code from Chapter 15 (“Homepwner_LowMemory”).

Reproduce case:

  1. Install & run the Homepwner_LowMemory application, using the iPhone Device 3.1.3 as the Active SDK. This does not fail using the simulator.
  2. Once the app starts, click Edit and add about 6 new possession items.
  3. Go into each item and take a picture. I did this for a couple of items at a time, quitting the app in between, so that it would be sure to archive the possession data in case of a crash.
  4. Once each item has a picture, fire up the app. Click on each item to bring up the detail page. At some point (after clicking on say 4-10 different items), the application will crash.

Here’s the dump I get in the console:

run
Running...
Switching to thread 11779
Switching to thread 11779
sharedlibrary apply-load-rules all
continue
Program received signal:  "0".

The Debugger has exited due to signal 10 (SIGBUS). The Debugger has exited due to signal 10 (SIGBUS).

Notes:
a. In ImageCache:imageForKey, if I comment out the “[dictionary setObject:result forKey:s]”, then the problem seems to go away. The problem might have nothing to do with the ImageCache - using the ImageCache is probably just the fastest way to run out of memory.
b. I was unable to reproduce this problem using the simulator, even with the “Simulate Memory Warning”.

Can someone try this and verify that they also get the crash?

-Chris


#2

Are you sure you are clearing out the cache when a low memory warning occurs? Have you stuck an NSLog in the clearCache method and made sure the dictionary is empty after it executes?


#3

Hi Joe,

I am using the code downloaded from the website, without modifications.

You need to create at least 6 items with images (perhaps more depending upon your iPhone memory - I have an iPhone 3G, not a 3Gs).

Here’s a clue:
When I display the 4th item in the detail view, everything is fine.
Then I go back to the table view.
If I do nothing for a few moments, the iPhone will send the low memory warning, the image cache will be cleared, and everything is fine - the app keeps working.
However, if I very quickly click on the 5th cell, it reads in the image, adds it to the dictionary, puts it into the imageView within the detail view, and then the app crashes. I never actually see the detail page for that 5th item. I added some NSLog’s to verify that it is getting as far as [imageView setImage:imageToDisplay]; within ItemDetailViewController:viewWillAppear. I never get the NSLog output within the low memory handlers.

Can you get this to fail as well?

-Chris


#4

Hi,

just to let you know that I can also reproduce this.

It takes me about 14 items before it crashes (on a 3Gs).

Gareth


#5

Hi Gareth,

Whew! Thanks for reproducing it. I was afraid it was something quirky about my machine or phone. Maybe we can use this as an excuse to get familiar with Instruments…

It feels like an object is getting released prematurely. It’s been pretty difficult to track down, as the gdb debugger never halts execution - it just bombs out.

-Chris


#6

I’ll check this out when I get a chance, guys.


#7

Okay, I can reproduce it.

The reason it dies is because the operating system believes the application is using too much memory (> 24MB of graphical memory on iPhone 2g/3g). Before the OS realizes you have freed the memory consumed by the images in the cache, another image gets loaded in. This could be due to some internal race condition or maybe it is just erring on the side of memory safety. I don’t really know, but I can offer some suggestions:

  1. In reality, you would have to have a pretty odd reason to have more than one 1024x1024 (or however large) image in memory at a time. In a more-real, less-teachy application, you would immediately cut an image down to a size that fits on the screen better when it is loaded from disk or grabbed from the camera. This, of course, only makes the problem less likely because you are using less memory per image - effectively delaying it, perhaps indefinitely if the user doesn’t create hundreds of images.

  2. You could use a smarter image cache that didn’t rely on memory warnings. I’m thinking of a FIFO queue that you monitor by keeping track of how much memory you are consuming and freeing an image or two at a time before a low memory warning occurs. For example, the image cache would record the size of an image (4 * width * height is the number of bytes of an image, plus a tiny bit of overhead for the UIImage object internals) when added to the cache and add that number to its running total. When you get close to some threshold (24MB is where Apple cuts you off on the original iPhone and 3G, but you must account for other image objects as well), you could drop the first one or two images loaded in. Now, because you don’t know what other objects might still have a hold on images in the cache (like the image view), you might want to check the retain count of images you plan to remove from the cache and only remove those that have a retain count of 1.

  3. You could also, on viewWillDisappear: in ItemDetailViewController.m, try setting the imageView’s image property to nil. When the image cache remove all of its images, the image that was last displayed in the imageView will still be resident in memory because the imageView maintains a hold on it. This extra few megabytes might be the tipping point for the OS shutdown (in fact, the more I think about it, the more I believe that is true).


Now, for some fun Objective-C twiddling I did to confirm that this was the problem. I wanted to make sure that the UIImages were immediately being deallocated and not added to an autorelease pool, and I wanted to log the order of things. So I needed to know exactly when the UIImage was dealloc’ed (and if it was autoreleased, waiting for release + dealloc).

Every Objective-C object has an instance variable called “isa”. This is a pointer to the class that is the object’s type. When an object gets send a message, it passes that info to the class that is pointed to by this isa pointer, and the method that matches the message selector in that class is executed (with the implicit variables self set to the instance sent the message and _cmd set to the selector).

So, if I were to create a UIImage object as normal, and then change its isa pointer to some UIImage subclass that I created, I could override retain, autorelease, release and dealloc to NSLog what was happening. When the image object, which would now be of my new subclass type, was sent one of these messages, I could see some debug logs. The implementation of that class, whose interface is declared as ImageMemory : UIImage, looks like this:

@implementation ImageMemory
- (id)retain
{
	NSLog(@"%@: Retain (%d)", self, [self retainCount] + 1);
	return [super retain];
}
- (id)autorelease
{
	NSLog(@"%@: Autorelease (%d)", self, [self retainCount]);
	return [super autorelease];
}
- (oneway void)release
{
	NSLog(@"%@: Release (%d)", self, [self retainCount] - 1);
	[super release];
}
- (void)dealloc
{	
	NSLog(@"%@: Dealloc", self);
	[super dealloc];
}
@end

Of course, the isa instance variable is protected so I couldn’t just create an image and change its isa pointer wherever I wanted to. That is to say, I couldn’t do this:

UIImage *img = ...;
img->isa = [ImageMemory class];

But, any instance can access its own protected instance variables, so I added a category to UIImage to change its isa pointer.

@implementation UIImage (Swizzle)
- (void)swizzle
{
	self->isa = [ImageMemory class];
}
@end

Then, I changed the code in ImageCache.m for the method imageForKey: to the following:

     result = [[UIImage alloc] initWithContentsOfFile:pathInDocumentDirectory(s)];  
     [result swizzle];
     [result autorelease];

Anyway, I thought that might be a fun thing to know. :slight_smile: You also might be wondering why I didn’t just create a category for UIImage that replaced autorelease, retain, release and dealloc: if I did this, I couldn’t call UIImage’s implementation of these methods. They are gone for good and I have replaced them. If I were to naively call [super release] in my replacement release method, it would go to NSObject and not UIImage.


#8

Hi,

Fascinating stuff - I’m starting to realise how useful categories are.

I put some memory monitoring in to see what was happening and I start at 160MB, initially lose about 24MB each time the image is cached (which pretty much tallies with the 4widthheight for the two instances) and get down to 11MB just before the crash.

Setting the image to nil in viewWillDisappear certainly helps and I have to work a lot harder now to reproduce it.

Anyway - huge thanks for taking the time to investigate and especially sharing your workings.

Gareth

P.S. Whoever wrote that ImageCache class needs to go on one of your courses :smiley: