15

我最近开始使用 ARC,从那以后我把每一个内存问题都归咎于它。:) 也许,你可以帮助我更好地理解我做错了什么。

我目前的项目与 CoreGraphics 有关 - 图表绘制、带有缩略图的视图等等。我相信使用手动内存管理不会有问题,除了一些僵尸......但到目前为止,每次我尝试创建大量缩略图或重绘更复杂的图表时,应用程序都会崩溃。

在使用 Instruments 进行分析时,我可以在常驻内存和脏内存中看到非常高的价值。堆分析显示相当惊人的不规则增长......

仅绘制几个缩略图时,常驻内存会增长约 200 MB。绘制完所有内容后,内存会回落到与绘制前几乎相同的值。但是,对于大量缩略图,常驻内存中的值高于400 MB,这显然会使应用程序崩溃。我试图限制同时绘制的缩略图数量(NSOperationQueue 及其 maxConcurrentOperationCount),但由于释放这么多内存似乎需要更多时间,它并没有真正解决问题。

现在我的应用程序基本上不起作用,因为真实数据适用于很多复杂的图表 = 很多缩略图。

每个缩略图都是用我从这里得到的代码创建的:(UIImage 上的类别)

+ (void)beginImageContextWithSize:(CGSize)size
{
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
        if ([[UIScreen mainScreen] scale] == 2.0) {
            UIGraphicsBeginImageContextWithOptions(size, YES, 2.0);
        } else {
            UIGraphicsBeginImageContext(size);
        }
    } else {
        UIGraphicsBeginImageContext(size);
    }
}

+ (void)endImageContext
{
    UIGraphicsEndImageContext();
}

