UIImage Animation Causing App to Crash / Memory Leaks - iphone

I'm using a UIImage animation and it is causing numerous memory leaks and crashes for different users using the application.
Below is my code. I am preloading the set of two animation in viewDidAppear
pointsView.image = [UIImage imageNamed:#"C72.png"];
NSMutableArray *menuanimationImages = [[NSMutableArray alloc] initWithCapacity:21];
NSString *imageName;
for( int aniCount = 0; aniCount < 72; aniCount++ )
{
imageName = [NSString stringWithFormat:#"C%d.png", aniCount];
[menuanimationImages addObject:[UIImage imageNamed:imageName]];
}
pointsView.animationImages = menuanimationImages;
pointsView2.image = [UIImage imageNamed:#"I72.png"];
NSMutableArray *menuanimationImagess = [[NSMutableArray alloc] initWithCapacity:21];
NSString *imageNames;
for( int aniCounts = 0; aniCounts < 72; aniCounts++ )
{
imageNames = [NSString stringWithFormat:#"I%d.png", aniCounts];
[menuanimationImagess addObject:[UIImage imageNamed:imageNames]];
}
pointsView2.animationImages = menuanimationImagess;
}
I am then running the animation using
pointsView.animationDuration = 3.11;
pointsView.animationRepeatCount = 1;
[pointsView startAnimating];
Any suggestions?

Please read my blog post about this subject: video-and-memory-usage-on-ios-devices. The root of the problem is that you simply cannot have this many images loaded into main memory at the same time. You need to simply not use the UIImageView.animationImages API, it is badly broken and lures developers into writing bad code that will crash when run on the device.

You are loading it looks like 72 png images into memory at once? And depending on the size of those images, you could probably be reaching the memory limits of some older devices causing them to give a memory warning and eventually crash. My suggestion is to not do a 72 image animation. You could try to compress each image which will lower their quality and memory size but loading 72 images to do an animation is just not good in the first place.

Related

Animations Array Loads Slowly. How can I speed the process up?

we're loading about 30 full 320 x 480 images into an animations array. These images are very small in size at about 5 - 7Kb per images. The problem is when we run the loop to populate an array for the images, it seems to pause the entire app while this array get's populated. Is there perhaps a way to load this mutable array with the images in a separate thread or something to allow the array to populate without hurting performance? Thanks
NSMutableArray *totalAnimationImages = [[NSMutableArray alloc] initWithCapacity:numOfImages];
for(int i = 1; i < numOfImages; i++) {
[totalAnimationImages addObject:[UIImage imageNamed:
[NSString stringWithFormat:#"%#%d.png", image, i]]];
}
annImage.animationImages = totalAnimationImages;
annImage.animationDuration = speed; //.4 second
annImage.animationRepeatCount = 1; //infinite loop
[annImage startAnimating]; //start the animation
[totalAnimationImages release];
In working with this issue, and using the threading suggestion provided by shabzco, I've come up with the below code to process a loop, changing the image in an image view, with what seem to be absolutely no performance hit. This only holds one image at a time instead of 30 in an NSMutableArray. Just as Richard J. Ross III said, as soon as the animation would start it would be processing all of the images "into a in-memory bitmap format". This is where the long wait / lag came from. In the below code this is only happening once per image update instead of 30 just to start the animation. I use the while loop with timer in the suggested dispatch thread to keep this timer from effecting the main thread thus potentially creating jerky performance during animations. Please provide feedback on this solution! Thanks
UPDATE: Shabzco suggested that I replace the "ImageNamed" with "imageWithContentsOfFile" to avoid strange memory leaks due to ImageNamed potentially caching images in memory. This was discovered after running xcode profiler and watching the "Real Memory" counter. It became evident that every time an animation ran, the real memory would increase more and more until ultimately the app would crash. After replacing "ImageNamed" with "imageWithContentsOfFile" the issue went away. I've updated the below code.
annImage.image = [UIImage imageNamed:#""];
[annImage setAlpha:1.0];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
NSTimeInterval timeout = 1.00 / speed; // Number of seconds before giving up
NSTimeInterval idle = 1.00 / 8; // Number of seconds to pause within loop
BOOL timedOut = NO;
NSDate *timeoutDate;
for(int i = 1; i < numOfImages; i++) {
timeoutDate = [[NSDate alloc] initWithTimeIntervalSinceNow:timeout];
timedOut = NO;
while (!timedOut)
{
NSDate *tick = [[NSDate alloc] initWithTimeIntervalSinceNow:idle];
[[NSRunLoop currentRunLoop] runUntilDate:tick];
timedOut = ([tick compare:timeoutDate] == NSOrderedDescending);
[tick release];
}
dispatch_async(dispatch_get_main_queue(), ^{
//annImage.image = [UIImage imageNamed:
// [NSString stringWithFormat:#"%#%d.png", image, i]];
NSString *filePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"%#%d", image, i] ofType:#"png"];
annImage.image = [UIImage imageWithContentsOfFile:filePath];
});
}
dispatch_async(dispatch_get_main_queue(), ^{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration: 0.3];
[annImage setAlpha:0.0];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
[UIView commitAnimations];
});
});
This will make your for loop happen in a different thread and will start animating once everything has been loaded.
#autoreleasepool {
NSMutableArray *totalAnimationImages = [[NSMutableArray alloc] initWithCapacity:numOfImages];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
for(int i = 1; i < numOfImages; i++) {
[totalAnimationImages addObject:[UIImage imageNamed:
[NSString stringWithFormat:#"%#%d.png", image, i]]];
}
annImage.animationImages = totalAnimationImages;
annImage.animationDuration = speed; //.4 second
annImage.animationRepeatCount = 1; //infinite loop
[annImage startAnimating]; //start the animation
dispatch_async(dispatch_get_main_queue(), ^{
[totalAnimationImages release];
});
});
}

Quit app while for-loop is running

In my app I have a tableView filled with contents from a server. To download these contents I use NSURLConnection and I create a NSMutableArray (tableItems) to hold and to manage the addresses to the images I want to use.
In connectionDidFinishLoading, after populating tableItems, there is this for-loop:
for (int i = 0; i < [tableItems count]; i++) {
// HERE I CHECK IF THE IMAGE I'M LOOKING FOR IS ALREADY ON DISK
NSString *pathToCheckImage = [NSString stringWithFormat:#"%#/%#.png", pathToPreviews, [tableItems objectAtIndex:i]];
NSData *dataOfCheckImage = [NSData dataWithContentsOfFile:pathToCheckImage];
UIImage *checkImage = [UIImage imageWithData:dataOfCheckImage];
NSString *pathToImage;
UIImage *image;
if (checkImage != nil) {
// THE IMAGE IS ALREADY ON DISK SO I GET IT FROM TEMP FOLDER
image = checkImage;
} else {
// THE IMAGE IS NEW SO I HAVE TO GET THE IMAGE FROM THE SERVER
pathToImage = [NSString stringWithFormat:#"http://www.SERVER.com/SERVER_PATH/%#.png", [tableItems objectAtIndex:i]];
NSURL *url = [NSURL URLWithString:pathToImage];
NSData *data = [NSData dataWithContentsOfURL:url];
image = [UIImage imageWithData:data];
// AND SAVE IT ON DISK
NSString *path = [NSString stringWithFormat:#"%#/%#.png", pathToPreviews, [tableItems objectAtIndex:i]];
[self cacheImageOnDisk:image withPath:path];
}
if (image != nil) {
[arrayOfImages addObject:image];
}
}
This code is working, even if, depending on the number and size of the images I have to download from the server, it can take 1 or 2 minutes to perform its task.
The problem is that if the user quits (home button pushed) while this for-loop is running it keeps on performing its task until the end, even if it needs 1 minute to finish it.
During this period, launching the app again inevitably ends up crashing on startup.
I've tried to stop this for-loop on exit but applicationDidEnterBackground, applicationWillResignActive and applicationWillTerminate are not called until for-loop ends its task.
I've tried to set "application does not run in background", but nothing changed.
Any suggestion will be really appreciated.
You should not be downloading images on the main thread. The app won't be responsive. Grand Central Dispatch is an easy method to accomplish these tasks.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSUInteger count = [tableItems count];
for (int i = 0; i < count; i++) {
// TODO: check if app didResignActive and break
UIImage *image = ... // get image from disk or network
// Add the image to the array on the main thread to avoid any threading issues
dispatch_sync(dispatch_get_main_queue(), ^{
[arrayOfImages addObject:image];
});
}
});
You need to either process your loop off the main thread (very easy to get wrong), or break your work into chunks (much simpler). Try extracting your loop into a separate method that runs it 10 times and then schedules another run with [... performSelector:#selector(myMethod) afterDelay:0]; That will give the runloop a chance to cycle and process events (like quit) every 10 iterations.
Threading (whether via older ways or the newer dispatch_async) will still get you better responsiveness though, so if you want to do that, my recommendation is this:
Share NO data between the background thread and the main thread while it's working.
Spawn your background thread, do your work entirely with local state not accessed anywhere else, then dispatch back to the main thread with your completed array. Any shared state is very likely to make your life very hard.

Preload an additional image using Apple's PhotoScroller example

I am trying to modify Apple's PhotoScroller example and I encountered a problem that I couldn't solve.
Basically, the PhotoScroller originally loaded a bunch of image in an array locally. I try to modify this and change to it request an image file dynamically from an URL. Once the user scroll to the next page, it will fetch the next image from a new URL.
In order to improve the performance, I wanted to preload the next page so user doesn't need to wait for the image being downloaded while scrolling to the next page. Once the next page is on current page, the page after that will be loaded and so on...
I'm not quite sure how I can achieve this and I hope someone can show me what to do.
Here is my custom code: (Please refer the full code from Apple's PhotoScroller example)
tilePage method: (It will be call at the beginning and every time when user did scroll the scrollView)
- (void)tilePages
{
// Calculate which pages are visible
CGRect visibleBounds = pagingScrollView.bounds;
int firstNeededPageIndex = floorf(CGRectGetMinX(visibleBounds) / CGRectGetWidth(visibleBounds));
int lastNeededPageIndex = floorf((CGRectGetMaxX(visibleBounds)-1) / CGRectGetWidth(visibleBounds));
firstNeededPageIndex = MAX(firstNeededPageIndex, 0);
lastNeededPageIndex = MIN(lastNeededPageIndex, [self imageCount] - 1);
// Recycle no-longer-visible pages
for (ImageScrollView *page in visiblePages) {
if (page.index < firstNeededPageIndex || page.index > lastNeededPageIndex) {
[recycledPages addObject:page];
[page removeFromSuperview];
}
}
[visiblePages minusSet:recycledPages];
// add missing pages
for (int index = firstNeededPageIndex; index <= lastNeededPageIndex; index++) {
if (![self isDisplayingPageForIndex:index]) {
ImageScrollView *page = [self dequeueRecycledPage];
//ImageScrollView *nextpage = [self dequeueRecycledPage];
if (page == nil) {
page = [[[ImageScrollView alloc] init] autorelease];
}
[self configurePage:page forIndex:index];
[pagingScrollView addSubview:page];
[visiblePages addObject:page];
}
}
}
To configure page index and content:
- (void)configurePage:(ImageScrollView *)page forIndex:(NSUInteger)index
{
//set page index
page.index = index;
//set page frame
page.frame = [self frameForPageAtIndex:index];
//Actual method to call image to display
[page displayImage:[self imageAtIndex:index]];
NSLog(#"index: %i", index);
}
To fetch image from URL:
- (UIImage *)imageAtIndex:(NSUInteger)index {
NSString *string1 = [NSString stringWithString:#"http://abc.com/00"];
NSString *string2 = [NSString stringWithFormat:#"%d",index+1];
NSString *string3 = [NSString stringWithString:#".jpg"];
NSString *finalString = [NSString stringWithFormat:#"%#%#%#",string1,string2,string3];
NSLog(#"final string is: %#", finalString);
NSURL *imgURL = [NSURL URLWithString: finalString];
NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
return [UIImage imageWithData:imgData];
}
Thanks for helping!
Lawrence
You can use concurrent operations to fetch images one after another. Using NSOperations, you can set a dependency chain so images are loaded in a serial fashion in the background, or you can have them all downloaded concurrently.
The problem with large images is that even though you save them in the file system, there is a "startup" time to get the images rendered. Also, in PhotoScroller, Apple "cheats" by having all the images pre tiled for each level of detail. Apple provides no way to render just a Plain ole JPEG in PhotoScroller, so unless you can pre tile it will be of no use to you.
If you are curious, you can see how to both download multiple images and pre tile them into a temporary file by perusing the PhotoScrollerNetwork project on github.

Ipad animating images crashing issue

I have an application in which I have taken imageview as a background and in that imageview I am using 89 images to make an animation.Here's my code to do the animation
-(void)viewWillAppear:(BOOL)animated
{
NSString *cachesDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSLog(#"cachesDirectoryPath: %#", cachesDirectoryPath);
CPIAppDelegate *obj=(CPIAppDelegate *)[[UIApplication sharedApplication]delegate];
arrayOfImages=[[NSMutableArray alloc]init];
viewMenu.hidden = obj.buttonStatus;
for (int i=0; i<IMAGE_ANIMATION_COUNT; i++) {
// [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/streamvideo351_frame%d.jpg",cachesDirectoryPath,i]];
[arrayOfImages addObject:[UIImage imageNamed:[NSString stringWithFormat:#"streamvideo351_frame%d.jpg",i]]];
}
BackGrdImage.animationImages=arrayOfImages;
BackGrdImage.animationDuration =5.0;
BackGrdImage.animationRepeatCount =3;
[BackGrdImage startAnimating];
[arrayOfImages removeAllObjects];
}
and in dealloc method I am using
[imageAnimations release];
[BackGrdImage removeFromSuperview];
[BackGrdImage release];
it works fine on simulator But crashes on Ipad.What actually happens in ipad is sometimes it gets blink and some time time it disappear.So please help me out with this friends.I am also releasing the array on -(void)viewWillDisappear So please friends help me out with it Any help or suggestion will be appreciated.
Your app crashing because BackGrdImage animating only one time after that you releasing the array
[arrayOfImages removeAllObjects];
and
BackGrdImage.animationRepeatCount =3;
due to this your image goes animating second time but BackGrdImage does not get arrayOfImages for animation therefore your app going to crash.
There is one way when your image animation three time means in 1.5 sec then after you have to call one method for releasing your arrayOfImages.
The above process sure will work out.
May be you are getting memory warning. Because I don't think that device will support images array of 89 images/imageviews. Try debuggig your app on device.

Optimized Image Loading in a UIScrollView

I have a UIScrollView that has a set of images loaded side-by-side inside it. You can see an example of my app here: http://www.42restaurants.com. My problem comes in with memory usage. I want to lazy load the images as they are about to appear on the screen and unload images that aren't on screen. As you can see in the code I work out at a minimum which image I need to load and then assign the loading portion to an NSOperation and place it on an NSOperationQueue. Everything works great apart from a jerky scrolling experience.
I don't know if anyone has any ideas as to how I can make this even more optimized, so that the loading time of each image is minimized or so that the scrolling is less jerky.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[self manageThumbs];
}
- (void) manageThumbs{
int centerIndex = [self centerThumbIndex];
if(lastCenterIndex == centerIndex){
return;
}
if(centerIndex >= totalThumbs){
return;
}
NSRange unloadRange;
NSRange loadRange;
int totalChange = lastCenterIndex - centerIndex;
if(totalChange > 0){ //scrolling backwards
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex - 5;
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex + 6;
}else if(totalChange < 0){ //scrolling forwards
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex - 6;
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex + 5;
}
[self unloadImages:unloadRange];
[self loadImages:loadRange];
lastCenterIndex = centerIndex;
return;
}
- (void) unloadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(thumbView.loaded){
UnloadImageOperation *unloadOperation = [[UnloadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:unloadOperation];
[unloadOperation release];
}
}
}
}
- (void) loadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(!thumbView.loaded){
LoadImageOperation *loadOperation = [[LoadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:loadOperation];
[loadOperation release];
}
}
}
}
EDIT:
Thanks for the really great responses. Here is my NSOperation code and ThumbnailView code. I tried a couple of things over the weekend but I only managed to improve performance by suspending the operation queue during scrolling and resuming it when scrolling is finished.
Here are my code snippets:
//In the init method
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:4];
//In the thumbnail view the loadImage and unloadImage methods
- (void) loadImage{
if(!loaded){
NSString *filename = [NSString stringWithFormat:#"%03d-cover-front", recipe.identifier, recipe.identifier];
NSString *directory = [NSString stringWithFormat:#"RestaurantContent/%03d", recipe.identifier];
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:#"png" inDirectory:directory];
UIImage *image = [UIImage imageWithContentsOfFile:path];
imageView = [[ImageView alloc] initWithImage:image andFrame:CGRectMake(0.0f, 0.0f, 176.0f, 262.0f)];
[self addSubview:imageView];
[self sendSubviewToBack:imageView];
[imageView release];
loaded = YES;
}
}
- (void) unloadImage{
if(loaded){
[imageView removeFromSuperview];
imageView = nil;
loaded = NO;
}
}
Then my load and unload operations:
- (id) initWithOperableImage:(id<OperableImage>) anOperableImage{
self = [super init];
if (self != nil) {
self.image = anOperableImage;
}
return self;
}
//This is the main method in the load image operation
- (void)main {
[image loadImage];
}
//This is the main method in the unload image operation
- (void)main {
[image unloadImage];
}
I'm a little puzzled by the "jerky" scrolling. Since NSOperationQueue runs operations on separate thread(s) I'd have expected at worst you might see empty UIImageViews showing up on the screen.
First and foremost I'd be looking for things that are impacting the processor significantly as NSOperation alone should not interfere with the main thread. Secondly I'd be looking for details surrounding the NSOperation setup and execution that might be causing locking and syncing issues which could interrupt the main thread and therefore impact scrolling.
A few items to consider:
Try loading your ThumbnailView's with a single image at the start and disabling the NSOperation queuing (just skip everything following the "if loaded" check. This will give you an immediate idea whether the NSOperation code is impacting performance.
Keep in mind that -scrollViewDidScroll: can occur many times during the course of a single scroll action. Depending on how for the scroll moves and how your -centerThumbIndex is implemented you might be attempting to queue the same actions multiple times. If you've accounted for this in your -initWithOperableImage or -loaded then its possible you code here is causing sync/lock issues (see 3 below). You should track whether an NSOperation has been initiated using an "atomic" property on the ThumbnailView instance. Prevent queuing another operation if that property is set and only unset that property (along with loaded) at the end of the NSOperation processes.
Since NSOperationQueue operates in its own thread(s) make sure that none of your code executing within the NSOperation is syncing or locking to the main thread. This would eliminate all of the advantages of using the NSOperationQueue.
Make sure your "unload" operation has a lower priority than your "load" operation, since the priority is the user experience first, memory conservation second.
Make sure you keep enough thumbnails for at least a page or two forward and back so that if NSOperationQueue falls behind, you have a high margin of error before blank thumbnails become visible.
Make sure your load operation is only loading a "pre-scaled" thumbnail and not loading a full size image and rescaling or processing. This would be a lot of extra overhead in the middle of a scrolling action. Go even further and make sure you've converted them to PNG16 without an alpha channel. This will give at least a (4:1) reduction in size with hopefully no detectable change in the visual image. Also consider using PVRTC format images which will take the size down even further (8:1 reduction). This will greatly reduced the time it takes to read the images from "disk".
I apologize if any of this doesn't make sense. I don't see any issues with the code you've posted and problems are more likely to be occurring in your NSOperation or ThumbnailView class implementations. Without reviewing that code, I may not be describing the conditions effectively.
I would recommend posting your NSOperation code for loading and unloading and at least enough of the ThumbnailView to understand how it interacts with the NSOperation instances.
Hope this helped to some degree,
Barney
One option, although less visually pleasing, is to only load images when the scrolling stops.
Set a flag to disable image loading in:
-scrollViewWillBeginDragging:
Re-enable loading images when the scrolling stops using the:
-scrollViewDidEndDragging:willDecelerate:
UIScrollViewDelegate method. When the willDecelerate: parameter is NO, the movement has stopped.
the problem is here:
UIImage *image = [UIImage imageWithContentsOfFile:path];
It seems that threaded or not when you load a file from disk (which maybe that happens on the main thread regardless, I'm not totally sure) everything stalls. You normally don't see this in other situations because you don't have such a large area moving if any at all.
While researching this problem, I found two more resources that may be of interest:
Check out the iPhone sample project "PageControl": http://developer.apple.com/iphone/library/samplecode/PageControl/index.html
It lazy loads view controllers in a UIScrollView.
and -
Check out the cocoa touch lib: http://github.com/facebook/three20 which has a 'TTPhotoViewController' class that lazy loads photos/thumbnails from web/disk.
Set shouldRasterize = YES for the sub content view adde to the scrollview. It is seen to remove the jerky behavior of custom created scroll view like a charm. :)
Also do some profiling using the instruments in the Xcode. Go through the tutorials created for profiling by Ray Wenderlich it helped me a lot.