在NSOperation中设置UIImage时内存泄漏

Err*_*ity 2 xcode memory-leaks memory-management ios

我遇到的问题是,相对较大的图像似乎永远不会从内存中释放出来(大小为1MB~5MB).当用户滚动浏览一组图像时,将调用以下代码块.大约15张图像后,应用程序将崩溃.有时候,"didReceiveMemoryWarning"被调用,有时doesn't - 应用程序只会崩溃,停止,退出调试,并在任何一行代码不会停止 - 什么都没有.我假设当设备内存不足时会发生这种情况?另一个问题是'dealloc'似乎永远不会被称为子类'DownloadImageOperation'.有任何想法吗?

获取和设置图像:

//Calling this block of code multiple times will eventually cause the
// application to crash

//Memory monitor shows real memory jumping 5MB to 20MB increments in instruments.  
//Allocations tool shows #living creeping up after this method is called.
//Leaks indicate something is leaking, but in the 1 to 5 kb increments. Nothing huge.

DownloadImageOperation * imageOp = [[DownloadImageOperation alloc] initWithURL:imageURL localPath:imageFilePath];
[imageOp setCompletionBlock:^(void){
    //Set the image in a UIImageView in the open UIViewController.
    [self.ivScrollView setImage:imageOp.image];
}];
//Add operation to ivar NSOperationQueue
[mainImageQueue addOperation:imageOp];
[imageOp release];
Run Code Online (Sandbox Code Playgroud)

DownloadImageOperation定义:

.h文件

#import <Foundation/Foundation.h>

@interface DownloadImageOperation : NSOperation {
    UIImage * image;
    NSString * downloadURL;
    NSString * downloadFilename;
}

@property (retain) UIImage * image;
@property (copy) NSString * downloadURL;
@property (copy) NSString * downloadFilename;

- (id) initWithURL:(NSString *)url localPath:(NSString *)filename;

@end
Run Code Online (Sandbox Code Playgroud)

.m文件

#import "DownloadImageOperation.h"
#import "GetImage.h"

@implementation DownloadImageOperation

@synthesize image;
@synthesize downloadURL;
@synthesize downloadFilename;

- (id) initWithURL:(NSString *)url localPath:(NSString *)filename {

    self = [super init];

    if (self!= nil) {
        [self setDownloadURL:url];
        [self setDownloadFilename:filename];
        [self setQueuePriority:NSOperationQueuePriorityHigh];
    }

    return self;

}

- (void)dealloc { //This never seems to get called?
    [downloadURL release], downloadURL = nil;
    [downloadFilename release], downloadFilename = nil;
    [image release], image = nil;
    [super dealloc];
}

-(void)main{

    if (self.isCancelled) {
        return;
    }

    UIImage * imageProperty = [[GetImage imageWithContentsOfFile:downloadFilename andURL:downloadURL] retain];
    [self setImage:imageProperty];
    [imageProperty release];
    imageProperty = nil;
}

@end
Run Code Online (Sandbox Code Playgroud)

获取图像类

.m文件

+ (UIImage *)imageWithContentsOfFile:(NSString *)path andURL:(NSString*)urlString foundFile:(BOOL*)fileFound {

    BOOL boolRef;

    UIImage *image = nil;

    NSString* bundlePath = [[NSBundle mainBundle] bundlePath];

    if (image==nil) {
        boolRef = YES;
        image = [UIImage imageWithContentsOfFile:[[AppDelegate applicationImagesDirectory] stringByAppendingPathComponent:[path lastPathComponent]]];
    }
    if (image==nil) {
        boolRef = YES;
        image = [super imageWithContentsOfFile:path];
    }
    if (image==nil) {
        //Download image from the Internet
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

        NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];

        ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
        [request setTimeOutSeconds:120];
        [request startSynchronous];

        NSData *responseData = [[request responseData] retain];

        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];

        NSData *rdat = [[NSData alloc] initWithData:responseData];
        [responseData release];

        NSError *imageDirError = nil;
        NSArray *existing_images = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[path stringByDeletingLastPathComponent] error:&imageDirError];

        if (existing_images == nil || [existing_images count] == 0) {
            // create the image directory
            [[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:NO attributes:nil error:nil];
        }

        BOOL write_success = NO;
        write_success = [rdat writeToFile:path atomically:YES];

        if (write_success==NO) {
            NSLog(@"Error writing file: %@",[path lastPathComponent]);
        }

        image = [UIImage imageWithData:rdat];
        [rdat release];

    }

    return image;
}
Run Code Online (Sandbox Code Playgroud)