+ (UIImage*)imageFromView:(UIView*)view
{
    [self beginImageContextWithSize:[view bounds].size];
    BOOL hidden = [view isHidden];
    [view setHidden:NO];
    [[view layer] renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    [self endImageContext];
    [view setHidden:hidden];
    return image;
}

+ (UIImage*)imageFromView:(UIView*)view scaledToSize:(CGSize)newSize
{
    UIImage *image = [self imageFromView:view];
    if ([view bounds].size.width != newSize.width ||
        [view bounds].size.height != newSize.height) {
        image = [self imageWithImage:image scaledToSize:newSize];
    }
    return image;
}

+ (UIImage*)imageWithImage:(UIImage*)image scaledToSize:(CGSize)newSize
{
    [self beginImageContextWithSize:newSize];
    [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    [self endImageContext];
    return newImage;
}

在使用 ARC 时,是否有其他方式不会占用太多内存或者代码真的有问题?

发生内存警告+崩溃的另一个地方是任何视图的重绘过多。它不需要很快,只要多次。内存堆积起来,直到它崩溃,我无法找到任何真正对它负责的东西。(我可以在 VM Tracker 中看到不断增长的常驻/脏内存和在 Allocations 工具中的堆增长)

我的问题基本上是:如何找到它为什么会发生?我的理解是当给定对象没有所有者时,它会尽快发布。我对代码的检查表明,许多对象根本没有释放,即使我看不出有任何原因发生。我不知道任何保留周期...

我已经阅读了 Transitioning to ARC Release Notes、bbum 的关于堆分析的文章以及可能还有其他十几篇文章。使用和不使用 ARC 的堆分析有何不同?我似乎无法对它的输出做任何有用的事情。

谢谢你的任何想法。

更新:(不强迫每个人阅读所有评论并遵守我的承诺)

通过仔细检查我的代码并添加@autoreleasepool,它有任何意义,内存消耗降低了。UIGraphicsBeginImageContext最大的问题是从后台线程调用。修复后(有关详细信息,请参阅@Tammo Freese 的答案)释放很快就会发生,不会导致应用程序崩溃。

我的第二次崩溃(由多次重绘同一图表引起)完全通过 CGContextFlush(context)在我的绘图方法末尾添加来解决。真丢人。


对任何尝试做类似事情的人的一个小警告:使用 OpenGL。CoreGraphics 对大型绘图的动画制作速度不够快,尤其是在 iPad 3 上。(第一个带有视网膜的)

4

3 回答 3

18

回答您的问题:使用 ARC 识别内存警告和崩溃问题基本上像以前使用手动保留释放 (MRR) 一样工作。ARC 使用retain,就像 MRR 一样,它只为你插入调用,并且有一些优化,在某些情况下甚至可以降低内存消耗releaseautorelease

关于你的问题:

您发布的 Instruments 屏幕截图中,可以看到分配峰值。到目前为止,在我遇到的大多数情况下,这些峰值是由自动释放的对象悬停太久引起的。

  1. 你提到你使用NSOperationQueue. 如果重写-[NSOperationQueue main],请确保将方法的全部内容包装在@autoreleasepool { ... }. 自动释放池可能已经存在,但不能保证(即使有,它的存在时间也可能比您想象的要长)。

  2. 如果 1. 没有帮助,并且您有一个处理图像的循环,请将循环的内部包裹起来,@autoreleasepool { ... }以便立即清理临时对象。

  3. 你提到你使用NSOperationQueue. 从 iOS 4 开始,在 UIKit 中绘制到图形上下文是线程安全的,但如果文档是正确的,UIGraphicsBeginImageContext仍然应该只在主线程上调用!更新:文档现在声明,从 iOS 4 开始,可以从任何线程调用该函数,以下实际上是不必要的!为了安全起见,使用 . 创建上下文CGBitmapContextCreate并使用 检索图像CGBitmapContextCreateImage。这些方面的东西:

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);
    
    // draw to the context here
    
    CGImageRef newCGImage = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    UIImage *result = [UIImage imageWithCGImage:newCGImage scale:scale orientation: UIImageOrientationUp];
    CGImageRelease(newCGImage);
    
    return result;
    
于 2012-07-27T16:49:53.043 回答
2

因此,您在内存管理方面所做的任何事情(没有!)看起来都不合适。但是,您提到使用 NSOperationQueue。那些 UIGraphics... 调用被标记为不是线程安全的,但其他人说它们是 iOS 4 的(我找不到明确的答案,但记得这是真的。

在任何情况下,您都不应该从多个线程调用这些类方法。您可以创建一个串行调度队列并通过它提供所有工作以确保单线程使用。

当然,这里缺少的是您在使用图像后如何处理它们。您可能以某种不明显的方式保留它们。这里有一些技巧:

  • 在任何使用大量图像的类中,添加一个仅记录其名称和一些标识符的 dealloc() 方法。

  • 你可以尝试向 UIImage 添加一个 dealloc 来做同样的事情。

  • 尝试使用最简单的设置来驱动您的应用程序 - 最少的图像等 - 这样您就可以验证图像及其所有者是否正在被释放。

  • 当您想确保发布某些内容时,请将 ivar 或属性设置为 nil

去年夏天,我将一个 100 个文件的项目转换为 ARC,它开箱即用。当我不正确地使用桥接时,我也将几个开源项目转换为 ARC,但只有一个问题。该技术坚如磐石。

于 2012-07-24T13:42:32.617 回答
2

这不是您问题的答案,但早在引入 ARC 之前,我就试图解决类似的问题。最近我正在开发一个应用程序,它在内存中缓存图像并在收到内存警告后将它们全部释放。只要我以正常速度使用该应用程序(没有疯狂敲击),它就可以正常工作。但是当我开始生成大量事件并开始加载许多图像时,应用程序无法获得内存警告并且崩溃了。

我曾经写过一个测试应用程序,它在点击一个按钮后创建了许多自动释放的对象。我能够比操作系统释放内存的速度更快(并创建对象)。内存在慢慢增加,所以在很长一段时间后或只是使用更大的对象后,我肯定会使应用程序崩溃并导致设备重新启动(看起来真的很有效;))。我检查了使用 Instruments 不幸地影响了测试并使一切变慢但我想这在不使用 Instruments 时也是如此。

另一次,我正在做一个更大的项目,该项目非常复杂,并且有很多由代码创建的 UI。它也有很多字符串处理,没有人关心使用 release - 我上次检查时有几千个 autorelease 调用。因此,在稍微广泛使用此应用程序 5 分钟后,它正在崩溃并重新启动设备。

如果我是正确的,那么负责实际释放内存的操作系统/逻辑不够快或没有足够高的优先级来避免应用程序在执行大量内存操作时崩溃。我从未证实这些怀疑,除了简单地减少分配的内存之外,我不知道如何解决这个问题。

于 2012-07-25T11:59:00.513 回答