我花了整整一个星期的时间来追踪和重击记忆力泄露的源头,而我在那一周的另一端却有点茫然。
必须有更好的方法来做到这一点,
是我唯一能想到的,所以我想是时候问问这个相当沉重的话题了。
结果这篇文章相当大。为此道歉,尽管我认为在这个案例中,尽可能详细地解释细节是有必要的。很明显,因为它给了你我为找到这个家伙所做的所有事情的全貌,这是很多。仅这个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];
}
所有这些都说了(我说了很多——对不起),最大的问题是:
你是如何在不经历上述所有事情的情况下解决这些奇怪的事情的?
在上述过程中,我遗漏了什么推理?在什么时候
你
意识到问题在哪里?我的方法中有哪些冗余步骤?我能跳过第三阶段吗(
返回
加洛尔)不知何故,还是削减开支,还是提高效率?
我知道这个问题是,嗯,模糊和巨大的,但整个概念是模糊和巨大的。我不是要你教我如何找到漏洞(我能做到…只是非常非常非常痛苦),我在问人们为了减少处理时间会做些什么。问人们“你如何发现漏洞?”是不可能的,因为有很多不同的种类。但我有问题的一种类型是看起来像上面的那种,在你的实际应用程序中没有调用。
您使用什么过程来更有效地跟踪它?