Under the Bridge

imageNamed is evil

So a little while back we wrote about the confusing crashes on the device that we eventually figured out were caused by [UIImage imageNamed:] running out of memory when it tried to cache lots of big images. So we apparently solved the problem by making a thumbnail-sized image set and caching those, some 2.9 meg worth, whilst loading in the full sized ones uncached and only as needed.

Well, apparently turned out to be not good enough. See, it’s a few days later and we’ve finalized the design and got everything implemented and all seems to be good … except that it gets terminated without warning sooner or later. And quite often, Springboard terminates as well. Absolutely no correlation to any action or sequence in the program, no leaks, no out of bounds memory accesses, memory usage of the program barely a pittance. Yet, somehow, look at the console and you see system memory warnings scrolling by, you see it quitting background processes, and eventually quitting Springboard and/or the active application. Whilst that active application hums merrily along in blithe ignorance of the system crashing to the ground behind its back.

So apparently not only is 2.9 meg of images cached with +imageNamed enough to bring the iPhone OS to its knees, it’s not smart enough to, you know, actually do anything about it, like oh I don’t know, empty the cache or something?

It’s not like this is hard or anything, you can replicate the caching functionality precisely by declaring yourself an  NSMutableDictionary *thumbnailCache and populating it like

- (UIImage*)thumbnailImage:(NSString*)fileName
{
   UIImage *thumbnail = [thumbnailCache objectForKey:fileName];

   if (nil == thumbnail)
   {
      NSString *thumbnailFile = [NSString stringWithFormat:@"%@/thumbnails/%@.jpg", [[NSBundle mainBundle] resourcePath], fileName];
      thumbnail = [UIImage imageWithContentsOfFile:thumbnailFile];
      [thumbnailCache setObject:thumbnail forKey:fileName];
   }
   return thumbnail;
}

and if you do get a low memory issue, just [thumbnailCache removeAllObjects] and you’re good. But you know what? After replacing the various +imageNamed calls with this … not a single quibble anywhere, the whole 2.9 meg worth of cached thumbnails go right in there and not a single problem for, literally, hours.

(Not that there were any problems after those hours, we hasten to add; simply that, you know, if you think about it, there probably really isn’t a lot of point extending testing of an iPhone application much past the lifespan of a full battery charge…)

So the moral of the story is: DO NOT USE [UIImage imageNamed] for any significant amount of images. It is EVIL. It WILL bring down your application and/or Springboard, even when your application is putting along using just barely a nibble of memory on its own. Take the handful of lines above and implement your own cache!

