内存泄漏狩猎的改进

Kal*_*lle 6 performance xcode memory-leaks objective-c instruments

我花了整整一个星期的时间来追踪和打击头部的内存泄漏,我在那个星期的另一端到达时有点茫然.必须有一个更好的方法来做到这一点,我能想到的所有,所以我认为是时候问这个相当沉重的主题了.

这篇文章结果相当巨大.对此表示歉意,尽管我认为在这种情况下,尽可能彻底地解释细节是有道理的.显然是这样,因为它为你提供了我所做的所有事情的全貌,以找到这个bugger,这很多.这个错误花了我大约三个10多个小时的日子来追踪......

当我打猎泄漏

当我捕获泄漏时,我倾向于分阶段进行,如果在早期阶段无法解决问题,我会更深入地升级到问题中.这些阶段以泄漏开始告诉我有一个问题.

在这种特殊情况下(这是一个例子;错误已经解决;我不是要求解决这个错误的答案,我正在寻找方法来改进我发现错误的过程),我发现了泄漏(两个,偶数)在一个相当大的多线程应用程序中,特别是包括我正在使用的3个左右的外部库(解压缩功能和http服务器).那么让我们看看我解决这个漏洞的过程.

阶段1:泄漏告诉我有泄漏

Foundation的NSPushAutoreleasePool泄漏了2个GeneralBlock-160,泄漏160个字节http://enrogue.com/so/leaks.png

嗯,这很有趣.由于我的应用程序是多线程的,我首先想到的是我忘了把它NSAutoreleasePool放在某个地方,但在检查了所有正确的地方之后,事实并非如此.我看一下堆栈跟踪.

阶段2:堆栈跟踪

泄漏的堆栈跟踪http://enrogue.com/so/leaks_extended_detail.png

两者的GeneralBlock-160泄漏具有相同的堆栈跟踪(其是奇数,因为我有它由"相同的回溯"分组,但无论如何),其开始于thread_assign_default并在结束malloc_NSAPDataCreate.在这两者之间,绝对没有任何与我的应用程序相关的内容.这些电话中没有一个是"我的".所以我做了一些谷歌搜索,以弄清楚这些可能用于什么.

首先,我们有许多方法显然与线程回调有关,例如进入NSThread调用的POSIX线程调用.

在这个(倒置的)堆栈跟踪中的#8-6,我们已经+[NSThread exit]跟随pthread_exit并且_pthread_exit这很有趣,但根据我的经验,我无法确定它是否表明某些特定情况或者它是否仅仅是"事情如何".

之后我们有一个线程清理方法叫做_pthread_tsd_cleanup- 无论"tsd"代表我不确定,但无论如何,我继续前进.

在#4-#3,我们有:

CA::Transaction::release_thread(void*)
CAPushAutoreleasePool
Run Code Online (Sandbox Code Playgroud)

有趣.我们在Core Animation这里.那,我已经学到了很难的方法,意味着我可能正在UIKit从后台线程进行调用,我绝不能这样做.最大的问题是在哪里,以及如何.虽然可能很容易说"你不应该UIKit从你们的背景线索中打电话",但要知道究竟什么构成一个UIKit电话并不容易.正如你在这种情况下所看到的那样,它远非显而易见.

然后#2-1变得太低,没有任何实际用途.我认为.

我仍然不知道哪里开始寻找内存泄漏.所以我做了唯一能想到的事情.

第3阶段:return嘉豪

建议我们有一个看起来像这样的调用树:

App start
    |
Some init
  |      \
A init   B init - Other case - Fourth case
   \     /              \
 Some case            Third case
     |
  Fifth case
   ...
Run Code Online (Sandbox Code Playgroud)

应用程序生命周期的粗略轮廓,即.简而言之,我们有许多路径可以根据发生的事情采取应用程序,并且这些路径中的每一个都包含在各个地方调用的一堆代码.所以我拔出剪刀开始砍.我最初开始接近"App start",然后慢慢向着十字路口向下移动,我只允许一条路径.

所以我有

// ...
[fooClass doSomethingAwesome:withThisCoolThing];
// ...
Run Code Online (Sandbox Code Playgroud)

我做到了

// ...
return;
[fooClass doSomethingAwesome:withThisCoolThing];
// ...
Run Code Online (Sandbox Code Playgroud)

