内存警告和崩溃(ARC)- 如何确定其发生的原因?

15
我最近开始使用ARC,自那以后,我把每一个内存问题都归咎于它。 :) 或许,你可以帮我更好地理解我做错了什么。
我的当前项目与CoreGraphics有很多关系 - 图表绘制、缩略图填充视图等等。我认为,如果使用手动内存管理,除了可能会出现一些僵尸进程外,应该不会有问题... 但目前,每当我尝试创建大量缩略图或重新绘制稍微复杂的图表时,应用程序就会崩溃。
在使用Instruments进行分析时,我可以看到驻留内存和脏内存的值非常高。堆分析显示出了相当令人担忧的不规则增长...
当只绘制几个缩略图时,驻留内存会增长约200MB。当所有东西都被绘制完后,内存会回到几乎与绘制前相同的值。然而,对于大量的缩略图,驻留内存中的值高于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中看到不断增长的常驻/脏内存和在分配仪器中的堆增长)
我的问题基本上是:如何找出为什么会发生这种情况?我的理解是,当给定对象没有所有者时,它会尽快释放。我检查代码发现,很多对象根本没有被释放,即使我看不到任何原因发生这种情况。我不知道是否存在任何保留周期...
我已经阅读了“过渡到ARC发布说明”、bbum关于堆分析的文章以及可能有十几篇其他文章。有什么区别是使用ARC和不使用ARC进行堆分析吗?我似乎无法对其输出做任何有用的事情。
感谢任何想法。
更新:(为了不强迫每个人都阅读所有评论并遵守我的承诺)
通过仔细检查我的代码并在有意义的地方添加@autoreleasepool,内存消耗得到了降低。最大的问题是从后台线程调用UIGraphicsBeginImageContext。修复后(详见@Tammo Freese的答案),释放发生得足够快,不会导致应用程序崩溃。
我的第二个崩溃(由于多次重绘相同的图表而引起)完全通过在绘图方法末尾添加CGContextFlush(context)解决。我真是太丢人了。

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


编译器不会自动管理Core Foundation对象的生命周期;您必须根据Core Foundation内存管理规则调用CFRetain和CFRelease(或相应的类型特定变体)(请参见Core Foundation内存管理编程指南)。请参阅http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html此外,可以在此处找到关于ARC、免费桥接和核心图形的优秀文章:https://dev59.com/P2sz5IYBdhLWcg3wlY0n#7800359 - Lefteris
@Lefteris 谢谢,我已经阅读了苹果的文章并观看了你提供的答案中提到的视频,但是当我在绘图时,我并没有真正记住任何东西,只是简单的 CGContextAddLineToPointCGContextAddRect等返回空值。我应该释放从 CGContextRef context = UIGraphicsGetCurrentContext(); 获取的上下文吗?虽然我会研究一下。 - xius
3个回答

18

回答你的问题:使用ARC识别内存警告和崩溃问题的基本方法与使用手动保留-释放(MRR)相同。ARC使用retainreleaseautorelease,只是为您插入调用,并且具有一些优化,应该甚至可以在某些情况下降低内存消耗。

关于你的问题:

在你发布的Instruments截图中,可以看到分配峰值。在我遇到的大多数情况下,这些峰值是由自动释放对象挂起时间过长引起的。

  1. You mentioned that you use NSOperationQueue. If you override -[NSOperationQueue main], make sure that you wrap the whole content of the method in @autoreleasepool { ... }. An autorelease pool may already be in place, but it is not guaranteed (and even if there is one, it may be around for longer than you think).

  2. If 1. has not helped and you have a loop that processes the images, wrap the inner part of the loop in @autoreleasepool { ... } so that temporary objects are cleaned up immediately.

  3. You mentioned that you use NSOperationQueue. Since iOS 4, drawing to a graphics context in UIKit is thread-safe, but if the documentation is right, UIGraphicsBeginImageContext should still only be called on the main thread! Update: The docs now state that since iOS 4, the function can be called from any thread, to the following is actually unnecessary! To be on the safe side, create the context with CGBitmapContextCreate and retrieve the image with CGBitmapContextCreateImage. Something along these lines:

    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;
    

