代码之家  ›  专栏  ›  技术社区  ›  Kalle

内存泄漏查找的改进

  •  6
  • Kalle  · 技术社区  · 14 年前

    我花了整整一个星期的时间来追踪和重击记忆力泄露的源头,而我在那一周的另一端却有点茫然。 必须有更好的方法来做到这一点, 是我唯一能想到的,所以我想是时候问问这个相当沉重的话题了。

    结果这篇文章相当大。为此道歉,尽管我认为在这个案例中,尽可能详细地解释细节是有必要的。很明显,因为它给了你我为找到这个家伙所做的所有事情的全貌,这是很多。仅这个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: 或者在电话下面 足类 .

    所以我把它移到电话下面一步 足类 再次测试。如果泄漏现在没有出现,太好了, 足类 是无辜的

    这个方法有几个问题。

    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
    

    但如果我进入 -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];
    }
    

    所有这些都说了(我说了很多——对不起),最大的问题是: 你是如何在不经历上述所有事情的情况下解决这些奇怪的事情的?

    在上述过程中,我遗漏了什么推理?在什么时候 意识到问题在哪里?我的方法中有哪些冗余步骤?我能跳过第三阶段吗( 返回 加洛尔)不知何故,还是削减开支,还是提高效率?

    我知道这个问题是,嗯,模糊和巨大的,但整个概念是模糊和巨大的。我不是要你教我如何找到漏洞(我能做到…只是非常非常非常痛苦),我在问人们为了减少处理时间会做些什么。问人们“你如何发现漏洞?”是不可能的,因为有很多不同的种类。但我有问题的一种类型是看起来像上面的那种,在你的实际应用程序中没有调用。

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

    2 回复  |  直到 14 年前
        1
  •  2
  •   JeremyP    14 年前

    在上述过程中,我遗漏了什么推理?

    在多个线程之间共享uiview对象应该在编写代码的时候就有非常大的警报响起。

        2
  •  2
  •   Community Maksym Gontar    7 年前

    将来,你可以考虑看看其他的 memory leak hunting tools ,像 MallocDebug .