ponraj
ponraj

Reputation: 768

iOS UIImageView memory not getting deallocated on ARC

I want to animate an image view in circular path and on click of image that image view need to change the new image. My problem is the images i allocated to the image view is not deallocated. And app receives memory warning and crashed. I surfed and tried lot of solutions for this problem but no use. In my case i need to create all ui components from Objective c class. Here am posting the code for creating image view and animation.

@autoreleasepool {
    for(int i= 0 ; i < categories.count; i++)
    {
        NSString *categoryImage = [NSString stringWithFormat:@"%@_%ld.png",[categories objectAtIndex:i],(long)rating];
        if (paginationClicked) {
            if([selectedCategories containsObject:[categories objectAtIndex:i]]){
                categoryImage = [NSString stringWithFormat:@"sel_%@",categoryImage];
            }
        }
        UIImageView *imageView = [[UIImageView alloc] init];

        imageView.image = [self.mySprites objectForKey:categoryImage];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        imageView.clipsToBounds = NO;
        [imageView sizeToFit];
        imageView.accessibilityHint = [categories objectAtIndex:i];
//        imageView.frame = CGRectMake(location.x+sin(M_PI/2.5)*(self.view.frame.size.width*1.5),location.y+cos(M_PI/2.5)*(self.view.frame.size.width*1.5) , 150, 150);
        imageView.userInteractionEnabled = YES;
        imageView.multipleTouchEnabled = YES;
        UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                                    action:@selector(categoryTapGestureCaptured:)];
        singleTap.numberOfTapsRequired = 1;
        [imageView addGestureRecognizer:singleTap];
        [categoryView addSubview:imageView];

        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];

        UIBezierPath *path = [UIBezierPath bezierPath];
        [path addArcWithCenter:location
                        radius:self.view.frame.size.width*1.5
                    startAngle:0.8
                      endAngle:-0.3+(0.1*(i+1))
                     clockwise:NO];

        animation.path = path.CGPath;
        imageView.center = path.currentPoint;
        animation.fillMode              = kCAFillModeForwards;
        animation.removedOnCompletion   = NO;
        animation.duration              = 1+0.25*i;
        animation.timingFunction        = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

        // Apply it
        [imageView.layer addAnimation:animation forKey:@"animation.trash"];
   }
    }

And this is the code to change the image on click.

for (UIImageView *subview in subviews) {
        NSString *key = [NSString stringWithFormat:@"%@_%ld.png",subview.accessibilityHint,(long)rating];
        if ([SelectedCategory isEqualToString:subview.accessibilityHint]) {
            NSString *tempSubCategory = [categoryObj objectForKey:SelectedCategory];
            if([selectedCategories containsObject:SelectedCategory]){
                subview.image = [self.mySprites objectForKey:key];
                [selectedCategories removeObject:SelectedCategory];
                if (tempSubCategory.length != 0) {
                    subCategoriesAvailable = subCategoriesAvailable-1;
                }
                [self showNoPagination:subCategoriesAvailable+2];
            }else{
                if(selectedCategories.count != 2){
                key = [NSString stringWithFormat:@"sel_%@",key];
                subview.image = [self.mySprites objectForKey:key];
                [selectedCategories addObject:SelectedCategory];
                    if ([SelectedCategory isEqualToString:@"Other"]) {
                        [self showCommentDialog];
                    }else{
                        if (tempSubCategory.length != 0) {
                            subCategoriesAvailable = subCategoriesAvailable+1;
                        }
                        [self showNoPagination:subCategoriesAvailable+2];
                    }
                }
            }
            [self disableCategories];
            break;
        }
    }

And i don't know what am doing wrong here. I tried nullifying on for loop but no use.

Allocation inspector

Code which i used for removing the image view

UIView *categoryView = [self.view viewWithTag:500];
    NSArray *subviews = [categoryView subviews];
    for (UIImageView *subview in subviews) {
        if(![selectedCategories containsObject:subview.accessibilityHint]){
            [subview removeFromSuperview];
            subview.image = Nil;
        }
    }

Adding sprite reader code for reference

#import "UIImage+Sprite.h"
#import "XMLReader.h"

@implementation UIImage (Sprite)