为这个巨大的代码块道歉.我真的不知道问题可能在哪里,所以我试图尽可能包容.谢谢阅读.

Rob*_*Rob 9

操作未被解除分配的主要问题是您有一个保留周期,由imageOp完成块中的引用引起.考虑一下您的代码:

DownloadImageOperation * imageOp = [[DownloadImageOperation alloc] initWithURL:imageURL localPath:imageFilePath];
[imageOp setCompletionBlock:^(void){
    //Set the image in a UIImageView in the open UIViewController.
    [self.ivScrollView setImage:imageOp.image];
}];
Run Code Online (Sandbox Code Playgroud)

在ARC中,您将为操作添加__weak限定符并使用该限定符而不是imageOp在其中completionBlock,以避免强引用循环.在手动引用计数中,可以通过使用__block限定符来避免保留周期,以实现相同的目的,即保持块不被保留imageOp:

DownloadImageOperation * imageOp = [[DownloadImageOperation alloc] initWithURL:imageURL localPath:filename];
__block DownloadImageOperation *blockImageOp = imageOp;
[imageOp setCompletionBlock:^(void){
    //Set the image in a UIImageView in the open UIViewController.
    [self.imageView setImage:blockImageOp.image];
}];
Run Code Online (Sandbox Code Playgroud)

我想如果你这样做,你会看到你的操作正确发布.(请参阅" 转换为ARC发行说明 "中的"使用寿命限定符以避免强引用周期" .我知道您没有使用ARC,但本节介绍了ARC和手动引用计数解决方案.)


如果您不介意,我对您的代码有其他意见:

  • 您不应该在completionBlock不将其分配到主队列的情况下更新UI ...所有UI更新都应该在主队列上进行:

    DownloadImageOperation * imageOp = [[DownloadImageOperation alloc] initWithURL:imageURL localPath:filename];
    __block DownloadImageOperation *blockImageOp = imageOp;
    [imageOp setCompletionBlock:^(void){
        //Set the image in a UIImageView in the open UIViewController.
        UIImage *image = blockImageOp.image;
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [self.imageView setImage:image];
        }];
    }];
    
    Run Code Online (Sandbox Code Playgroud)
  • 您在方法中使用了访问器方法init.作为一个良好的实践,你真的不应该.请参阅" 高级内存管理编程指南"中的"不使用初始化方法中的访问器方法"和"dealloc ".

  • 虽然我们可能已经修复了操作未被释放的问题,但我怀疑除非您已经编写了UIScrollViewDelegate调用以释放已从可见屏幕滚动的图像,否则您将继续存在内存问题.话虽如此,你可能已经解决了这个问题,如果是这样,我甚至为提及它而道歉.我只提出这个问题,因为它很容易解决这个NSOperation问题,但是当他们滚动屏幕时忽略滚动视图释放图像.

  • 我不确定您的子类NSOperation是否支持并发性,因为您缺少" 并发编程指南"中的" 定义自定义操作"中讨论的一些关键方法.也许你已经这样做了,但为了简洁起见省略了它.或者,我认为如果你使用现有的一个NSOperation类(例如NSBlockOperation)来处理这些东西会更容易.你的电话,但如果你追求并发,你就要确保你把队列设置为maxConcurrentOperationCount合理的,比如4.

  • 您的代码有一些冗余retain语句.话虽如此,你也有必要的release陈述,所以你已经确保你不会有问题,但这只是有点好奇.显然,ARC让你摆脱了那种杂草,但我很欣赏这是一个很大的进步.但是当你有机会的时候,看看ARC,因为它可以让你不必担心这个问题.

  • 您可能应该通过静态分析器运行代码("产品"菜单上的"分析"),因为您有一些死库等.