Я просто провел целую неделю, выслеживая утечки памяти над головой, и прибыл на другом конце этой недели, немного ошеломленный. Должен быть лучший способ сделать это, - это все, что я могу думать, и поэтому я решил, что пришло время спросить об этом довольно тяжелом предмете.
Этот пост оказался довольно огромным. Извинения за это, хотя я думаю, что в этом случае, объяснение деталей как можно более тщательно, является оправданным. Явно так, потому что он дает вам полную картину всех вещей, которые я сделал, чтобы найти этого педераста, что было много. Одна эта ошибка заняла у меня примерно три 10+ часовых дня, чтобы выследить ...
Когда я охотюсь на утечки
Когда я выслеживаю утечки, я стараюсь делать это поэтапно, где я углубляюсь в проблему "глубже", если она не разрешима на более ранней стадии. Эти фазы начинаются с утечек, говорящих мне, что есть проблема.
В данном конкретном случае (который является примером; ошибка устранена; я не прошу ответов на решение этой ошибки, я прошу способы улучшить процесс, в котором я нахожу ошибка), я обнаружил утечку (две, даже) в многопоточном приложении, которое довольно велико, особенно включая 3 или около того внешних библиотек, которые я использую в нем (функция распаковки и http-сервер). Итак, давайте посмотрим процесс, где я исправлю эту утечку.
Фаза 1: Утечки говорят мне, что есть утечка
Утечки с 2 утечками GeneralBlock-160 при 160 байтах в NSPushAutoreleasePool Фонда http://enrogue.com/so/leaks.png
Ну, это интересно. Поскольку мое приложение является многопоточным, я сначала подумал, что я забыл поместить куда-нибудь NSAutoreleasePool
, но после проверки во всех нужных местах это , а не . Я смотрю на трассировку стека.
Фаза 2: трассировка стека
трассировка стека для утечки 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
звонки из фонового потока, чего я не должен. Большой вопрос, где и как. Хотя может быть легко сказать «ты не должен звонить UIKit
из своей старой фоновой ветки», не так легко узнать, что именно представляет собой UIKit
звонок. Как вы увидите в этом случае, это далеко не очевидно.
Тогда # 2-1 оказывается слишком низким уровнем для какого-либо реального использования. Я думаю.
Я до сих пор не знаю, с чего начать поиск утечки памяти. Поэтому я делаю единственное, о чем могу думать.
Фаза 3: return
в изобилии
Предположим, у нас есть дерево вызовов, которое выглядит примерно так:
App start
|
Some init
| \
A init B init - Other case - Fourth case
\ / \
Some case Third case
|
Fifth case
...
Грубая схема жизненного цикла приложения. Короче говоря, у нас есть несколько путей, которые приложение может использовать в зависимости от того, что происходит, и каждый из этих путей состоит из набора кода, вызываемого в разных местах. Я вынимаю ножницы и начинаю рубить. Сначала я начинаю близко к «Запуску приложения» и медленно спускаюсь по линии к перекрестку, где разрешаю только один путь.
Итак, у меня есть
// ...
[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
, что по-английски означало «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
и шагнул через него, утечки исчезли бы и появлялись очень случайным образом.При одном большом прогоне у меня были бы утечки, и если бы я переместил оператор return ниже, например UIImage *small, *bloated;
, у меня бы появились утечки.Короче говоря, это было очень странно.
После еще нескольких испытаний я понял, что утечки будут появляться чаще, если я перезагружаю вещи быстрее в приложении.После многих часов боли я понял, что если этот внешний поток не завершит свою работу до того, как загрузит другой сеанс (таким образом, создав второй класс миниатюр и отбросив этот), появится утечка.
Это хорошая подсказка,Поэтому я добавил BOOL
с именем worldExists
, для которого было установлено значение 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
в изобилии), или сократить ее, или сделать ее более эффективной?
Я знаю, что этот вопрос, конечно, расплывчатый и огромный, но вся эта концепция расплывчата и огромна. Я не прошу вас учить меня, как находить утечки (я могу это сделать ... просто очень, очень больно), я спрашиваю, что люди обычно делают, чтобы сократить время процесса Спрашивая людей "как вы находите утечки?" невозможно, потому что есть так много разных видов. Но у меня, как правило, возникают проблемы с типом, похожим на приведенный выше, без вызовов внутри вашего реального приложения.
Какой процесс вы используете, чтобы более эффективно отследить его?