+ (NSDictionary*)spritesWithContentsOfFile:(NSString*)filename
{
    CGFloat scale = [UIScreen mainScreen].scale;
    NSString* file = [filename stringByDeletingPathExtension];
    if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)] && 
        (scale == 2.0))
    {
        file = [NSString stringWithFormat:@"%@@2x", file];
    }
    NSString* extension = [filename pathExtension];
    NSData* data = [NSData dataWithContentsOfFile:[NSString stringWithFormat:@"%@.%@", file,extension]];
    NSError* error = nil;
    NSDictionary* xmlDictionary = [XMLReader dictionaryForXMLData:data error:&error];
    NSDictionary* xmlTextureAtlas = [xmlDictionary objectForKey:@"TextureAtlas"];
    UIImage* image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@.%@", file,[[xmlTextureAtlas objectForKey:@"imagePath"]pathExtension]]];
    CGSize size = CGSizeMake([[xmlTextureAtlas objectForKey:@"width"] integerValue],
                             [[xmlTextureAtlas objectForKey:@"height"] integerValue]);

    if (!image || CGSizeEqualToSize(size, CGSizeZero)) return nil;
    CGImageRef spriteSheet = [image CGImage];
    NSMutableDictionary* tempDictionary = [[NSMutableDictionary alloc] init];

    NSArray* xmlSprites = [xmlTextureAtlas objectForKey:@"sprite"];
    for (NSDictionary* xmlSprite in xmlSprites)
    {
        CGRect unscaledRect = CGRectMake([[xmlSprite objectForKey:@"x"] integerValue],
                                         [[xmlSprite objectForKey:@"y"] integerValue],
                                         [[xmlSprite objectForKey:@"w"] integerValue],
                                         [[xmlSprite objectForKey:@"h"] integerValue]);
        CGImageRef sprite = CGImageCreateWithImageInRect(spriteSheet, unscaledRect);
        // If this is a @2x image it is twice as big as it should be.
        // Take care to consider the scale factor here.
        [tempDictionary setObject:[UIImage imageWithCGImage:sprite scale:scale orientation:UIImageOrientationUp] forKey:[xmlSprite objectForKey:@"n"]];
        CGImageRelease(sprite);
    }

    return [NSDictionary dictionaryWithDictionary:tempDictionary];
}

@end

Please help me to resolve this. Thanks in advance.

Upvotes: 1

Views: 1221

Answers (4)

Sanjay Mohnani
Sanjay Mohnani

Reputation: 5967

Alternatively, to resolve memory leak issue you can remove the corresponding animation on corresponding imageView' layer, by implementing delegate of CAAnimation, like as shown below -

/* Called when the animation either completes its active duration or * is removed from the object it is attached to (i.e. the layer). 'flag' * is true if the animation reached the end of its active duration * without being removed. */

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if (flag) {
        NSLog(@"%@", @"The animation is finished. Do something here.");
    }

     //Get the reference of the corresponding imageView
    [imageView.layer removeAnimationForKey:@"animation.trash"];
}

 UIView *categoryView = [self.view viewWithTag:500];
    NSArray *subviews = [categoryView subviews];
    for (UIImageView *subview in subviews) {
        if(![selectedCategories containsObject:subview.accessibilityHint]){
            [subview.layer removeAnimationForKey:@"animation.trash"]; // either this
            //or// [subview.layer removeAllAnimations]; //alternatively
            [subview removeFromSuperview];
            subview.image = Nil;
        }
    }

Upvotes: 0

Sanjay Mohnani
Sanjay Mohnani

Reputation: 5967

Is there any specific requirement to set

animation.removedOnCompletion = NO;

since, setting

animation.removedOnCompletion = YES;

could solve the issue related to memory leak.

Upvotes: 0

Sanjay Mohnani
Sanjay Mohnani

Reputation: 5967

CAKeyframeAnimation inherits from CAPropertyAnimation which in tern is inherited from CAAnimation.

If you see the delegate of CAAnimation class, it is strongly referenced as written below as it is declared -

/* The delegate of the animation. This object is retained for the * lifetime of the animation object. Defaults to nil. See below for the * supported delegate methods. */

@property(strong) id delegate;

Now you have added the reference of animation on imageView.layer, doing so the reference of imageView.layer will be strongly retained by CAAnimation reference.

Also you have set animation.removedOnCompletion = NO;

which won't remove the animation from layer on completion

So if you are done with a image view then first removeAllAnimations from its layer and then release the image view.

I think as the CAAnimation strongly refers the imageView reference(it would also have increased it's retain count) and this could be the reason that you have removed the imageView from superview, after which it's retain count would still not be zero, and so leading to a leak.

Upvotes: 0

Sam Clewlow
Sam Clewlow

Reputation: 4351

It looks like all the images are being retained by the dictionary(assumption) self.mySprites, as you are loading them with the call imageView.image = [self.mySprites objectForKey:categoryImage];

If you loaded the images into the dictionary with +[UIImage imageNamed:], then the dictionary initially contains only the compressed png images. Images are decompressed from png to bitmap as they are rendered to the screen, and these decompressed images use a large amount of RAM (that's the memory usage you're seeing labeled "ImageIO_PNG_Data"). If the dictionary is retaining them, then the memory will grow every time you render a new one to the screen, as the decompressed data is held inside the UIImage object retained by the dictionary.

Options available to you:

  • Store the image names in the self.mySprites dictionary, and load the images on demand. You should be aware that +[UIImage imageNamed:] implements internal RAM caching to speed things up, so this might also cause memory issues for you if the images are big, as the cache doesn't clear quickly. If this is an issue, consider using +[UIImage imageWithContentsOfFile:], although it requires some additional code (not much), which doesn't cache images in RAM.

  • Re-implement self.mySprites as an NSCache. NSCache will start throwing things out when the memory pressure gets too high, so you'll need to handle the case that the image is not there when you expect it to be, and load it from disk (perhaps using the above techniques)

Upvotes: 2

Related Questions