Улучшения в поиске утечек памяти - PullRequest
6 голосов
/ 17 июля 2010

Я просто провел целую неделю, выслеживая утечки памяти над головой, и прибыл на другом конце этой недели, немного ошеломленный. Должен быть лучший способ сделать это, - это все, что я могу думать, и поэтому я решил, что пришло время спросить об этом довольно тяжелом предмете.

Этот пост оказался довольно огромным. Извинения за это, хотя я думаю, что в этом случае, объяснение деталей как можно более тщательно, является оправданным. Явно так, потому что он дает вам полную картину всех вещей, которые я сделал, чтобы найти этого педераста, что было много. Одна эта ошибка заняла у меня примерно три 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 невиновен.

Есть несколько проблем с этим методом.

  1. Утечки памяти, как правило, немного снобиткогда раскрыть себя.Вам нужна романтическая музыка и свечи, так сказать, и разрезание одного конца в одном месте иногда приводит к утечке памяти, решающей вообще не появляться.Мне часто приходилось возвращаться назад , потому что утечка появилась после того, как я добавил, скажем, эту строку: UIImage *a; (которая явно не протекает сама по себе)
  2. Это мучительно медленно и утомительносделать для большой программы.Особенно, если вам в конечном итоге придется сделать резервную копию снова.
  3. Трудно отследить.Я продолжал вводить // 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 в изобилии), или сократить ее, или сделать ее более эффективной?

Я знаю, что этот вопрос, конечно, расплывчатый и огромный, но вся эта концепция расплывчата и огромна. Я не прошу вас учить меня, как находить утечки (я могу это сделать ... просто очень, очень больно), я спрашиваю, что люди обычно делают, чтобы сократить время процесса Спрашивая людей "как вы находите утечки?" невозможно, потому что есть так много разных видов. Но у меня, как правило, возникают проблемы с типом, похожим на приведенный выше, без вызовов внутри вашего реального приложения.

Какой процесс вы используете, чтобы более эффективно отследить его?

Ответы [ 2 ]

2 голосов
/ 24 июля 2010

В будущем вы можете рассмотреть другие средства поиска утечек памяти , например MallocDebug .

2 голосов
/ 18 июля 2010

Какие рассуждения я пропустил в вышеуказанном процессе?

Совместное использование объектов UIView между несколькими потоками должно было иметь очень громкие сигналы тревоги в вашей голове, почти сразу же, как вы писали код.

...