【问题标题】:Memory leak when setting a UIImage in an NSOperation在 NSOperation 中设置 UIImage 时的内存泄漏
【发布时间】:2013-02-04 22:28:14
【问题描述】:

我遇到了一个问题,相对较大的图像似乎永远不会从内存中释放出来(大小为 1MB~5MB)。当用户滚动一组图像时,会调用以下代码块。大约 15 张图像后,应用程序将崩溃。有时会调用“didReceiveMemoryWarning”,有时不会调用——应用程序只会崩溃、停止、退出调试,并且不会在任何代码行上停止——什么都没有。我认为这是设备内存不足时会发生的情况?另一个问题是“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];

下载图像操作定义:

.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

.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

获取图像类

.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;
}

为这个巨大的代码块道歉。我真的不知道问题可能出在哪里,所以我尽量做到包容。感谢阅读。

【问题讨论】:

  • 您可以在您的项目中使用 ARC 吗?它通常可以简化内存管理。
  • 很遗憾,这不是使用 ARC。这个项目是在 iOS 5 之前开始的。不过我很想找到一些方法来转换它。
  • @Erracity - 我认为转向 ARC 确实值得,即使您第一次这样做需要付出一些努力。它使您摆脱了内存管理的麻烦,并消除了许多简单的可能泄漏。您仍然需要使用我在下面向您展示的blockImageOp 技巧的__weak 变体(例如__weak DownloadImageOperation *weakImageOp = imageOp;)来避免完成块中的保留循环,但是所有手动调用retainrelease走开。
  • 感谢您的洞察力,现在运行良好。我很可能会花一些时间来重构它以使用 ARC,特别是因为看起来 xcode 中有一个工具可以这样做

标签: ios xcode memory-management memory-leaks


【解决方案1】:

操作未释放的主要问题是您有一个保留周期,这是由完成块中的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];
}];

在 ARC 中,您可以在操作中添加一个 __weak 限定符,并在 completionBlock 中使用它而不是 imageOp,以避免强引用循环。在手动引用计数中,您可以通过使用__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];
}];

我认为如果你这样做,你会看到你的操作被正确释放。 (请参阅Transitioning to ARC Release Notes 的“使用生命周期限定符来避免强引用循环”部分。我知道您没有使用 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];
        }];
    }];
    
  • 您在 init 方法中使用了访问器方法。作为一个良好的做法,你真的不应该。请参阅高级内存管理编程指南中的Don’t Use Accessor Methods in Initializer Methods and dealloc

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

  • 我不确定您的子类 NSOperation 是否支持并发,因为您缺少 并发编程指南中 Defining a Custom Operation 中讨论的一些关键方法。也许您有已经这样做了,但为简洁起见省略了它。或者,我认为如果您使用现有的 NSOperation 类之一(例如 NSBlockOperation),它会为您处理这些事情。您的电话,但如果您追求并发,您需要确保将队列的 maxConcurrentOperationCount 设置为合理的值,例如 4。

  • 您的代码有一些多余的retain 语句。话虽如此,你也有必要的release 语句,所以你确保你不会有问题,但这只是有点好奇。显然,ARC 让你摆脱了这类东西的麻烦,但我很感激这是一大步。但是,如果您有机会,不妨看看 ARC,因为它可以让您不必担心很多此类问题。

  • 您可能应该通过静态分析器(“产品”菜单上的“分析”)运行您的代码,因为您有一些死店之类的东西。

【讨论】:

  • 我应该在 OP 中提到这不是使用 ARC,尽管我在其他项目中。这个项目在 iOS 5 之前就开始了。然而,知道这会导致一个保留周期是一个非常有用的例子。我已经运行了分析并清除了很多明显的缺陷,但是分析在上面的代码中没有发现任何东西。非常感谢 Rob,您对上述代码进行了非常彻底的批评。
  • 不应该是__block DownloadImageOperation *blockImageOp = imageOp;这一行实际上是__weak DownloadImageOperation *blockImageOp = imageOp;吗?
  • 在这种情况下不是。如果他使用 ARC,是的,但这是一个手动引用计数问题。你不能在手动引用计数代码中使用weak。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多