然后在设备上安装应用程序,关闭它,alt-tab到Instruments,点击cmd-R,在应用程序上锤击像猴子一样,寻找泄漏,如果没有什么可能在10个"周期"之后,我得出结论泄漏是进一步的代码.在可能fooClassdoSomethingAwesome:或调用下面fooClass.

所以我把这个返回到调用下面一步fooClass并再次测试.如果现在没有出现泄漏,很棒,fooClass是无辜的.

这种方法存在一些问题.

  1. 内存泄漏往往对何时揭示自己有点势利.你需要浪漫的音乐和蜡烛,所以说,在一个地方切割一端有时会导致内存泄漏决定不出现.我经常不得不回去,因为在我添加了这条线后出现了泄漏:( UIImage *a;显然本身并没有泄漏)
  2. 对于一个大型项目来说,这是非常缓慢和累人的.特别是如果你最终不得不再次备份.
  3. 很难跟踪.我一直// 17 14.48.25: 3 leaks @ RSx10用英语表示"7月17日,14:48.25:3当我反复选择项目10次时发生泄漏"在整个应用程序中撒了一遍.凌乱,但至少它让我清楚地看到我测试的东西和结果是什么.

这种方法最终将我带到了处理缩略图的类的最底层.该类有两个方法,一个初始化事物然后[NSThread detachThreadWithSeparator:]调用一个单独的方法,该方法处理实际图像并在将它们缩小到正确的大小后将它们放入各个视图中.

它有点像这样:

// no leaks if I return here
[NSThread detachNewThreadSelector:@selector(loadThumbnails) toTarget:self withObject:nil];
// leaks appear if I return here
Run Code Online (Sandbox Code Playgroud)

但是,如果我进入-loadThumbnails并逐步通过它,泄漏将消失,并以一种非常随机的方式出现.在一次广泛的运行中,我会泄漏,如果我将返回语句移到下面,例如UIImage *small, *bloated;我会出现泄漏.简而言之,它非常不稳定.

经过一些更多的测试,我意识到如果我在应用程序中更快地重新加载东西,泄漏会更频繁地出现.经过几个小时的痛苦,我意识到如果这个外部线程在我加载另一个会话之前没有完成执行(因此创建了第二个缩略图类并丢弃了这个),就会出现泄漏.

这是一个很好的线索.所以我添加了一个BOOL名为worldExists其设置为NO只要一个新的会话启动了,然后开始撒-loadThumbnailsfor带环

if (worldExists) [action]
if (worldExists) [action 2]
// ...
Run Code Online (Sandbox Code Playgroud)

并且在我发现它之后也确保退出循环!worldExists.但泄漏仍然存在.

并且该return方法显示在非常不稳定的地方泄漏.随机地,它出现了.

所以我尝试在最顶端添加-loadThumbnails:

for (int i = 0; i < 50 && worldExists; i++) {
    [NSThread sleepForTimeInterval:0.1f];
}
return;
Run Code Online (Sandbox Code Playgroud)

不管你信不信,但如果我在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 ()
Run Code Online (Sandbox Code Playgroud)

嗯......看起来并不太糟糕.如果我等到-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
Run Code Online (Sandbox Code Playgroud)

事实上,完全不同.在这一点上,我仍然无能为力,信不信由你,但我终于弄清楚发生了什么.

问题如下:当我[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];
}
Run Code Online (Sandbox Code Playgroud)

所有这一切(我说了很多 - 抱歉),最大的问题是:如果不经历上述所有事情,你如何计算这些奇怪的东西?

我在上述过程中错过了什么推理?在什么时候意识到问题出在哪里?我方法中的冗余步骤是什么?我可以return以某种方式跳过第3阶段(嘉豪),或者将其削减,或者使其更有效率?

我知道这个问题是模糊而庞大的,但整个概念模糊不清.我不是要求你教我如何找到泄漏(我能做到这一点......这只是非常非常痛苦),我问的是人们倾向于减少处理时间.问人们"你怎么发现泄漏?" 是不可能的,因为有很多不同的种类.但是,我倾向于遇到问题的一种类型是看起来像上面的那种,在你的实际应用程序中没有调用.

您使用什么过程来更有效地追踪它?

Jer*_*myP 2

在上述过程中我错过了什么推理?

在多个线程之间共享 UIView 对象应该在您编写代码时就在您的脑海中敲响非常响亮的警钟。