2

我有兴趣了解更多关于如何在使用 ARC 的紧密循环下最好地处理内存管理的信息。特别是,我有一个我正在编写的应用程序,它有一个while循环运行了很长时间,我注意到尽管在 ARC 中实现了(我相信是)最佳实践,但堆仍然存在无限成长。

为了说明我遇到的问题,我首先将以下测试设置为故意失败:

while (true) {         
    NSMutableArray *array = [NSMutableArray arrayWithObject:@"Foo"];
    [array addObject:@"bar"]; // do something with it to prevent compiler optimisations from skipping over it entirely
}

运行此代码并使用分配工具进行分析表明内存使用量无休止地增加。但是,将其包装@autoreleasepool如下,可以立即解决问题并保持内存使用良好且低:

while (true) {
    @autoreleasepool {         
        NSMutableArray *array = [NSMutableArray arrayWithObject:@"Foo"];
        [array addObject:@"bar"];
    }
}

完美的!这一切似乎都运行良好——甚至对于使用[[... alloc] init]. 一切正常,直到我开始涉及任何UIKit课程。

例如,让我们创建一个UIButton并看看会发生什么:

while (true) {
    @autoreleasepool {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        button.frame = CGRectZero;
    }
}

现在,内存使用量无限增加——实际上,似乎@autoreleasepool没有任何效果。

所以问题是为什么@autoreleasepool对 和 保持内存正常工作NSMutableArray,但是当应用于 a 时UIButton,堆继续增长?

UIKit最重要的是,当在这样的无限循环中使用类时,如何防止堆无限扩展,这告诉我们关于 ARCwhile(true)while(keepRunningForALongTime)样式循环的最佳实践是什么?

我对此的直觉是(我可能完全错了)可能是关于如何while (true)保持运行循环不循环,这将UIKit实例保存在内存中而不是释放它们......但显然我错过了一些东西我对ARC的理解!

(并且为了消除一个明显的原因,NSZombiedEnabled没有启用。)

4

3 回答 3

5

至于为什么 UI* 对象会无限增长?内部实现细节。最有可能的是,您通过阻塞主运行循环来有效地禁用某种缓存或运行循环交互。

这让我得到了真正的答案:

最重要的是,当在这样的无限循环中使用 UIKit 类时,如何防止堆无限扩展,这告诉我们在 while(true) 或 while(keepRunningForALongTime) 样式循环中 ARC 的最佳实践是什么?

如何解决? 永远不要在主线程上使用紧密循环,不要阻塞主运行循环

即使您要弄清楚并解决 UI* 引起的堆增长,如果您要while(...)在主线程上使用循环,您的程序仍然无法运行。iOS 应用程序和 Cocoa 应用程序的整个设计是主线程有一个主运行循环,并且主运行循环必须可以自由运行。

如果不?您的应用程序不会响应用户输入(最终会被系统杀死),并且您的绘图代码不太可能按预期工作(因为运行循环会合并脏区域并与主线程一起按需绘制它们,经常卸载到辅助线程)。

于 2013-01-13T18:05:47.850 回答
1

我在这里推测,但是,它可以归结为 UI 相关对象特别倾向于使用 GCD 或类似对象(例如 performSelectorOnMainThread:...)以确保在主线程上发生一些操作。正如您所怀疑的那样 - 排队的块或其他执行单元维护对实例的引用,等待它在运行循环中执行的时间,并且永远不会得到它。

通常,阻止运行循环是不好的。曾几何时,它曾经是相对常见的——拖动跟踪通常以这种方式完成(或者有效地这样做,通过在拖动进行时仅在特殊模式下运行 runloop)。但这会导致奇怪的交互甚至死锁,因为很多代码的设计都没有考虑到这种可能性——尤其是在异步为王的 GCD 世界中。

请记住,如果您愿意,可以在 while 循环中显式运行 runloop,虽然这与让它自然运行并不完全相同,但它通常可以工作。

于 2013-01-13T17:00:54.383 回答
0

是的,我对此做了更多的思考,以及bbumWade Tegaskis关于阻塞 runloop 的巨大贡献,并意识到缓解此类问题的方法是让 runloop 循环,通过使用performSelector:withObject:afterDelay:which 让runloop continue,同时安排循环在将来继续。

例如,回到我原来的例子,UIButton现在应该重写为这样的方法:-

- (void)spawnButton {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectZero;
    [self performSelector:@selector(spawnButton) withObject:nil afterDelay:0];
}

这样,该方法立即结束并button在超出范围时正确释放,但在最后一行,spawnButton指示runloopspawnButton在0秒内再次运行(即尽快),这反过来又指示runloop运行。 ..等等等等,你明白了。

然后,您需要做的就是[self spawnButton]在代码中的其他地方调用以使循环继续进行。

这也可以使用 GCD 类似地解决,下面的代码基本上做同样的事情:

- (void)spawnButton {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectZero;
    dispatch_async(dispatch_get_main_queue(), ^{
        [self spawnButton];
    });
}

这里唯一的区别是方法调用是使用 GCD (异步)分派到主队列(主运行循环)上的。

在 Instruments 中对其进行分析,我现在可以看到,尽管整体分配内存在增加,但活动内存仍然很低并且是静态的,这表明 runloop 正在循环,并且旧UIButton的 s 正在被释放。

通过考虑这样的运行循环,并使用performSelector:withObject:afterDelay:或 GCD,实际上还有许多其他实例(不仅仅是 UIKit),这种方法可以防止由运行循环锁定引起的意外“内存泄漏”(我在引用中使用它,因为在在这种情况下,我是一个 UIKit 流氓.. 但在其他情况下,这种技术很有用。)

于 2013-01-30T23:02:45.213 回答