我花了整整一个星期的时间来追踪和重击记忆力泄露的源头,而我在那一周的另一端却有点茫然。
   
    必须有更好的方法来做到这一点,
   
   是我唯一能想到的,所以我想是时候问问这个相当沉重的话题了。
  
  
   结果这篇文章相当大。为此道歉,尽管我认为在这个案例中,尽可能详细地解释细节是有必要的。很明显,因为它给了你我为找到这个家伙所做的所有事情的全貌,这是很多。仅这个bug就花了我大约3个10多小时的时间来追踪…
  
  
   
    当我追捕泄密者时
   
  
  
   当我寻找漏洞时,我倾向于分阶段进行,如果问题在早期阶段无法解决,我会逐步“深入”到问题中。这些阶段从泄漏开始,告诉我有一个问题。
  
  
   在这个特定的情况下(这是一个例子;这个bug已经解决;我不是在寻求解决这个bug的答案,而是在寻求改善我所处过程的方法
   
    找到
   
   这个bug),我在一个相当大的多线程应用程序中发现了一个漏洞(两个甚至两个),特别是包括我在其中使用的3个左右的外部库(解压特性和HTTP服务器)。让我们看看我修复这个漏洞的过程。
  
  
   
    第一阶段:泄漏告诉我有泄漏
   
  
  
   
    Leaks with 2 GeneralBlock-160 leaks at 160 bytes in Foundation's NSPushAutoreleasePool http://enrogue.com/so/leaks.png
   
  
  
   嗯,那很有趣。由于我的应用程序是多线程的,我的第一个想法是我忘记了
   
    NSAutoreleasePool
   
   在某个地方,但在检查了所有合适的地方之后,这是
   
    不
   
   案件。我看看堆栈跟踪。
  
  
   
    阶段2:堆栈跟踪
   
  
  
   
    The stack trace for the leak http://enrogue.com/so/leaks_extended_detail.png
   
  
  
   两者都
   
    GeneralBlock-160
   
   泄漏具有相同的堆栈跟踪(这很奇怪,因为我将其按“相同的回溯”分组,但无论如何),从
   
    thread_assign_default
   
   结束于
   
    malloc
   
   在下面
   
    _NSAPDataCreate
   
   . 在两者之间,绝对没有任何东西与我的应用程序相关。这些电话中没有一个是“我的”。所以我四处搜索,想弄清楚这些可能用来做什么。
  
  
   首先,我们有许多方法显然与线程回调有关,例如POSIX线程调用进入nsthread调用。
  
  
   在这个(反向)堆栈跟踪中的8-6处,我们有
   
    +[NSThread exit]
   
   然后
   
    pthread_exit
   
   和
   
    _pthread_exit
   
   这很有意思,但根据我的经验,我不能真正判断它是指某个特定的案例,还是仅仅是“事情进展如何”。
  
  
   之后,我们有一个线程清理方法调用
   
    _pthread_tsd_cleanup
   
   --不管“tsd”代表什么,我不确定,但不管怎样,我继续前进。
  
  
   在4-3我们有:
  
  CA::Transaction::release_thread(void*)
CAPushAutoreleasePool
  
   有趣。我们有
   
    Core Animation
   
   在这里。我学到了很难的方法,这意味着我可能正在做
   
    UIKit
   
   从后台线程调用,我不能这样做。最大的问题是在哪里,如何。虽然说“你不应该打电话
   
    乌伊特
   
   从旧的背景线来看,“要知道什么才是真正的
   
    乌伊特
   
   打电话。正如您在本例中看到的,这一点还不明显。
  
  
   然后2-1被证明是太低的水平,任何真正的用途。我想。
  
  
   我仍然不知道从哪里开始寻找这个记忆泄漏。所以我只做我能想到的事。
  
  
   
    第3阶段:
    
     return
    
    加洛尔
   
  
  
   建议我们有一个这样的呼叫树:
  
  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];
