До появления механизма ARC основное место, где программисты могли совершить ошибку при управлении памятью, было связано с переменными экземпляра.

Управление памятью, выделяемой для временных переменных экземпляра в одном методе, осуществляется довольно просто, поскольку все тело метода видно сразу, и остается лишь следовать золотому правилу управления памятью, уравновешивая каждую команду retain, alloc или сору командой release. Если из этого метода возвращается объект с увеличенным подсчетом сохраненных ссылок, то эти команды следует уравновешивать командой autorelease. Однако переменные экземпляра усложняют управление памятью по многим причинам, включая следующие:

Переменные экземпляра являются сохраняемыми

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

Управление памятью для переменных экземпляра осуществляется из разных мест в прикладном коде

Управление памятью, выделяемой для переменных экземпляра, может быть распределено среди нескольких различных методов, что затрудняет достижение правильной работы прикладного кода и отладку, если ofo работает неправильно. Так, если сохранить значение, присвоенное переменной экземпляра, то в дальнейшем его придется освободить, согласуясь с золотым правилом управления памятью, чтобы исключить ее утечку, но сделать это нужно в каком-нибудь другом методе.

Переменные экземпляра могут вам не принадлежать

Вам нередко придется присваивать или получать значение из переменной экземпляра, принадлежащей другому объекту. Допустим, вы разделяете общий доступ к значению вместе с каким-нибудь другим сохраняемым объектом. Если этот объект прекратит свое существование и освободит свои переменные экземпляра, а у вас имеется указатель на значение в переменной экземпляра этого объекта и вы не подтвердили свои права на владение данным значением, сохранив его, то в конечном итоге в вашем распоряжении может оказаться висячий указатель.

Следовательно, до появления механизма ARC простая, на первый взгляд, операция присваивания объекта переменной экземпляра могла быть сопряжена с большим риском. Рассмотрим в качестве примера следующий фрагмент кода:

NSMutableDictionary* d = [NSMutableDictionary dictionary],-// ... здесь следует код заполнения словаря по переменной d ... self->_theData = d;

// в коде без поддержки механизма ARC это была бы неудачная идея!

 

До появления механизма ARC такой код мог быть чреват серьезной ошибкой. По завершении этого кода остается сохраняемый указатель на автоматически освобождаемый объект, права на владение которым вообще отсутствуют. Он может исчезнуть, оставив после себя висячий указатель. Очевидно, что в качестве выхода из положения следует сохранить данный объект во время его присваивания переменной экземпляра. Этого можно добиться следующим образом:

[d retain]; self->_theData = d;

С другой стороны, то же самое можно сделать так:

self->_theData = d;

[self->_theData retain];

Поскольку метод retain возвращает объект, которому он посылается, то сохранить этот объект можно и так, как показано ниже.

self->_theData - [d retain];

Однако ни один из этих способов на самом деЛе нельзя считать удовлетворительным. Рассмотрим, в частности, те хлопоты, которые может доставить присваивание другого значения переменной экземпляра self->_theData. В этом случае нужно будет не забыть освободить объект, на который уже имеется указатель, чтобы уравновесить сохранение этого объекта, а также сохранить следующее значение. Поэтому было бы намного лучше инкапсулировать управление памятью, выделяемой для данной переменной экземпляра, в -методе доступа, а точнее — в set-методе. Благодаря этому управление памятью окажется корректным, поскольку оно будет всегда осуществляться через метод доступа. В примере 12.4 показано, каким образом может выглядеть стандартный образец такого метода.

 

Пример 12А Простой сохраняющий set-метод

- (void) setTheData: (NSMutableArray*) value { if (self->_theData != value) {

[self->_theData release]; self->_theData = [value retain];

}

)

В примере 12.4 освобождается объект, на который в настоящий момент указывает переменная экземпляра, и если этот объект оказывается пустым (nil), то никакого вреда такая операция не приносит. Кроме того, входящее значение сохраняется перед его присваиванием переменной экземпляра, и если это значение оказывается пустым (nil), то и такая операция никакого вреда не приносит. Проверка на соответствие входящего значения тому же самому объекту, на который указывает переменная экземпляра, делается не только из соображений экономии, но и потому, что если освободить данный объект, то он может исчезнуть, а значение превратится в висячий указатель, который (страшно подумать) может быть затем присвоен переменной экземпляра self->_theData.

Устанавливающий метод доступа теперь автоматически управляет памятью корректно, при условии, что он всегда применяется для установки переменной экземпляра. В этом заключается одна из основных причин, по которым методы доступа так важны! Следовательно, присваивание переменной экземпляра в первоначальном варианте кода должно выглядеть следующим образом:

 

