I have an array of UIImages that contains some .jpg images downloaded from the net when the app starts. I also have a table view that shows some of these images in its cells. The problem is that when I scroll the table, the app's memory consumption always increases up to the point where the app crashes due to low memory conditions. The table cells seem to be reused fine, so my theory is the following.
Since the UIImageView in a table cell only retains one of the elements in the image array, when the cell gets reused, and a new image is assigned to it, the previously used image is of course not destructed (the cell releases it, but the array still retains). However, the decompression cache used to hold the raw image data (computed the first time the UIImage is displayed by a view) belongs to the UIImage itself, so it also remains. But I'm just guessing about all this.
Can this really be the problem? If so, how can I work around it? My first idea was to create a copy of the UIImage whenever it is assigned to a cell, but looks like UIImages can't be deep copied. Is there any way to tell a UIImage to keep the compressed jpg data in memory but throw away the decompression cache? (In the worst case I guess I can still download all the images and store them in local files, then load them from there and completely release the UIImages when not displayed anymore, but this doesn't seem to be an elegant solution.)
--
I can't post the original code (as suggested in comments) as it is fairly complicated with custom table cells and custom views and even a background thread downloading the images, but I've just created a small test app and it seems to show the same behavior. Here's a little function that is called when the user taps a UIButton:
- (IBAction)onNext:(UIButton*)sender
{
static NSMutableArray* images = nil;
if (!images)
{
NSArray* names = [NSArray arrayWithObjects:
#"IMG_2957.JPG",
#"IMG_2962.JPG",
#"IMG_2965.JPG",
#"IMG_2970.JPG",
#"IMG_2971.JPG",
#"IMG_2978.JPG",
nil];
images = [NSMutableArray new];
for (int i = 0; i < names.count; ++i)
[images addObject:[UIImage imageNamed:[names objectAtIndex:i]]];
int i = 42;
}
static int current = 0;
imageView.image = [images objectAtIndex:current];
++current;
}
Yes, I know that the images array is leaking, that's not the point (in the original code I also want to retain the images for the entire lifetime of the app, and only release on quitting). But. According to Memory Monitor, after the first tap (all images get loaded, and the first one is displayed), the app consumes ~5MB (all jpgs loaded, one of them decompressed). After all subsequent taps, the memory consumption increases by ~2MBs (yep, I tested with 1024x768 images). So it looks like decompressed data of the previously displayed images is still there. I know this wouldn't be a problem if I released the image once it isn't visible anymore, and that would also release the decompressed buffer, but if possible, I'd like to retain all the images, and only release the decompressed data. Is that possible?
If you're reusing cells, make sure those cells implement:
-(void)prepareForReuse
{
// releasing the image
self.imageView = nil; // Or release depending on you memory strategy
self.labelView = nil; // get them all
}
I don't think UIImage has no memory leaks that fatal. I have been working on few applications which has to deallocate UIImage a lot due to memory constraint, but it's still working fine. I believe your image leaks somewhere. Show us some code so someone will point that out for you.
Related
I have a gallery view of photos that are downloaded from the internet so I used Enormego's EGOImageView. I noticed that when i scrolled down my tableview after the images were in the cache, the scrolling would lag. I immediately found that when the image was retrieved from the hard drive with return [UIImage imageWithContentsOfFile:cachePathForKey(key)]; it was working on the main thread so I added the operation to an NSOperationQueue. This reduced the lag by half but the scrolling still stuttered. After going through the code, I noticed that in the success method
- (void)imageLoaderDidLoad:(NSNotification*)notification {
if(![[[notification userInfo] objectForKey:#"imageURL"] isEqual:self.imageURL]) return;
UIImage* anImage = [[notification userInfo] objectForKey:#"image"];
self.image = anImage;
[self setNeedsDisplay];
if([self.delegate respondsToSelector:#selector(imageViewLoadedImage:)]) {
[self.delegate imageViewLoadedImage:self];
}
}
commenting out the self.image = anImage; got rid of the lag completely (but obviously I get no image). And as far as I can tell, if I want to alter the UI, it must be done in the main thread. Is there a way to set the image for the EGOImageView without it lagging the scrolling?
Note: the JPGs are around 50kB
Thanks
Subclass uitableViewCell and do your own drowing in drawContentView.
Resize images in background thread and then present them in cells.
P.S: if the code found on git hub is not good enough for you, try to write your own that suits your problem.
From what I understand, all the steps required to initiate an asynchronous disk query takes a couple milliseconds on the mainthread, and that is enough time for the scrolling to look like it stutters, so I decided to completely remove hard drive caching for large images and instead create an NSMutableDictionary which holds the UIImage as an object, and the NSURL.absoluteString as the key. This works seamlessly but obviously has the disadvantage of being a memory hog. I checked out the memory usage of some photo-sharing apps and I've been able to get the memory usage for the app to over 100MB so it seems everybody else is doing this.
UIImageView *mooshinLogo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"mooshin.png"]];
I'm not sure if im instantiating my image correctly because i've been reading that imageNamed will not be emptied from the cache and should only be used for buttons and small icons. What about background images, and image galleries?
How do I implement UIImageView the right way?
imageNamed: method caches images that you load. In case of low memory conditions or memory warning that cache will be emptied.
If you want to show images in gallery it is better to use imageWithContentsOfFile: method as it doesn't cache the data.
In case of very big images you should use CATiledLayer to display your image using tiles.
No worries! The method imageNamed returns an autoreleased object, and it will do what you call "be emptied from cache" (i.e. its memory will be released) when it is not needed any more.
[UIImage imageNamed:(NSString *)imageName] is an autorelease convenience constructor. This means that it's retain count is increased when you initialize it and decreased at the end of the runloop. You could make 100 of them and they'd vanish from memory a few seconds later, unless they're retained by something else. By passing it to the UIImageView, the UIImageView will retain it and it will stay in memory only until the UIImageView is done with it, so you're doing that correctly, unless you're referring to the OS caching the image in "inactive" RAM. It may do that behind the scenes (and will know when to get rid of it), but you're certainly handling the object's lifecycle correctly. Since you're using the [[Class alloc] init...] way to construct your UIImageView, make sure you later call [mooshinLogo release] or [mooshinLogo autorelease].
(Please just ignore this if you already know.) Objective-C (at least, for iOS development) is a reference counted language. All objects start out with a reference count, or retain count, of 1, from the time they are alloc'd. From there, they can be retained ( [id retain] ), released ( [id release] ), or marked to be released at the end of the runloop ([id autorelease] ). Once the count is zero, they will be dealloc'd, but you should never concern yourself with it's actual retain count and only use objects you own (or are retaining).
When in doubt, you can check with Clang's Static Analyzer. It's finds probably 75% of the your leaks, and I've only had a handful of false positives. Either Build & Analyze, or Cmd+Shift+B.
Others above are right about imageNamed keeps a cache of these images. This is especially true when using NIB files. Releasing a Viewcontroller with ImageViews on them doesn't directly release the associated images.
I had an app that had lots of images with lots of pages in a navigation controller. Eventually it would crash when only using imageNamed method. So, I now use (found here on SO) and over:
+ (UIImage *)imageNamed:(NSString *)name {
//NSLog(#"ImageNamed: %#", name);
return [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:nil]];
}
Now I can control when I want to remove an image and and make it actually clear from memory. The aforementioned app, I would actually load the images on viewDidLoad and remove them on viewDidDisappear. So, when I was 10-15 pages deep in the NavController, I could keep memory down.
I am loading images into a UIImage with the values of a slider (these are pages or slides, if you like). This UIImage switches them very fast with the use of a slider. Problem is, at some point, the app crashes on the device with an error of:
2011-04-02 17:39:01.836 Book1[2123:307] Received memory warning. Level=1
Here's the code:
- (IBAction)slidePages:(id)sender{
int sliderValue = pageSlider.value;
NSString *slideToPage = [NSString stringWithFormat:#"P%i.jpg", sliderValue];
imagePlaceholder.image = [UIImage imageNamed:slideToPage];
pageDisplay.text = [NSString stringWithFormat:#"Page %i", sliderValue];
currentPage = sliderValue;
}
Is there anything I could do to make it more efficient? Maybe the error is somewhere else but I'm guessing it has to do with the fast loading of images.
Still, I don't know how iOS deals with this. Every time I load a new image into the UIImage what happens with the "unloaded" one?
Thanks in advance.
One imortant aspect of [UIImage imageNamed] is that it caches all images loaded in that way and they never get unloaded, even if you dealloc the UIImage that was created! This is good in some circumstances (e.g. smallish images in UITableView cells), but bad in others (e.g. large images).
The solution is to use [UIImage imageWithData] which does not do this caching and which unloads the data when the UIImage is dealloc'd.
More info and discussion here:
Difference between [UIImage imageNamed...] and [UIImage imageWithData...]?
Update
This question has some good info on the question of [UIImage imageNamed:] not emptying its cache when a memory warning occurs.
[UIImage imageNamed:] caches images, so every image thus loaded effectively leaks. I don't know what the canonical solution is, but one option might be to load the image with CGImageCreateWithJPEGDataProvider() and initialise it with [UIImage imageWithCGImage:].
The original (wrong) answer:
You may need to release the previous image before loading the current one.
Loading a bunch of large images in iOS has always been a memory issue. As Marcelo mentioned, you should only keep around images are you currently viewing. All other images should be released so they can be garbage collected and the memory freed up. In my experience, even loading 2 fairly large images (500k-2mb each) will cause memory issues, especially on older devices with less RAM.
I've got a fairly simple app that has the following in the view the program is mostly in:
int currentPageIndex;
NSArray *images;
NSString *nextImage;
IBOutlet UIImageView *firstPage;
IBOutlet UIButton *bigButton;
In the implementation viewDidLoad, I load the array with a bunch of image file names:
images = [NSArray arrayWithObjects:#"image1.jpg", #"image2.jpg", etc, nil];
[images retain];
Each time the bigButton is tapped, the image changes:
- (IBAction)bigButtonTapped:(id)sender {
currentPageIndex++;
nextImage = [images objectAtIndex:currentPageIndex];
firstPage.image = [UIImage imageNamed:nextImage];
}
Everything works as I want it to, except that I am getting a "Received memory warning. Level=1" in the console with my device plugged in. This warning comes up after every 12 images or so, and eventually it crashes with "EXC_BAD_ACCESS"
I thought this would actually be a good way not to put anything in memory, as there is only one UIImageView on the screen and its image is changed as I need it to be.
It is a very simple app so I'm sure the fix is very simple... any ideas what I might be overlooking? Thanks so much!!
Since you get a memory warning, the problem must be that the images aren't released. However, in the code you show, you're handling the images correctly. So the problem is most likely in a part of the code you're not showing us.
The only minor problem is see, which has been mentioned before, is that the currentPageIndex will eventually point outside of the range of the array. But this will cause a different error.
To avoid going over the size of the Array,
currentPageIndex= 0;
for(currentPageIndex in images){
doStuff;
}
OH!!!!! I THINK I spotted the your problem. Whenever you use UIImage imageNamed to load images all the images stay in memmory even though release reference to it. use the other UIImage method:contentOfFile.
One other thing, make sure your images are optimize for iOS. Use .png when posibible.
dibu2z
I assume Image is a retained property.
Try to release it at the beginning of your bigButtonTapped.
Hope it helps.
Could be that you've reached the end of the array and you're trying to access past the end of the array. You could do a
currentPageIndex++;
if ( currentPageIndex < [images count]) {
nextImage = [images objectAtIndex:currentPageIndex];
firstPage.image = [UIImage imageNamed:nextImage];
}
Also could be that the image you listed doesn't exist in the bundle.
There isn't really enough information here to say for sure what your problem is. EXC_BAD_ACCESS generally happens when you try to access an objects that has already been deallocated.
The quickest way to track down the real cause of EXC_BAD_ACCESS is by using the NSZombieEnabled executable argument, and then setting a breakpoint on objc_exception_throw. This will get you a stack trace, and allow you to determine specifically which object you are trying to access.
http://www.cocoadev.com/index.pl?NSZombieEnabled
Using Malloc to debug
So this one is pretty odd ad I'm not sure if the trouble is with the AssetsLibrary API, but I can't figure out what else might be happening.
I am loading an array with ALAssets using the -enumerateAssetsUsingBlock method on ALAssetsGroup. When it completes, I am loading a custom image scroller. As the scroller finishes scrolling, I use NSInvocationOperations to load the images for the currently visible views (pages) from the photo library on disk. Once the image is loaded and is cached, it notifies the delegate which then grabs the image from the cache and displays it in an image view in the scroller.
Everything works fine, but the time it takes from when -setImage: actually gets called to the time it actually shows up visibly on the screen is unbearable--sometimes 10 seconds or more to actually show up.
I have tried it both with and without image resizing which adds almost nothing to the processing time when I do the resizing. As I said, the slowdown is somewhere after I call -setImage on the image view. Is anyone aware of some sort of aspect of the AssetLibrary API that might cause this?
Here's some relevant code:
- (void)setImagesForVisiblePages;
{
for (MomentImageView *page in visiblePages)
{
int index = [page index];
ALAsset *asset = [photos objectAtIndex:index];
UIImage *image = [assetImagesDictionary objectForKey:[self idForAsset:asset]];
// If the image has already been cached, load it into the
// image view. Otherwise, request the image be loaded from disk.
if (image)
{
[[page imageView] setImage:image];
}
else {
[self requestLoadImageForAsset:asset];
[[page imageView] setImage:nil];
}
}
}
This will probably mess up any web searches looking to solve problems with the AssetsLibrary, so for that I apologize. It turns out that the problem wasn't the AssetsLibrary at all, but rather my use of multi-threading. Once the image finished loading, I was posting a notification using the default NSNotificationCenter. It was posting it on the background thread which was then updating (or trying to update, at least) the UIImageView with -setImage. Once I changed it to use -performSelectorOnMainThread and had that selector set the image instead, all was well.
Seems no matter how familiar I get with multi-threading, I still forget the little gotchas from time to time.