谢谢!看起来是解决内存问题的可能方案,但CGBitmapContextCreate总是返回nil,我还没有找到原因...?@autoreleasepool在我的代码周围。 :)(就像你提到的所有地方) - xius
@xius,您能告诉我们哪个修复方案减少了内存消耗吗? - Tammo Freese
@TammoFreese 不是100%确定,因为现在只有第一代iPad可用,而且该应用程序没有针对其进行优化...但内存不会堆积,它已经被正确释放 - 看起来你是赢家! :) 我认为摆脱 UIGraphicsBeginImageContext 并在 [NSOperationQueue main] 中添加 @autoreleasepool 是至关重要的。(我已经在绘制图表本身中修复了一个非常愚蠢的错误,这可能也极大地帮助了:))我稍后会更新我的问题并列出所有更改。谢谢! :) - xius
哇!第二步解决了我的问题。我被困在这个问题上已经几周了! - JCutting8
@JCutting8 很高兴我能帮到你! - Tammo Freese
显示剩余11条评论

2
所以,就内存管理而言(实际上没有),你所做的一切看起来都是合适的。但是,你提到使用了NSOperationQueue。虽然其他人已经说明它们在iOS 4中已经线程安全了,但是你仍然不应该从多个线程调用这些类方法。你可以创建一个串行调度队列,并将所有的工作通过它来保证单线程使用。
当然,在使用完图片后你缺少的是接下来要怎么处理这些图片。以下是一些技巧:
- 在任何使用大量图片的类中添加dealloc()方法,只需记录其名称和某些标识符即可。 - 你可以尝试为UIImage添加dealloc()方法来执行相同的操作。 - 尽可能使用最简单的设置 - 最少的图片等等 - 这样你就可以验证图片及其所有者是否确实被释放了。 - 当你想要确保某些东西被释放时,将ivar或property设置为nil。
去年夏天,我将一个包含100个文件的项目转换为ARC,结果直接开箱即用。我还将几个开源项目转换为ARC,只有在错误使用桥接时才出现了一个问题。这项技术非常稳定。

1
谢谢提供的一些想法!我明天会试试。我还可以确认一个问题不在ARC中-我已经重新编写了整个应用程序来手动管理内存,猜猜怎么着...所有东西都完全相同。(至少从这里讨论的问题角度来看) - xius

2
这并不是您问题的答案,但是在引入ARC之前,我一直在努力解决类似的问题。最近,我正在开发一个应用程序,将图像缓存在内存中,并在收到内存警告后释放所有图像。只要我以正常的速度使用应用程序(没有疯狂的点击),这个方法运行良好。但当我开始生成大量事件并且许多图像开始加载时,应用程序就无法得到内存警告并崩溃了。
我曾经编写过一个测试应用程序,当我点击按钮时会创建很多自动释放的对象。我能够比操作系统更快地点击(并创建对象)。由于内存释放不及时,内存会慢慢增加,因此在显著的时间或仅仅使用更大的对象后,我肯定会崩溃应用程序并导致设备重新启动(看起来真的很有效 ;))。我使用Instruments检查了这个问题,但不幸的是,这影响了测试并使一切变得更慢,但我认为即使不使用Instruments,这也是正确的。
另一次,我正在处理一个较大的项目,该项目相当复杂,并且有很多从代码中创建的UI。它还有很多字符串处理,没有人使用释放 - 上一次检查时,有几千次自动释放调用。因此,在稍微广泛使用该应用程序5分钟后,它就会崩溃并重新启动设备。
如果我正确的话,那么实际上负责释放内存的操作系统/逻辑不够快或优先级不够高,无法在执行大量内存操作时避免应用程序崩溃。我从未确认这些怀疑,并且我不知道如何解决这个问题,除了简单地减少分配的内存。

我还通过疯狂地点击四处来重置设备! :) 我已经成功地降低了一点内存需求,让我可以慢慢地运行它。不够用于实际使用 - 这可能是设计不良的表现吧。我的错,我从未意识到我实际上可以使用所有可用的内存。如果我之前读过像你这样的答案就好了。 :) 谢谢。 - xius

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