Monday, June 4, 2012

Dispelling the UIImage imageNamed: FUD


I see a lot of people saying imageNamed is bad but equal numbers of people saying the performance is good - especially when rendering UITableView s. See this SO question for example or this article on iPhoneDeveloperTips.com



UIImage 's imageNamed method used to leak so it was best avoided but has been fixed in recent releases. I'd like to understand the caching algorithm better in order to make a reasoned decision about where I can trust the system to cache my images and where I need to go the extra mile and do it myself. My current basic understanding is that it's a simple NSMutableDictionary of UIImages referenced by filename. It gets bigger and when memory runs out it gets a lot smaller.



For example, does anyone know for sure that the image cache behind imageNamed does not respond to didReceiveMemoryWarning ? It seems unlikely that Apple would not do this.



If you have any insight into the caching algorithm, please post it here.


Source: Tips4all

2 comments:

  1. The sister thread on the Apple Dev Forums received some better traffic. Specifically Rincewind added some authority.


    There are issues in iPhone OS 2.x where the imageNamed: cache would not be cleared, even after a memory warning. At the same time +imageNamed: has gotten a lot of use not for the cache, but for the convenience, which has probably magnified the problem more than it should have been.


    whilst warning that


    On the speed front, there is a general misunderstanding of what is going on. The biggest thing that +imageNamed: does is decode the image data from the source file, which almost always significantly inflates the data size (for example, a screen sized PNG file might consume a few dozen KBs when compressed, but consumes over half a MB decompressed - width * height * 4). By contrast +imageWithContentsOfFile: will decompress that image everytime the image data is needed. As you can imagine, if you only need the image data once, you've won nothing here, except to have a cached version of the image hanging around, and likely for longer than you need it. However, if you do have a large image that you need to redraw often, then there are alternatives, although the one I would recommend primarily is to avoid redrawing that large image :).

    With respect to the general behavior of the cache, it does cache based on filename (so two instances of +imageNamed: with the same name should result in references to the same cached data) and the cache will grow dynamically as you request more images via +imageNamed:. On iPhone OS 2.x a bug prevents the cache from being shrunk when a memory warning is received.


    and


    My understanding is that the +imageNamed: cache should respect memory warnings on iPhone OS 3.0. Test it when you get a chance and report bugs if you find that this is not the case.


    So, there you have it. imageNamed: will not smash your windows or murder your children. It's pretty simple but it is an optimisation tool. Sadly it is badly named and there is no equivaluent that is as easy to use - hence people overuse it and get upset when it simply does its job

    I added a category to UIImage to fix that:

    // header omitted
    // Before you waste time editing this, please remember that a semi colon at the end of a method definition is valid and a matter of style.
    + (UIImage*)imageFromMainBundleFile:(NSString*)aFileName; {
    NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
    return [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@/%@", bundlePath,aFileName]];
    }


    Rincewind also included some example code to build your own optimised version. I can't see it is worth the maintentace but here it is for completeness.

    CGImageRef originalImage = uiImage.CGImage;
    CFDataRef imageData = CGDataProviderCopyData(
    CGImageGetDataProvider(originalImage));
    CGDataProviderRef imageDataProvider = CGDataProviderCreateWithCFData(imageData);
    CFRelease(imageData);
    CGImageRef image = CGImageCreate(
    CGImageGetWidth(originalImage),
    CGImageGetHeight(originalImage),
    CGImageGetBitsPerComponent(originalImage),
    CGImageGetBitsPerPixel(originalImage),
    CGImageGetBytesPerRow(originalImage),
    CGImageGetColorSpace(originalImage),
    CGImageGetBitmapInfo(originalImage),
    imageDataProvider,
    CGImageGetDecode(originalImage),
    CGImageGetShouldInterpolate(originalImage),
    CGImageGetRenderingIntent(originalImage));
    CGDataProviderRelease(imageDataProvider);
    UIImage *decompressedImage = [UIImage imageWithCGImage:image];
    CGImageRelease(image);


    The trade off with this code is that the decoded image uses more memory but rendering is faster.

    ReplyDelete
  2. In my experience, the image cache created by imageNamed does not respond to memory warnings. I've had two applications that were as lean as I could get them as far as mem management, but were still inexplicably crashing due to lack of mem. When I stopped using imageNamed to load the images, both applications became dramatically more stable.

    I will admit that both applications loaded somewhat large images, but nothing that would be totally out of the ordinary. In the first application, I just skipped caching altogether because it was unlikely a user would come back to the same image twice. In the second, I built a really simple caching class doing just what you mentioned - keeping UIImages in an NSMutableDictionary and then flushing its contents if I received a memory warning. If imageNamed: were to cache like that, then I shouldn't have seen any performance upgrade. All of this was running on 2.2 - I don't know if there's any 3.0 implications on this.

    You can find my other question around this issue from my first app here:
    StackOverflow question about UIImage cacheing

    One other note - InterfaceBuilder uses imageNamed under the covers. Something to keep in mind if you do run into this problem.

    ReplyDelete