[self setTheData: d] ;

 

Обратите внимание на то, что этот метод установки можно также использовать для освобождения значения переменной экземпляра и установки пустого значения в этой переменной. Благодаря этому исключается появление висячего указателя, и все это делается одновременно, как показано ниже.

 

[self setTheData: nil];

 

Применение метода доступа для управления памятью дает еще одно преимущество. Впрочем, управление памятью для данной переменной экземпляра еще не завершено. Нужно еще не забыть освободить объект, на который указывала данная переменная экземпляра в последний момент перед тем, как прекратит свое существование объект этого экземпляра. В противном случае, если данная переменная экземпляра указывает на сохранившийся объект, произойдет утечка памяти. В качестве принимаемой в последний момент меры обычно служит метод dealloc из класса NSObject (см. главу 10), который вызывается, когда объект прекращает свое существование.

- (void) dealloc (

[self->_theData release];

[super dealloc];

}

 

Ни в коем случае не вызывайте метод dealloc в своем коде, кроме последнего вызова по ссылке super в переопределяемом варианте метода dealloc. Если действует механизм ARC, то вызывать метод dealloc нельзя, и это еще один пример того, как механизм ARC уберегает вас от собственных ошибок.

 

Как видите, до появления механизма ARC управление памятью, выделяемой для переменных экземпляров объектов, требовало немалых трудов! Для этого нужно было не только правильно написать устанавливающие методы доступа к переменным экземпляра, но и обеспечить вызов метода dealloc для каждого объекта, чтобы освободить каждую переменную экземпляра, значение которой было сохранено. Это, очевидно, была еще одна весьма вероятная возможность совершить ошибку.

Однако это еще не все! Как насчет инициализатора, устанавливающего значение переменной экземпляра? В этом случае требуется управление памятью. Здесь, к сожалению, на помощью не придет метод доступа, как, впрочем, и для обращения к собственным переменным экземпляра в методе dealloc. Следовательно, собственные методы доступа нельзя применять для обращения к собственным переменным экземпляра в инициализаторе (см. главу 5). Дело в том, что, с одной стороны, объект еще не полностью сформирован, а с другой — метод доступа может иметь другие побочные эффекты.

За иллюстрацией обратимся к примеру инициализатора из главы 5 (см. пример 5.3). Перепишем его исходный код таким образом, чтобы инициализировать объект Dog по имени. Такая возможность не рассматривалась в главе 5 потому, что символьная строка является объектом, а следовательно, выделяемая для нее память подлежит управлению! Теперь представьте, что имеется переменная экземпляра _name, значением которой является объект типа NSString, и что требуется инициализатор, позволяющий вызывающему коду передать значение этой переменной. В примере 12.5 показано, как это можно реализовать непосредственно в коде.

Пример 12.5. Простой пример инициализатора, сохраняющего переменную экземпляра

- (id) initWithName:            (NSString*) s (

self = [super init]; if (self) {

self->_name = [s retain];

)

return self;

)

На самом деле более вероятным для объекта типа NSString было бы копирование, а не только сохранение. Дело в том, что у класса NSString имеется изменяемый подкласс NSMutableString, а следовательно, какой-нибудь другой объект может вызвать метод initWithName: и передать изменяемую символьную строку, ссылку на которую он все еще хранит, а затем видоизменить ее таким образом, чтобы кличка собаки (объект Dog) незаметно изменилась. В примере 12.6 показано, каким образом исходный код инициализатора будет выглядеть на этот раз.

 

Пример 12.6. Простой пример инициализатора, копирующего переменную экземпляра

- (id) initWithName: (NSString*) s {

self = [super init]; if (self) (

self->_name - [s copy];

)

return self;

)

В примере 12.6 существующее значение переменной экземпляра name не освобождается. Безусловно, она не указывает на какое-то конкретное предыдущее значение, поскольку такое значение отсутствует. Следовательно, освобождать значение этой переменной экземпляра не имеет смысла.

Таким образом, управление памятью, выделяемой для переменной экземпляра без применения механизма ARC, может быть осуществлено в трех местах: в инициализаторе, методе установки и методе dealloc. Это довольно распространенная архитектура! Для того чтобы заглянуть во многие места и проверить в них правильность и согласованность управления памятью, требуется немало труда, а это зачастую чревато ошибками. Однако именно так приходится поступать, если механизм ARC не применяется (хотя в языке Objective-C имеется, по крайней мере, возможность для автоматического формирования методов доступа, как будет показано далее в этой главе).


 

 

 

Добавить комментарий