在UIScrollView中使用较大图片的UIImageViews释放内存时出现问题

11

我有一个很大的UIScrollView,里面放了3-4个相当大的(大约320x1500像素)UIImageView图像块。这些UIImageView被添加到我的NIB文件中的滚动视图中。我的控制器上只有一个插座,即用于UIScrollView的。我为此使用属性(nonatomic,retain),并合成它。

我的问题是:当我在Memory Monitor中观察时,在加载所有这些图像的视图时,使用的内存会增加相当多(预料之中)。但是当我离开该视图时,它及其控制器都被dealloc'd,但似乎没有释放掉它们所占用的远远不足的内存。当我将其中一个视图(我的应用程序中有几个)减少到仅包含1-3个大小为320x460的图像,并保持其他所有内容不变时,它可以很好地重新获取内存。

使用这么大的图像是否存在问题?在这段代码中我做错了什么吗?

这是引起问题的viewController的一部分代码:

- (CGFloat)findHeight 
{
    UIImageView *imageView = nil;
    NSArray *subviews = [self.scrollView subviews];

    CGFloat maxYLoc = 0;
    for (imageView in subviews)
    {
            if ([imageView isKindOfClass:[UIImageView class]])
            {
                    CGRect frame = imageView.frame;

                    if ((frame.origin.y + frame.size.height) > maxYLoc) {
                            maxYLoc  = frame.origin.y;
                            maxYLoc += frame.size.height;
                    }
            }
    }
    return maxYLoc;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.scrollView setContentSize:CGSizeMake(320, [self findHeight])];

    [self.scrollView setCanCancelContentTouches:NO];
    self.scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
    self.scrollView.clipsToBounds = YES;                
    self.scrollView.scrollEnabled = YES;

    self.scrollView.pagingEnabled = NO;
}

- (void)dealloc {
    NSLog(@"DAY Controller Dealloc'd");
    self.scrollView = nil;
    [super dealloc];
}

更新:我注意到另一个奇怪的现象。如果我不使用视图上的滚动条,它似乎会占用内存。但是,如果我频繁滚动并确保所有UIImageView在某个时刻都可见,它将释放并重新获得失去的大部分内存。

更新2:我之所以问这个问题是因为我的应用程序实际上由于低内存而崩溃。如果它只是缓存并使用额外的内存,我也不介意,但似乎从来没有释放它 - 即使在didReceiveMmoryWarning条件下也是如此。

7个回答

15

我已经解决了这个问题-我相当确定这是苹果的一个bug。

正如Kendall所建议的那样(谢谢!),问题在于InterfaceBuilder从NIB文件中加载图像的方式。当您initFromNib时,所有UIImageView都将使用UIImage的imageNamed:方法初始化UIImage。此调用对图像使用缓存。通常,这就是您想要的。但是,对于非常大的图像以及滚动到可见区域之外的图像,它似乎没有遵守内存警告并卸载该缓存。这就是我认为是苹果端的一个bug(如果您同意或不同意,请发表评论-如果其他人同意,我会提交这个问题)。正如我上面所说,如果用户滚动足够多的时候使它全部可见,则使用这些图像的内存似乎被释放。

我发现的解决方法(也是Kendall的建议)是在NIB文件中留空图像名称。因此,您可以像平常一样布置您的UIImageView元素,但不选择图像。然后在您的viewDidLoad代码中,您可以使用imageWithContentsOfFile:加载图像。此方法不会缓存图像,因此不会导致保留大型图像的任何内存问题。

当然,imageNamed:要容易得多,因为它默认为bundle中的任何内容,而不必查找路径。但是,您可以使用以下路径获取bundle的路径:

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

综合起来,以下是代码示例:

NSString *fullpath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:[NSString stringWithFormat:@"/%@-%d.png", self.nibName, imageView.tag]];
UIImage *loadImage = [UIImage imageWithContentsOfFile:fullpath];
imageView.image = loadImage;

那么将其添加到我上面的代码中,完整函数看起来像这样:

- (CGFloat)findHeight 
{
    UIImageView *imageView = nil;
    NSArray *subviews = [self.scrollView subviews];

    CGFloat maxYLoc = 0;
    for (imageView in subviews)
    {
        if ([imageView isKindOfClass:[UIImageView class]])
        {
            CGRect frame = imageView.frame;

            if ((frame.origin.y + frame.size.height) > maxYLoc) {
                maxYLoc  = frame.origin.y;
                maxYLoc += frame.size.height;
            }

            NSString *fullpath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:[NSString stringWithFormat:@"/%@-%d.png", self.nibName, imageView.tag]];
            NSLog(fullpath);


            UIImage *loadImage = [UIImage imageWithContentsOfFile:fullpath];
            imageView.image = loadImage;
        }
    }
    return maxYLoc;
}