89
  • Daniel Alexander

    Now, keep in mind that I think that you don’t have to recreate the timer. I think you could just create the timer once and simply put it on the run loop and invalidate it to remove it from the run loop. But I can’t remember so for this example I am just going to release it each time and recreate it.

    So, here is the code to create the timer.

    myThingyTimer = [[NSTimer timerWithTimeInterval:0.01 target:self selector:@selector(myThingyTimerFired:) userInfo:nil repeats:YES] retain];
    [[NSRunLoop currentRunLoop] addTimer:myThingyTimer forMode:NSDefaultRunLoopMode];

    Just remember that you need to match up your timer release with this timer creation. If you repeat this creation code then you will need to have done a release in between.

    Ok, we are almost there. The last thing we need to do is create a drawRect to put the images into our UIView. That might look like this:

    - (void) drawRect: (CGRect) aRect
    {

    CGContextRef viewContext;

    if ((currFrame > 0) && (currFrame <= 185)) {

    viewContext = UIGraphicsGetCurrentContext();
    CGContextTranslateCTM(viewContext, 0, self.bounds.size.height);
    CGContextScaleCTM(viewContext, 1.0, -1.0);
    CGContextDrawImage(viewContext, self.bounds, myThings[currFrame]);
    }
    }

    I think you need to flip the context so that the image doesn't get displayed backwards. So, I put that in there with the translate and scale. I can't remember exactly what that should be, but I think it is right. If your images are showing up backwards or upside down then it is those two commands that is doing it. Maybe comment them out or you could mess around with the values I used to get the image to be displayed properly. I think it is right though. Anyway, this will basically draw the image using the CGImageRef that you have in the myThings array at index currFrame. This function is called each time the timer triggers because you have a [self setNeedsDisplay] in your timer fire function (myThingyTimerFired). Also in the myThingyTimerFired function you increment currFrame so each time it triggers it advances to the next frame (image) in your animation. And that should do it.

    You have to forgive me any typos, and my memory is not perfect so I may have made some mistakes above. I didn't get this from any code so it isn't tested or anything, but I think it should get you a good start.

  • Daniel Alexander

    Ok, that seemed to work. So, just ignore that first coding post I did earlier. It doesn’t make sense with all the lines missing. Maybe it was just too long. Hope that helps.

  • Paul Squyres

    Hey Daniel,

    I really appreciate your help. I am trying your code example with the timer and will let you know how it goes. What you did makes sense, I just didn’t know how to get started with it. I hope this approach will work as I am getting tired and frustrated with it all. Thanks Again!!!

  • Daniel Alexander

    I typed more than I had too. It is not as hard as appears from what I typed. Put simply you need to create a subclass of a UIView. And most everything you need to do will be in that subclass. The iVars you will need will be your array of CGImageRefs, an integer called currFrame that tells drawRect which frame to draw during animation, and a timer to walk you through the animation.

    The timer basically increments currFrame and causes the UIView to redraw itself (which means forces a call to drawRect). drawRect is then called by the system and inside drawRect you draw whichever frame currFrame is pointing too (indexing).

    Thus, each time the timer fires you get a new frame drawn and that is the animation.

    All of the code is pretty much listed out for you in my mess of texts above, but in general it should be pretty simple to get it going….

  • Daniel Alexander

    …get it going yes, but that doesn’t mean it is going to work. All the other stuff I am saying above is to give you some insight into where the pitfalls are. For instance, you need to make sure your images are not too large for your animation duration. If you are trying to animate 300×300 images at over 40 fps then you might run into trouble (particularly if you are also trying to manage user interactions with your animations). This is just one of many problems that you might run into…some of the problems have solutions…some don’t. If your application requires you to do something that the iPhone just can’t do (like, to exaggerate the point, you wanted to animate 300×300 images at 500 fps) then you won’t be able to do it. However, sometimes you can change your requirements a bit (like reduce the number of images you are trying to animate and thus cut your fps down to something more managable). All of the other stuff above is stuff that might help you to work out how you might get around some of these issues.

    What you can know for sure is that you must use imageNamed if you are going to be animating. Short of that you have to figure out ways to get your images and code to balance between imageNamed sillyness and iPhone limitations (etc…). I would also say that you are going to also need to do the animating with a timer like I have above. Those are probably the two things you must do (imageNamed and timer). So, make that your minimum application and try to make it fit in with your data.

    Hope that helps…

  • Abhishek

    @Alex and everybody,

    It seems that the para at the top of this page might solve my problem and I would really appreciate if you could help…
    Here is what I am doing:
    1.I have a game in which a penguin hits an ice cube with a hammer
    2.if u have hit the correct ice cube then an animation for correct answer is played else an animation for wrong answer is played.
    ..
    ..
    and so on !!

    Well here there are 3 animations in a particular order…
    1. the penguin hits the ice cube…
    2. when the hammer touches the ice cube the ice cube breaks…
    3. depending upon the answer given (right or wrong) the penguin gets happy or sad !!

    now i m using an NSTimer and simply changing the images of the UIImageview
    and all this is really slowing down everything and the game is lagging
    ..
    ..
    CAN u tel me how to use the code u mentioned above in a simpler manner…
    LIKE MAY BE
    how to create arrays with that code which involves BUNDLE and CACHE etc…
    ..
    ..

  • Daniel Alexander

    I’m sorry I didn’t notice your message until today…bit late probably. The description of what you are trying to do is just enough to make me unsure whether you need to go with a more complicated approach or whether you can get away with some simpler approach.

    Still, from what you are saying you are changing the .image of a UIImageView inside of your timer handler. The worst thing you can do is this:

    myImageView.image = [UIImage imageNamed: [NSString stringWithFormat:@"d.png", currImageIndex]];

    Where your images are named with a digit followed by “.png” (like 1.png, 2.png, etc). Also the current image that you want to display would be indexed by currImageIndex.

    Now, you are probably not doing this, but that would probably be a big problem (particularly if you are not releasing the image after you use it). Instead, you are probably doing something like this:

    myImageView.image = [UIImage imageNamed: [myImageArray objectAtIndex:currImageIndex];

    Where myImageArray might be an NSMutableArray that you have loaded and stored all your UIImge object in. Since you load your images in you initWithFrame or whereever you only load them once into your myImageArray and the above would work better for you.

    Now, the way I would do it is not to put this stuff in your timer handler at all, rather I would put the painting of the next image in the drawRect and drop the UIImageView altogether.

    In other words, I would create a UIView that loads your myImageArray in the initWithFrame. You would actually have a few different arrays since you have at least three animations (hit ice cube so it cracks, get happy, or get sad), but lets just assume it is one animation for now.

    Ok, so you have a UIView, and you load your animation images on the init (or probably initWithFrame) into myImageArray (which would be your NSMutableArray iVAR). Then in your timer handler you would simply increment through you animation by incrementing the currImageIndex. Something like this:

    currImageIndex ;
    if (currImageIndex > numImagesInAnimation)
    {
    stop the animation timer (invalidate and release it)
    set currImageIndex to whatever you want it to look like at the end
    return;
    }
    [self setNeedsDisplay];

    Then you would create a drawRect function that simply draws the image into the context of the UIView. There are better ways to do this, but to keep it simple you might do it like this:

    [[myImageArray objectAtIndex:currImageIndex] drawInRect:self.bounds];

    Now, I would actually store the Image Context Ref in a C array instead of storing the UIImage in an NSMutableArray and I would then probably draw directly into the UIView graphics context, but the above is simple enough too.

    Most of this stuff I have already gone through above, so I won’t repeat myself (done that enough I think). If you have any specific questions I could try to answer them for you. Hope this helps.

  • Daniel Alexander

    Now, you don’t necessarily even need to be using an NSTimer. You could just use the built in animation methods. I mean, it seems like you don’t have a huge number of images and that they might not be very big images. In such an instance it might be better to just do animation without the NSTimer. Just a thought.

    Oh and sometimes there are some applications where you might use the performSelector method (with an afterDelay). I don’t like this approuch at all. In fact, I think using performSelector with the afterDelay is an easy way to get yourself into trouble, but there are some uses for it. Maybe you could use it to help you do your animation if it is not too many frames. I don’t think it would be of any help, but I thought I would mention it.

  • Daniel Alexander

    Ahhhh man, this board messes up my posts sometimes. I think the percent symbol messes things up. The above has this line:

    myImageView.image = [UIImage imageNamed: [myImageArray objectAtIndex:currImageIndex];

    and it should read:

    myImageView.image = [myImageArray objectAtIndex:currImageIndex];

  • Daniel Alexander

    And I put two plus signs at the end of this line that got removed when I hit submit:

    currImageIndex;

    But it cut them out. So that line should be this instead:

    currImageIndex = currImageIndex 1;

    Its a bummer cause it is selective what it changes…so there are probably all sorts of these mistakes in the code. All I can say is that it was good when I typed it…but that submit button messes it up. Sorry about that.

  • Daniel Alexander

    ahhhh…see…it removed the plus sign there too….bummer….it should be

    currImageIndex = currImageIndex insertSymbolOfPlusSignHere 1;

    Why would it cut out a plus sign….very strange…here are a bunch of plus signs…below

    Should have been 9 plus signs above this line…

  • Daniel Alexander

    Yep…all the plus signs on this entire page have all been removed and I can’t type one now….
    :(

    You will have to use your imagination a bit reading these posts.

  • Pingback: atomton » iPhone Image Cache w/ source code()

  • Abhishek

    @Daniel Alexander (ofCourse)

    Sir,

    First of all I really want to THANK YOU for taking time out and responding to my problems…
    I mean I posted for help but it was so late…i mean all the other posts are like a year old
    ..
    ..
    BUT STILL U RESPONDED TO MY POST AND EVEN WROTE 5 BIG REPLIES TRYING TO HELP ME WITH MY PROBLEM !!
    ..
    ..
    So Sir,
    Thanks a lot !!

  • Abhishek

    Sir,
    I also want to tell you that the Penguin Game for which i asked your help is complete and live on the App Store !!
    ..
    ..
    Please go and just check it out Sir…its named KeyBreaker
    (i want u to see it bcoz u played a very important role for me)
    ..
    ..
    And sir,
    I have a question for you as well…
    I mean i need some more help…

    => As you told, i used imageNamed for this game called KeyBreaker
    BUT NOW i want to make this game for the iPad
    ..
    ..
    And the images folder total size is around 4 mb
    and i m not able to use the imageNamed technique
    ..
    ..
    What i did for the iPod version was that i stored all the images in arrays during the AwakeFromNib function of the project…It worked fine

    BUT if i use the same for iPad then the game crashes during startup loading and gives a message like:
    SIGNAL 0
    and sigbus 10 something
    ..
    ..
    SO HAVE U FIGURED OUT HOW TO STORE IMAGES FOR ANIMATION
    AND ALSO CAN U HELP ME ON THIS PROBLEM OF MINE !!

  • Abhishek

    Sir,
    I just build the ode on my iPad and it game the following messages as soon as it launched:

    warning: Unable to read symbols for “/Developer/Platforms/iPhoneOS.platform/DeviceSupport/3.2 (7B367)/Symbols/System/Library/AccessibilityBundles/AccessibilitySettingsLoader.bundle/AccessibilitySettingsLoader” (file not found).
    Program received signal: “0”.

    The Debugger has exited due to signal 10 (SIGBUS).The Debugger has exited due to signal 10 (SIGBUS).
    (gdb)
    ..
    ..
    ..
    ..
    so can u help me now…

  • Abhishek

    And when i store the images with the following code:

    alphabet_Array = [[NSMutableArray alloc] initWithCapacity:26];
    for (i = 1; i <= 26; i )
    {
    picName = [NSString stringWithFormat:@"Alphabet_%d.png",i];
    [alphabet_Array addObject: [UIImage imageWithContentsOfFile:picName]];
    }
    ..
    ..

    instead of:

    alphabet_Array = [[NSMutableArray alloc] initWithCapacity:26];
    for (i = 1; i <= 26; i )
    {
    picName = [NSString stringWithFormat:@"Alphabet_%d.png",i];
    [alphabet_Array addObject: [UIImage imageNamed:picName]];
    }

    then i get the following messages:

    2010-06-11 11:18:27.826 KeyBreaker[810:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSCFArray insertObject:atIndex:]: attempt to insert nil'
    2010-06-11 11:18:27.837 KeyBreaker[810:207] Stack: (
    852041337,
    861292157,
    852040821,
    852041023,
    850612251,
    850434453,
    850434289,
    13413,
    828201059,
    828199873,
    827153745,
    827153023,
    827464035,
    827462369,
    827460907,
    819342131,
    851590557,
    851588321,
    827151529,
    827144691,
    12195,
    12120
    )
    terminate called after throwing an instance of 'NSException'
    Program received signal: “SIGABRT”.
    (gdb)

    ..
    ..
    but i think this is my mistake…
    Am i using the right way to implement imageWithContentsOfFile

  • Daniel Alexander

    Ok, first when using imageWithContentsOfFile you need to include the resource bundle path. So, you would do this instead:

    alphabet_Array = [[NSMutableArray alloc] initWithCapacity:26];
    for (i = 1; i <= 26; i )
    {
    picName = [NSString stringWithFormat:@"%@/%@", [[NSBundle mainBundle] resourcePath], [NSString stringWithFormat:@"Alphabet_%d.png",i]];
    [alphabet_Array addObject: [UIImage imageWithContentsOfFile:picName]];
    }

    This is a little sloppy, and it might have some typos in it. But it is something like this.

    I've not done too much with the iPad. I have one and I do need to run some of these tests on it, but I haven't messed with this sort of animation for the iPad yet. But I will be doing so in the near future, so maybe I can be of a little help to you. Let me look at what else you said. I just wanted to point out that you need the bundle resource path to reach your images. Otherwise it won't find them and it will return nil giving you the crash.

  • Daniel Alexander

    Hmmmmm….never saw that error (AccessibilitySettingsLoader). I don’t think that that is a problem with the images though. Whenever I see a problem like this (that I’ve never run into) I tend to suspect that it is IB related. I don’t use interface builder at all. I would recommend that people just avoid using it altogether. I think it causes more trouble than its worth, it slows down your development (go figure), and it makes it harder to debug your app. Most importantly it limits the newer developer and narrows their approach. That’s just my opinion though. I am sure that there will be a day in the distant future when Apple gets IB working in a beneficial way, but for now it is not worth using.

    Oh, and you should also turn on NSZombieEnabled if you don’t already have it on. That will give you a bit more debugging information (mainly if it is related to object retaining/releasing). You do this in the following way:

    In XCODE under the “executables” section you can right click on your project executable and choose “Get Info”.

    In the popup click on the Arguments tab.

    In the lower part of the screen under “Variables to be set in the environment” click the to add a new environment variable.

    Type in NSZombieEnabled and set its value to YES. Then check the checkbox to enable it.

    Now when you run you will get a bit more information in your console and your call stack will have more informative information in it.

    This will probably not be of much help for your current problem, but it is a useful tip in general.

    NOTE: YOU MUST REMEMBER TO UNCHECK THIS ENVIRONMENT VARIABLE BEFORE YOU DO YOUR FINAL BUILD FOR SUBMIT TO APPLE. I think they might reject you if you leave it checked (I am not sure, but why take the risk).

  • Daniel Alexander

    I went ahead and bought your App to give you some business. I don’t have any kids so I don’t know that I will find much use for it. But it looks neat.

    I know what it is like to wait for the app to start selling, so maybe I can help out your sales a bit. Hope it is successful for you. You should probably put together a YouTube video of its usage and put the link on your webpage. It is important to avoid negative reviews (and you will get them for the stupidest reasons). A YouTube video is an easy way to make sure that the person buying your app knows exactly what they are buying. That way they are less likely to give you a negative since they knew what they were buying to begin with (though some people will still give you a negative for some silly reason).

    Just to let you know that you should not expect much money from any particular app. Of course, there is the wonderful possibility that an App will go viral and that would definitely be a big payout (I think there are near 100 million devices out there), but you should think instead about the money you can make by developing 20 apps (or some such number). Each may bring in a small amount, but together they can add up to a nice sum. Plus, it can be depressing to get a mere 5-20 sales a day if you are hoping to be selling 100-200 sales a day. However, if you have 20 apps selling 10 (or whatever) a day isn’t too bad. At $1.40 per app (for a $1.99 app) that works out to $102K a year. And that would be residual income. Its good to plan for 20 apps. That way when you get 5-20 sales a day on some app you can get excited about it. Because that’s 5-20 closer to where you want to be.

    That’s about all the advice I have on iPhone development. It boils down to…”Don’t get discouraged by low sales numbers”. If they are 0 then feel bad (You will have at least 1 tomorrow…hee hee). But if they are averaging 5-20 then that isn’t horrible provided you have a plan that involves 20 apps (or whatever). You probably already know this.

  • Daniel Alexander

    Ok, I messed around with your App and found a bug or two.

    1. When you select the third letter/number the penguin animation plays. During that animation a thin green line is visible to the left of the penguin (goes down the full screen nearly). Probably just the frame being a little shifted or maybe the images within the animation are smaller (one pixel). The strange thing is that this doesn’t happen all the time. Since it doesn’t happen all the time that probably means that the images are being transformed (scaled probably). Or, I have noticed that sometimes when an image is placed at partial pixel locations (like 200.45f or something such), the iPhone will sometimes misinterpret that to be 201 or 200 at different times. I have also seen the images get blurry due to placing them out of the integer range (in the float innerspace). I have some ideas about what is happening, but maybe that is part of this problem.

    2. Found a Crash. The app will allow you to click the exit/menu button in the upper right even during animations and the voice will continue to play. So, you can click back and then quickly click “Start” and it will then kill the voice and start saying something different. That all seems to work fine, but I thought it strange so I tried to break the app at that point. Here is how to get it to crash.

    a. Click Start.
    b. Complete the first set of questions (3 questions) to get to the sticker screen.
    c. Now you need to do the following quickly…pick a sticker, quickly click on the “Back” button when it becomes visible, quickly click on the “Start” button when it becomes visible (the voice should still be talking at this point).
    d. So, it should have stopped the previous voice and asked a new question (I think it is the same question, which is probably an important observation since normally it would switch questions).
    e. Click on the right answer and the app will crash (I never tried clicking the wrong answer, but it probably will crash too).

    Other than that it seems to run fine. Though it might be nice for it to remember the score. You could have a “Continue” button and a “Start New” button. Continue will start back off with all the stickers from the previous runs until they are finished. It might be nice to put which stickers you have completed in the app Defaults, but you could write them to file instead.

    Putting them in the Defaults is pretty easy though. Normally you might think twice about saving a lot of stuff into the Defaults, but 30 BOOLs (or even integers) isn’t a huge deal.

    I can’t think of anything else. Looks good.

  • Daniel Alexander

    Just a quick note about the application defaults. You would do something like this:

    In your application delegate you probably already have this method. Just add in the code to save the sticker state. This assumes there are 30 stickers and that you have an integer array that stores whether they are selected or not (probably it would be a BOOL array). In any case this should give you an idea of what you might want to do:

    - (void)applicationWillTerminate:(UIApplication *)application
    {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    if (defaults)
    {
    for (int i=0; i<30; i )
    {
    [defaults setInteger:stickers[i] forKey:[NSString stringWithFormat:@"S_%d",i]];
    }

    [defaults synchronize];
    }
    }

    And then when your app loads (however you are doing this), in your entry point for your app you will just load these flags.

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    for (int i=0; i<30; i )
    {
    stickers[i] = [defaults integerForKey:[NSString stringWithFormat:@"S_%d",i]];
    if (stickers[i] = STICKER_Undefined)
    {
    stickers[i] = STICKER_Incomplete;
    }
    if (stickers[i] = STICKER_Complete)
    {
    continueOK = YES;
    }
    }

    And somewhere you might declare:

    #define STICKER_Undefined 0
    #define STICKER_Incomplete 1
    #define STICKER_Complete 2

    You don't really need the "if (stickers…" thingy. But I tossed it in because integerForKey doesn't have any default return value for when a value does not exist. So, this is one way you can treat a return of 0 as indicating that the value does not exist so that you can then initialize it on the first run. Probably a better way to do this in this instance is to just make 0 be your default value (#define STICKER_Incomplete 0).

    Like I said, you probably will use BOOLs so you will have to change the code a bit for that, but it will look essentially similar. Hope that is useful to you.

    Note: an alternative is to write the array out to a file. Also remember that once this data is loaded you need some way for the user to reset it (thus you should probably have a "Start New" button and a "Continue" button. I have added a flag above that will be set during the loading that will indicate if any stickers are completed (continueOK). If even one sticker is completed then you can show the "Continue" button…otherwise "Continue" would be hidden or grayed out.

    Note, Note: Whenever I post code this forum seems to delete my signs. So, you might have to check that and put them back.

    Hope this helps. Nice app.

  • Daniel Alexander

    I don’t yet have an iPhone 4, but I have done a little testing on them with OS4. I have found that there are some differences in the way that image processing works on the iPhone 4. I have run the tests on older iPhones with OS4, so it isn’t exactly an OS4 issue, but rather it is an OS4 with iPhone 4 issue. So, if you have written apps that do image animation similar to the way we are describing here (whether that be in a album view or in a game) you might find that the animations crawl when you put them on the iPhone 4 with OS4. I have not had enough time on the iPhone 4 to actually figure out what exactly is the trouble. When I get one I will test it out further and let you know. Its just a warning that you should test the iPhone 4′s before they sell to many of them and you start getting negative feedback due to such issues.

    Again, the troubles I have experienced where related to the iPhone 4 with OS4. Older iPhones with OS4 don’t seem to have the same issues (or iPads for that matter…they seem to work fine as well…its just the iPhone 4 with OS4 and it is only in some circumstances which I have not determined yet as I did not have enough time with the iPhone 4 that I borrowed to test it completely). You might not see these problems with what you are doing, but you might see them too…so I thought I would mention it. Test your app on an iPhone 4.

  • http://www.windowsgames.co.uk Sean O’Connor

    Thanks Alex, I think you’ve saved my game (“Tribes”)! It was working fine in older iOSes but ever since about 3.2 on the iPad and 4.0 on the iPhone it started crashing immediately on trying to load all the graphics in the game. I was loading about 1,000 small PNGs with imageNamed at the start and that seems to crash the device (but not the simulator).

    It still does crash my iPod Touch when I’ve loaded about 400 PNGs using your imageWithContentsOfFile method but the iPad seems to be immune. What I’ve done is added:

    if ([imageCache count] == 350)
    {
    [imageCache removeAllObjects];
    }

    just before it checks if the filename is in the dictionary. I cut down on a lot of the images too so it only rarely hits the 350 limit now.

    It’s a shame I’ve had to do that though as I’ve had to lose some animation frames and I don’t see why older OSes had no problems.

  • Daniel Alexander

    Its important to make note that “small png” can mean two different things. It could mean that the file sizes are small 4kb, 10kb, 90kb? But those file sizes have little effect on the problem you are running into (though there might be some indirect relationship, such can’t be relied on). What is important is the decoded image size. You can encode an image into a small file size by doing all sorts of tricks like using a smaller color pallet (web colors, etc). So, looking at the file sizes of the png’s is not terrible helpful. It is probably more helpful to consider the actual dimensions of your images. Like if you have a 38kb png file that is a 300×300 image then the decoded image size will be a lot more (I’ll guess around 300KB uncompressed). It is the decoded images that need to fit into memory and it is the decoded images that will dictate how fast you can animate.

    Mind you there is still good reason to get your png file sizes down. There is a limit to the app size that can be downloaded through ATT’s network. I have found that people want to buy apps when they are sitting outside with friends or some such. That means that if a user has a choice between waiting until they get home to buy your 11mb app and someone else’s 9mb app that they can download emmediatly they will by the 9mb app (I think the limit is still 10mb, but they may have changed it to 20mb recently…but I don’t think so…I think it is still 10mb). So, smaller png files sizes can be helpful in that regard (there are lots of tricks you can do to bring that down considerably). But those sizes don’t matter when you are trying to animate. Actually, they can hurt your animation speeds because there is more work decoding them.

  • Daniel Alexander

    Ok, so here is the issue as I can see. The issue I mentioned earlier (with the iPhone 4) is actually an issue with OS4 (though it is a bit more problematic with iPhone 4). Apparently the OS4 does not draw into a Rect, UIView, or into a device context as efficiently as the older OS’s did (OS3.2-). However, Apple did put effort into increasing the efficiency of Layers. On the older OS’s using Layers actually slowed things down for these types of image animations so drawing onto the device context was fastest. However, with OS4 it is the other way around. Using a sublayer contents to display images for this type of animation is now faster than drawing onto the context (which is now much slower).

    Sooooooo, if your animations all of a sudden slow down to a crawl once you upgrade to OS4 then this is why that is happening. You need to switch your animations over to using a sublayer and updating its contents.

  • Daniel Alexander

    By the way, you might still want your app to support older iPhones (like the iPhone1, etc). Those iPhones can’t even run OS4. You can use UIDevice to get the systemVersion. If the systemVersion floatVal is greater than or equal to 4.0 then you could use Layers. Otherwise, you would just keep doing what you used to do (drawing into a Rect most likely…or onto a device context of a UIView…or whatever was working with the older iOS). So, just create a simply iVar bool called useLayers that you set up in your init (using UIDevice). Also, add a subLayer (or multiple subLayers depending on how many animations you want to be doing) to your UIViews layer so you can update its contents for iOS4 iPhones. Then in your drawRect drop an “if” statement to determine which way you want to draw your images (into Layers or the old way).

    Remember this is only if you run into trouble with your animations. If the images are small enough you might not see any substantial slowing and then you can just keep doing what you are doing. But if you upgrade to OS4 and your animations start to crawl then this is how you can fix it.

  • Daniel Alexander

    Just one more note. Although this is an issue with iOS4 on any iPhone, the problem is more dramatic using the new iPhone4. Also, note that when I draw into the device context of an older iOS iPhone it is still faster than if I use layers on the same iPhone using iOS4. So, it would be nice to still get that speed if I am using iOS with an older iPhone. And the key is that sometimes you can. It depends on the sizes and number of images that you are using.

    So, for one of my Apps instead of using UIDevice to determine whether to use Layers or not use Layers depending on the iOS version number. I decided I would get the actual device model using sysctlbyname. Although UIDevice will also give you the model it does not distiguish between actual models of the iPhone. Instead it just tells you it is an iPhone. sysctlbyname will give you the additional information you need to distinguish between the different types of iPhones.

    Here is my set of Defines for what sysctlbyname returns (It returns a string…I don’t know if this forum is going to post this properly, but I will try anyway…all you really care about is the string literals):

    #define DIDRST_Unknown @"" 
    #define DIDRST_iPhoneSimulator @"i386"
    #define DIDRST_iPhone1G @"iPhone1,1"
    #define DIDRST_iPhone3G @"iPhone1,2"
    #define DIDRST_iPhone3GS @"iPhone2,1"
    #define DIDRST_iPhone4 @"iPhone3,1"
    #define DIDRST_iPad1 @"iPad,1"
    #define DIDRST_iPodTouch1G @"iPod1,1"
    #define DIDRST_iPodTouch2G @"iPod2,1"
    

    So, now you can determine which devices you want to support with Layers and which you want to support with the older approach. For instance, I wanted all the older devices to NOT use layers even though some of them may have been using iOS4. This was only an issue for one of my apps. On other apps I wanted any device using iOS4 to use Layers (in which case I used UIDevice instead of sysctlbyname).

    Sadly I know someone out there in Internet World there is someone that will need this post (I sure was, but couldn’t find anything). Hope they find it here cause this thread is the extent of my helpfulness. :)

  • Alex

    1,000,000 THANKS !!!! Just that! 1,000,000 THANKS !!!! You saved me from going crazy with the stupid animation!

    The only problem I have is that the animation goes really fast now. I put a wait() but it doesn’t seem to work. I use a CATransition to switch between two views. Do you know if there is a way to make the animation run slower? I tried the CAtransition.duration but it doesn’t work the right way.

    Again a 1,000,000 THANKS !!!!

  • http://ajfek.pl Paweł Kata

    Thanks for the article! I paired it with some NSCache mumbo-jumbo and my app finally manages memory properly! :D

  • ClamB

    Many many thanks for this posting.
    We just finished a simple slideshow app with 100 200k jpegs, and it failed without fail. After reading this posting I used the following simple code to load my images, and it now works without fail;

    NSString *fromName = [NSString stringWithFormat:@"image%d", i];
    NSString *fromPhotoName = [[NSBundle mainBundle] pathForResource:fromName ofType:@”jpg”];
    UIImage *fromPhoto = [UIImage imageWithContentsOfFile:fromPhotoName];

    Thanks again,
    CPL

  • Pingback: imageNamed is evil « Under The Bridge « Brainwash Inc. – iPhone/Mobile Development()

  • Pingback: imageNamed is evil « Under The Bridge « Brainwash Inc. – iPhone/Mobile Development()

  • http://www.facebook.com/people/Klaus-Busse/1285419642 Klaus Busse

    There’s a single case where imageNamed has it’s merits: If you have small files, that you use over and over, typically ressources. For everything else, DONT.

  • Jesse Compo

    Have you looked into NSCache?

  • http://depth-first.com Rich Apodaca

    Saw exactly the same thing. This article was a lifesaver – thanks!

  • Sam

    This is absolutely correct. There are some articles that suggest that Apple knows what to do when the imageNamed: cache takes up too much memory but this is NOT correct. I just seriously debugged an app that loads large images and using imageNamed: consistently caused it to crash after several memory warnings which the app cannot respond to because Apple owns the imageNamed: cache. I will mention that initWithContentsOfFile is slower, so I found it very helpful to call it on the background thread otherwise you can trip up the UI.

  • Mohan Chaudhari

    t depends on what you’re doing with the image. The imageNamed: method does cache the image, but in many cases that’s going to help with memory use. For example, if you load an image 10 times to display along with some text in a table view, UIImage will only keep a single representation of that image in memory instead of allocating 10 separate objects. On the other hand, if you have a very large image and you’re not re-using it, you might want to load the image from a data object to make sure it’s removed from memory when you’re done.

  • Mohan Chaudhari

    It depends on what you’re doing with the image. The imageNamed: method does cache the image, but in many cases that’s going to help with memory use. For example, if you load an image 10 times to display along with some text in a table view, UIImage will only keep a single representation of that image in memory instead of allocating 10 separate objects. On the other hand, if you have a very large image and you’re not re-using it, you might want to load the image from a data object to make sure it’s removed from memory when you’re done.