Главная > Apple, Coding > Ищем утечки памяти в iPhone приложениях

Ищем утечки памяти в iPhone приложениях

Одним из пожалуй главных условий публикации приложений в AppStore является отсутствие утечек памяти. В данный момент занимаюсь подготовкой своего третьего официального iPhone-приложения (и похоже до нового года не успеваю – Apple торжественно объявили, что iTunesconnect уходит на рождественские каникулы). Предыдущие приложения прошли валидацию на соотвтетствие требованиям с первого раза, хотя у коллег я встречал в блогах или на форумах упоминания о том, что их приложения заворачивали в том числе и из-за того, что в них обнаруживались утечки. То есть специалисты Apple Inc. тестируют все публикуемые приложения на соответствие дизайну, и, если обнаруживают какое-либо несоответствие, возвращают приложение из статуса On Review в Rejected. Следовательно, приложение необходимо протестировать перед публикацией, и в первую очередь на предмет утечек памяти. Прочитав официальный документ Instruments User Guide, нашел в нем очень скромный мануал по работе с приложениями из пакета Instruments (именно в нем находится тулза для обнаружения утечек). Поиск в рунете также не дал каких-то вразумительных результатов. Поскольку как-раз занимаюсь подготовкой приложения, решил накидать мануальчик (вдруг кому пригодится когда, или мне самому). И так. Что такое утечка памяти, и почему так важно, чтобы их не было? Историческим способом структурирования памяти программы является куча. Куча – это область памяти, в которой менеджер памяти хранит ссылки на объекты в памяти. Если, предположим, нам нужно зарезервировать кусок памяти под какую-то переменную или класс, мы вызываем, например, сишную функцию malloc, при этом менеджер памяти проверяет, есть ли еще у программы запрошенное количество памяти. Если нет, функция резервирования возвращает ошибку, если есть, добавляет в кучу информацию о выделенной памяти и возвращает указатель на эту запись. Если создается новый объект какого-либо класса, конструктор класса вычисляет, сколько для объекта данного класса потребуется памяти. Далее вызывает функцию резервирования, которая возвращает ссылку на участок памяти, после чего конструктор создает экземпляр класса путем копирования образа класса. Этот образ представляет на самом деле собой структуру данных record (struct), в элементах которых располагаются instance variables. Воспользовавшись объектом, мы должны его разрушить в памяти и освободить место в куче. Если мы забудем это сделать, этот объект так и останется в памяти. А что, если этот объект создается в цикле? А что, если в бесконечном цикле? Например, внутри функции потока. Рано или поздно программа упадет с ошибкой на тему нехватки ресурсов. Как понять, если в нашем приложении утечка? Во-первых, стоит проанализировать сам код. Особенное внимание обратить на создание объектов и присваивание их переменным с локальной областью видимости. Посмотрим на примере. Имеем iPhone-приложение (скачать можно в конце поста) с ViewController’ом, внутри которого создается объект «дырявого» класса.
DiscoverMemLeaksViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
 
	// This is the string that leaks
	leakyString = [[NSString alloc] initWithUTF8String:"I'm a leaky string."];
	LeakedClass * leakyClass = [[LeakedClass alloc] init];
	[leakyClass release];
	[self doSomethingNow];
}
 
- (void) doSomethingNow {
	// Allocate a new string and assign it to a pointer that hasn't been released
	leakyString = [[NSString alloc] initWithUTF8String:"Look, another alloc, but no release!"];
}

LeakedClass.m

-(id) init {
	if ([super init]) {
		lObj = [[LeakedObject alloc] init];
	}
	return self;
}
 
- (void)dealloc {
    [super dealloc];
}

В этом примере присутствует аж две учтечки, и сейчас мы их найдем с помошью Instruments. Запускаем Инструменты – можно через Spotlight, можно через Finder (/Developer/Applications/Instruments). В появившемся окне выбираем iPhone в сайдбаре, а в правом фрейме Leaks (Утечки)

Кликаем Choose, получаем

Далее нужно запустить приложение на отладку и присоединится к нему. Это можно сделать двумя способами: запустить приложение на девайсе или в симуляторе – инструментам не принципиально. Если на девайсе, убеждаемся, что оный подключен к компьютеру, раскрываем выпадающиq список Launch Executable, находим свой гаджет в списке, ставим на него галку, открываем снова список и в подменю Launch Executable выбираем тестирумемое приложение – понятное дело, оно должно быть инсталлировано на девайс

Под симулятором все так же, только первый раз чекаем My Computer, а далее в Launch Executable выбираем Choose Executable,

там листаем по файловой системе нашего мака до образа приложения.

Нажимаем кнопку Record, что приводит к запуску приложения.

Остлеживание утечек производится путем сканирования кучи и поиском объектов, на которые отсутствуют какие-либо ссылки. В появившемся тулбаре можно установить периодичность замеров – по умолчанию период 10 секунд. Можно отключить автоматический замер.

Буквально после первого же замера в таблице появляется первый улов – две утечки. Мы видим их также и на временной шакле.

Нажимаем маленьку кнопочку в статус-баре

И получаем более детальную информацию об утечке:

В нижней таблице выбираем одну из утечек и справа получаем стек вызовов. Находим последнюю строчку, которая относится к нашему проекту – DiscoverMemLeadkViewController viewDidLoad – в этом методе было последнее обращение к утекшему объекту типа NSString. Дабл-клик на этом вызове, и… xCode переносит нас в наш исходник к этому вызову.

Казалось бы, откуда здесь быть утечке? Ведь в dealloc мы освобождаем объект:

[mMyLeakyString release];

Все дело в вызове doSomethingNow – там создается новый объект NSString, и присваивается переменной leakyString. При этом предыдущий объект никуда не делся, а остался в памяти. Таким образом в dealloc мы освобождаем второй созданный объект NSString. Чтобы устранить эту утечку, разумнее всего перед созданием второго объекта внутри doSomethingNow:

 

- (void) doSomethingNow {
	[leakyStringleakyString release];
	leakyString = [[NSString alloc] initWithUTF8String:"Look, another alloc, but no release!"];
}

Теперь, что касается второй утечки. Кликаем для детализации вторую строчку.

Говорит, что утечка происходит в методе init. Не смотря на то, что сразу после создания экземпляра LeakyClass мы его освобождаем, Инструменты находят в нем утечку. Дабл клик, и мы оказываемся на строчке, в которой создается объект LeakedObject. Создать то создали, но ведь не освободили.

Вот пример того, как корректо освобождать объект LeakyObject. Подкорректируем dealloc у LeakyClass:

 

- (void)dealloc {
	if (lObj != nil) {
		[lObj release];
		lObj = nil;
	}
    [super dealloc];
}

Запускаем снова и видим, что утечек больше нет. И таким образом прогоняем всю программу, не пропускаем ни одной утечки.

P.S. Забавно вышло, сейчас заливал на сервер тестовое приложение, которое я назвал DiscoverMemLeaks (обнаружить утечки памяти), и до меня дошла этимология слова Discover. Способ его формирования очень схож с его русским аналогом — обнаружить. Обнаружить — значит сделать что-то наружу. Discover сотоит из двух элементов: dis — один из так называемых префиксов отрицания, и cover — покрывать, крышка. Discover означает лишить чего-то покрытия — то есть, открыть, обнаружить.

  1. Пока что нет комментариев.
Подписаться на комментарии по RSS