【发布时间】:2011-03-17 08:22:19
【问题描述】:
我刚刚花了整整一周的时间来追踪并解决内存泄漏问题,而在那周的另一端,我有点茫然。 必须有更好的方法来做到这一点,是我能想到的,所以我想是时候问这个相当沉重的话题了。
这个帖子原来是相当大的。对此深表歉意,尽管我认为在这种情况下,有必要尽可能彻底地解释细节。很明显,因为它为您提供了我为找到这个臭虫所做的所有事情的全貌,这很多。仅这个错误就花了我大约三个 10 多小时的时间来追踪......
当我寻找泄漏时
当我寻找泄漏时,我倾向于分阶段进行,如果在早期阶段无法解决,我会“更深入”地升级到问题。这些阶段从泄漏告诉我有问题开始。
在这种特殊情况下(这是一个示例;错误已解决;我不是在寻求解决此错误的答案,而是在寻求改进我发现的过程的方法错误),我在一个相当大的多线程应用程序中发现了一个泄漏(甚至两个),尤其是包括我在其中使用的3个左右的外部库(解压缩功能和http服务器)。那么让我们看看我修复这个泄漏的过程。
第 1 阶段:泄漏告诉我有泄漏
(来源:enrogue.com)
嗯,这很有趣。由于我的应用程序是多线程的,我的第一个想法是我忘记在某处放置 NSAutoreleasePool,但在检查了所有正确的位置后,情况不是。我看一下堆栈跟踪。
第 2 阶段:堆栈跟踪
(来源:enrogue.com)
GeneralBlock-160 的两个泄漏具有相同的堆栈跟踪(这很奇怪,因为我将它按“相同的回溯”分组,但无论如何),它们从 thread_assign_default 开始并在 _NSAPDataCreate 下的 malloc 结束。在这两者之间,绝对没有任何东西与我的应用程序相关。这些电话中没有一个是“我的”。所以我做了一些谷歌搜索,以找出这些可能用于什么。
首先,我们有许多显然与线程回调有关的方法,例如进入 NSThread 调用的 POSIX 线程调用。
在这个(倒置的)堆栈跟踪中的 #8-6 处,我们有 +[NSThread exit] 后跟 pthread_exit 和 _pthread_exit 这很有趣,但根据我的经验,我无法确定它是否表示某些特定的案例,或者只是“事情进展如何”。
之后我们有了一个名为_pthread_tsd_cleanup 的线程清理方法——我不确定“tsd”代表什么,但无论如何,我继续前进。
在 #4-#3 我们有:
CA::Transaction::release_thread(void*)
CAPushAutoreleasePool
有趣。我们在这里有Core Animation。那,我已经学会了非常艰难的方法,这意味着我可能正在从后台线程进行UIKit 调用,我不能这样做。最大的问题是在哪里,以及如何。虽然说“你不应该从你的后台线程调用UIKit”可能很容易,但要知道究竟是什么构成了UIKit 调用并不容易。正如您将在这种情况下看到的那样,这远非显而易见。
那么#2-1 的级别太低了,无法真正使用。我想。
我仍然不知道从哪里开始寻找这个内存泄漏。所以我只做我能想到的事情。
第 3 阶段:return galore
建议我们有一个如下所示的调用树:
App start
|
Some init
| \
A init B init - Other case - Fourth case
\ / \
Some case Third case
|
Fifth case
...
应用程序生命周期的大致轮廓,即。简而言之,根据发生的情况,我们有许多应用程序可以采用的路径,并且这些路径中的每一个都包含在不同位置调用的一堆代码。于是我拿出剪刀开始切菜。我一开始靠近“App start”,然后慢慢地沿着这条线向十字路口移动,在那里我只允许一条路。
所以我有
// ...
[fooClass doSomethingAwesome:withThisCoolThing];
// ...
我愿意
// ...
return;
[fooClass doSomethingAwesome:withThisCoolThing];
// ...
然后在设备上安装应用程序,关闭它,alt-tab 到 Instruments,点击 cmd-R,像猴子一样敲击应用程序,寻找泄漏,如果没有任何东西,大概 10 个“循环”后,我的结论是泄漏在代码的下方。可能在fooClass 的doSomethingAwesome: 或下面对fooClass 的调用。
所以我将返回值移到调用fooClass 的下一级并再次测试。如果泄漏现在没有出现,太好了,fooClass 是无辜的。
此方法存在一些问题。
- 内存泄漏对于何时暴露自己往往有点势利。可以这么说,您需要浪漫的音乐和蜡烛,并且在一个地方切割一端有时会导致内存泄漏决定根本不出现。我经常不得不返回,因为在我添加了这行之后就出现了泄漏:
UIImage *a;(显然它本身并没有泄漏) - 为大型程序执行此操作非常缓慢且累人。特别是如果您最终不得不再次备份。
- 很难跟踪。我不断输入
// 17 14.48.25: 3 leaks @ RSx10,它在英文中的意思是“7 月 17 日,14:48.25:当我重复选择该项目 10 次时发生了 3 次泄漏”遍布整个应用程序。乱七八糟,但至少它让我清楚地看到了我在哪里测试过东西以及结果是什么。
这个方法最终把我带到了处理缩略图的类的最底层。该类有两个方法,一个初始化事物,然后对一个单独的方法进行[NSThread detachThreadWithSeparator:] 调用,该方法处理实际图像并在将它们缩小到合适的大小后将它们放入单独的视图中。
有点像这样:
// no leaks if I return here
[NSThread detachNewThreadSelector:@selector(loadThumbnails) toTarget:self withObject:nil];
// leaks appear if I return here
但是如果我进入-loadThumbnails 并通过它走下来,泄漏就会消失并以非常随机的方式出现。在一次广泛的运行中,我会有泄漏,如果我将 return 语句移到下面,例如UIImage *small, *bloated; 我会出现泄漏。简而言之,它非常不稳定。
经过更多测试后,我意识到如果我在应用程序中更快地重新加载内容,泄漏往往会更频繁地出现。经过数小时的痛苦后,我意识到如果这个外部线程在我加载另一个会话之前没有完成执行(因此创建了第二个缩略图类并丢弃了这个),就会出现泄漏。
这是一个很好的线索。所以我添加了一个名为worldExists 的BOOL,它在新会话启动后立即设置为NO,然后开始在-loadThumbnails 的for 循环中添加
if (worldExists) [action]
if (worldExists) [action 2]
// ...
并确保在我发现!worldExists 后立即退出循环。但泄漏仍然存在。
return 方法在非常不稳定的地方显示泄漏。不经意间,就出现了。
所以我尝试在-loadThumbnails 的最顶部添加这个:
for (int i = 0; i < 50 && worldExists; i++) {
[NSThread sleepForTimeInterval:0.1f];
}
return;
信不信由你,但如果我在 5 秒内加载新会话,泄漏实际上就会出现。
最后,我在-dealloc 中为缩略图类设置了一个断点。堆栈跟踪如下所示:
#0 -[Thumbs dealloc] (self=0x162ec0, _cmd=0x32299664) at /Users/me/Documents/myapp/Classes/Thumbs.m:28
#1 0x32c0571a in -[NSObject release] ()
#2 0x32b824d0 in __NSFinalizeThreadData ()
#3 0x30c3e598 in _pthread_tsd_cleanup ()
#4 0x30c3e2b2 in _pthread_exit ()
#5 0x30c3e216 in pthread_exit ()
#6 0x32b15ffe in +[NSThread exit] ()
#7 0x32b81d16 in __NSThread__main__ ()
#8 0x30c8f78c in _pthread_start ()
#9 0x30c85078 in thread_start ()
嗯……看起来还不错。如果我等到 -loadThumbnails 方法完成,跟踪看起来会有所不同:
#0 -[Thumbs dealloc] (self=0x194880, _cmd=0x32299664) at /Users/me/Documents/myapp/Classes/Thumbs.m:26
#1 0x32c0571a in -[NSObject release] ()
#2 0x00009556 in -[WorldLoader dealloc] (self=0x192ba0, _cmd=0x32299664) at /Users/me/Documents/myapp/Classes/WorldLoader.m:33
#3 0x32c0571a in -[NSObject release] ()
#4 0x000045b2 in -[WorldViewController setupWorldWithPath:] (self=0x11e9d0, _cmd=0x3fee0, path=0x4cb84) at /Users/me/Documents/myapp/Classes/WorldViewController.m:98
#5 0x32c29ffa in -[NSObject performSelector:withObject:] ()
#6 0x32b81ece in __NSThreadPerformPerform ()
#7 0x32c23c14 in CFRunLoopRunSpecific ()
#8 0x32c234e0 in CFRunLoopRunInMode ()
#9 0x30d620da in GSEventRunModal ()
#10 0x30d62186 in GSEventRun ()
#11 0x314d54c8 in -[UIApplication _run] ()
#12 0x314d39f2 in UIApplicationMain ()
#13 0x00002fd2 in main (argc=1, argv=0x2ffff5dc) at /Users/me/Documents/myapp/main.m:14
事实上,完全不同。在这一点上,我仍然一无所知,信不信由你,但我终于弄清楚发生了什么。
问题如下:当我在缩略图加载器中执行[NSThread detachNewThreadSelector:] 时,NSThread 会保留对象,直到线程用完。如果在我加载另一个会话之前缩略图加载没有完成,我在缩略图加载器上的所有保留都将被释放,但由于线程仍在运行,NSThread 使其保持活动状态。
只要线程从-loadThumbnails 返回,NSThread 就会释放它,它会命中 0 保留并直接进入 -dealloc...同时仍在后台线程中。
然后当我调用[super dealloc] 时,UIView 乖乖地尝试将自己从其父视图中移除,这是一个后台线程上的UIKit 调用。因此发生泄漏。
我想出的解决方案是用另外两种方法包装加载器。我将其重命名为-_loadThumbnails,然后执行以下操作:
[self retain]; // <-- added this before the detaching
[NSThread detachNewThreadSelector:@selector(loadThumbnails) toTarget:self withObject:nil];
// added these two new methods
- (void)doneLoadingThumbnails
{
[self release];
}
-(void)loadThumbnails
{
[self _loadThumbnails];
[self performSelectorOnMainThread:@selector(doneLoadingThumbnails) withObject:nil waitUntilDone:NO];
}
说了这么多(我说了很多 - 对此感到抱歉),最大的问题是:如果不经历上述所有事情,你如何解决这些奇怪的事情?
我在上述过程中遗漏了什么推理? 你在什么时候意识到问题出在哪里?我的方法中有哪些多余的步骤?我可以以某种方式跳过第 3 阶段 (return galore),或者减少它,或者提高它的效率吗?
我知道这个问题是模糊而庞大的,但整个概念是模糊而庞大的。我不是要你教我如何找到泄漏(我可以做到……这非常非常痛苦),我是在问人们倾向于做什么来减少处理时间。询问人们“您如何发现泄漏?”是不可能的,因为有很多不同的种类。但是我倾向于遇到问题的一种类型是看起来像上面的那种,在你的实际应用程序中没有调用。
您使用什么流程来更有效地追踪它?
【问题讨论】:
标签: objective-c xcode memory-leaks performance instruments