有点晚了,但你向苹果提交了这个 bug 吗?因为我同意这是他们那边的一个 bug。它从来没有被删除过。当我使用 imageWithData 时,我也遇到了同样的问题。 - Rana Tallal

4
它可能是系统在内存中缓存您的图像引用,假设您刚从IB媒体浏览器中拖入了图像,则下面的代码可能使用UIImage imageNamed:方法...... 您可以尝试使用imageWithContentsOfFile:或imageWithData:加载所有图像,并查看是否行为相同(在IB中拖入未填充的UIImageViews并在viewDidLoad:中设置内容)。 如果希望了解更多细节,请阅读UIImage类参考,它还描述了哪些方法被缓存。如果这是缓存,则很可能没问题,因为如果需要,系统会将其释放(您是否也尝试在模拟器中点击“模拟内存警告”?)

4

除了您的imageNamed缓存问题之外,回答您的问题:

我有一个大的UIScrollView,里面放置了3-4个相当大的(大约320x1500像素)UIImageView图像平铺。[...]使用这么大的图像是否存在一些问题?

在UIImage文档的标题中已经有了答案:

You should avoid creating UIImage objects that are greater than 1024 x 1024 in size.
Besides the large amount of memory such an image would consume, you may run into 
problems when using the image as a texture in OpenGL ES or when drawing the image 
to a view or layer.

此外,苹果声称已经在3.0及以后的版本中解决了imageNamed缓存刷新问题,尽管我自己还没有进行过广泛的测试。

我当前遇到了一些内存问题,很可能是因为imageNamed缓存了图片,而我并不需要它们被缓存,也不希望它们被缓存。这就像是告诉某人去做100个y的任务,但是他们在完成每一个y之后,却不管它们的位置都带着它们走,直到所有y的重量把他们压垮并且死亡。 - Sneakyness
所以,不,它在3.1.2版本中并没有被修复。 - Sneakyness
关于限制图像大小的评论似乎有点奇怪,因为iPad本身可以很好地处理那么大甚至更大的图像,对吧?那么,你认为他们在使用什么呢? - Josh
@Josh:UIImage文档指出,图像可能在大于1024x1024的尺寸下出现显示问题。这个限制可能更多地适用于内存较少的旧设备。这只是我的猜测,但与观察结果相符。 - Olie
@Sneakyness:你确定你没有多余的保留计数吗?我的应用处理着数百(有时甚至上千)个imageNamed: 图像,尽管系统会缓存它们,但在需要更多内存时也会释放未使用的图像。 - Olie
这是在原始的iPhone和iPhone 3G上实现的,早在3GS和4的时代之前。整个平台过于缓慢,无法支持同时加载全屏PNG图像每秒12-16次,同时加载和播放声音。 - Sneakyness

1

完成Bdebeez的回答。

一个好的想法是重写imageNamed:方法,调用imageWithContentsOfFile:方法。

以下是代码的实现思路:

@implementation UIImage(imageNamed_Hack)

+ (UIImage *)imageNamed:(NSString *)name {

    return [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@/%@", [[NSBundle mainBundle] bundlePath], name ] ];
}

@end

注意:使用此覆盖将不会有任何缓存加载UIImages,如果您需要此功能,则必须实现自己的缓存。


2
那似乎是一个极其糟糕的想法。 - Rog
通过分类覆盖默认方法名可能(并且很可能)导致未定义的行为。如果您真的想要覆盖此方法,应该使用方法混合技术。然而,最好的方法是编写自己的方法名称“imageNamedNoCache”,只有在需要时才使用它。 - hris.to

0

imageName存在一个已知的问题——内存泄漏。我找到了一个非常有用的解决方案——在应用程序委托中创建图像缓存,从而优化了我的应用程序的性能和内存使用。 请参阅此博客文章


谢谢这个信息。这个页面是一个解决烦恼的宝库。 - Sneakyness
博客文章链接是404,而且使用站点搜索也没有找到。 - August

0

从我所了解的内存管理行为来看,除非内存很低,否则视图不会被释放。尝试官方演示,例如SQLBooks:

  1. 使用泄漏监视器运行
  2. 通过每个视图运行
  3. 返回到根视图
  4. 您将注意到内存使用级别仍然相同。正如肯德尔所说,这可能被缓存了吗?

我认为您不应该关注此内存使用模式,因为当新图像指向UIScrollView时,旧图像对象将被释放,为新图像释放内存。


0
- (void)dealloc {
    NSLog(@"DAY Controller Dealloc'd");
    [self.scrollView release];
    [super dealloc];
}

试一下,你的@property()定义要求它被保留,但你没有显式释放对象。


我实际上尝试过这个。按照我的方式做可以确保指针在释放后变为nil。这是因为它调用了生成的setter,该setter首先保留nil(没有效果),然后释放scrollView,最后将scrollView设置为nil。(我刚刚再次运行以进行双重检查 :)) - Bdebeez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接