// ...
  
   然后在设备上安装应用程序,关闭它,切换到仪器,点击Cmd-R,像猴子一样敲打应用程序,寻找漏洞,在大约10个“周期”后,如果没有任何东西,我得出结论,漏洞是进一步向下代码。可能在
   
    fooClass
   
   的
   
    doSomethingAwesome:
   
   或者在电话下面
   
    足类
   
   .
  
  
   所以我把它移到电话下面一步
   
    足类
   
   再次测试。如果泄漏现在没有出现,太好了,
   
    足类
   
   是无辜的
  
  
   这个方法有几个问题。
  
  
   - 
    记忆泄漏往往有点势利于何时暴露自己。比如说,你需要浪漫的音乐和蜡烛,在一个地方剪下一头有时会导致记忆泄露,决定根本不出现。我经常不得不去
    
     后面
    
    因为在我加上,比如说,这条线之后,泄漏就出现了:
    
     UIImage *a;
    
    (显然没有泄漏)
    
   - 
    为一个大项目做起来既慢又累。尤其是当你不得不再次备份的时候。
   
 
   - 
    很难跟踪。我一直在穿
    
     // 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
  
   但如果我进入
   
    -loadThumbnails
   
   然后踩下它,泄漏就会消失,以一种非常随机的方式出现。在一次大范围的运行中,我会有漏洞,如果我把返回语句移到下面,例如
   
    UIImage *small, *bloated;
   
   我会发现漏洞的。简而言之,这是非常不稳定的。
  
  
   经过更多的测试,我意识到如果我在应用程序中更快地重新加载东西,那么泄漏会更频繁地出现。在经历了许多小时的痛苦之后,我意识到如果这个外部线程在我加载另一个会话之前没有完成执行(从而创建了第二个缩略图类并丢弃了这个类),那么就会出现泄漏。
  
  
   这是个很好的线索。所以我添加了一个
   
    BOOL
   
   打电话
   
    worldExists
   
   设置为
   
    NO
   
   一旦启动新会话,然后开始喷洒
   
    -加载缩略图
   
   的
   
    for
   
   循环带
  
  if (worldExists) [action]
if (worldExists) [action 2]
// ...
  
   一旦我发现
   
    !worldExists
   
   . 但泄漏依然存在。
  
  
   以及
   
    返回
   
   方法是在非常不稳定的地方显示泄漏。随机地,它出现了。
  
  
   所以我试着在
   
    -加载缩略图
   
   :
  
  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 ()
  
   好。。。看起来还不错。如果我等到
   
    -加载缩略图
   
   方法已完成,但跟踪看起来不同:
  
  #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
   
   保留对象,直到线程用完。如果在加载另一个会话之前缩略图加载没有完成,那么缩略图加载程序上的所有保留都将被释放,但由于线程仍在运行,
   
    NS线程
   
   保持它活着。
  
  
   一旦线程从
   
    -加载缩略图
   
   ,
   
    NS线程
   
   释放它,点击0保留并直接进入
   
    -DELOLLC
   
   …
   
    仍在后台线程中时
   
   .
  
  
   当我打电话的时候
   
    [super dealloc]
   
   ,
   
    UIView
   
   顺从地试图将自己从它的超视界中移除,这是一个
   
    乌伊特
   
   调用后台线程。因此会发生泄漏。
  
  
   我提出的解决这个问题的方法是用另外两种方法包装装载机。我把它改名为
   
    -_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];
}
  
   所有这些都说了(我说了很多——对不起),最大的问题是:
   
    你是如何在不经历上述所有事情的情况下解决这些奇怪的事情的?
   
  
  
   在上述过程中,我遗漏了什么推理?在什么时候
   
    你
   
   意识到问题在哪里?我的方法中有哪些冗余步骤?我能跳过第三阶段吗(
   
    返回
   
   加洛尔)不知何故,还是削减开支,还是提高效率?
  
  
   我知道这个问题是,嗯,模糊和巨大的,但整个概念是模糊和巨大的。我不是要你教我如何找到漏洞(我能做到…只是非常非常非常痛苦),我在问人们为了减少处理时间会做些什么。问人们“你如何发现漏洞?”是不可能的,因为有很多不同的种类。但我有问题的一种类型是看起来像上面的那种,在你的实际应用程序中没有调用。
  
  
   您使用什么过程来更有效地跟踪它?