Пережеванные выкладки программиста

или же трудные будни ленивца-скурпулезы

Previous Entry Share Next Entry
Работа с памятью в Objective-C. Аналогия с C/C++. Часть 1.
mylogo, xydan, xydan83_logo
xydan

Цель
Цель данной статьи - помочь пройти не такой уж легкий этап в восприятии особенностей языка Objective-C по управлению памятью. При написании статьи я прежде всего ориентировался на людей, знающих C/C++ и желающих овладеть знаниями Objective-C. В данной статье не будет рассматриваться синтаксис языка, способы передачи сообщений, вызов методов и тд. Только по теме.

На самом деле, для многих программистов под Си/++ достаточно трудным этапом является восприятие по работе и организации объектов в памяти на языке от Apple. Хотя есть множество различных статей в интернете по поводу управления памятью в программах на Obj-C, при написании этой статьи я решил сделать упор на примеры, где объяснялись бы такие важные отличия методов работы с памятью, таких как: alloc, copy, assign, init, retain, release, autorelease от таких привычных нам функций выделения и уничтожения объектов/памяти как malloc(), free() и их производные в Си и new/delete в C++.





Для начала определю некоторые ключевые моменты:

1. Программа-пример на Objective-C выполнялись в ОС MacOs 10.7 в среде XCode 4.2;

2. Программа-пример на C/C++ выполнялась в ОС Windows XP в среде C++ Builder 2009.


Язык Objective-C полностью поддерживается компилятором GCC (http://ru.wikipedia.org/wiki/GNU_Compiler_Collection), поэтому примеры под этот язык можно выполнять на любом компьютере, под любую ОС, где запуститься GCC (Windows, Linux, Unix). До версии XCode 4 единственным компилятором в этой среде являлся измененный GCC, теперь XCode поддерживает по умолчанию другой компилятор - LLVM (http://ru.wikipedia.org/wiki/Low_Level_Virtual_Machine).

Поскольку язык Obj-C всетаки наиболее популярен в MAC среде, то буду ориентироваться на то, что Вы собираетесь проектировать программы для MACOs/IOs в среде Xcode (еще одна популярная среда используемая под маки - Qt(http://ru.wikipedia.org/wiki/Qt)).

Средства управления памятью в XCode:

В данной среде разработки под MAC в последней (XCode 4.2) версии поддерживаются, грубо говоря, 3 вида настроек компиляции для управления памятью в программе.

  1. Классическое управление, связанное с ручным выделением памяти, ручным или полуавтоматическим удалением объектов из памяти;

  2. «Модернизированное» классическое управление. По сути это тоже самое, что и Классическое управление, но к нему добавлен такой механизм, как автоматический подсчет ссылок (Automatic references count - ARC). Этот метод доступен только начиная с XCode 4.2 и только при использовании компилятора LLVM;

  3. Garbage Collector (GB) - сборщик мусора.

А теперь о всех по порядку.

Классическое управление

Этот способ давнишнего управления памятью используется и по сей день, наиболее часто. Суть управления заключается как и в C/C++, выделил память - освободи. Вот только средства выделения немного разные. Разберемся в чем суть.

Поскольку Obj-C является объектно-ориентированным языком, то классы и объекты - это то, на чем держиться модель памяти в данном языке. Вы вполне можете выделять память и освобождать обычными классическими Си-функциями, такими как malloc(...) и free(), но к объекто-ориентированному подходу это не имеет никакого отношения.

Теперь более подробно, как выделяется память в Objective-C.

Выделение памяти

Для выделения памяти объектам есть такие методы, как alloc, new, init.

Давайте сравним их с сишными вариантами:

alloc - выделяет память для объектов аналогично оператору new в C++;

init - по сути это метод инициализации, он сравним с конструктором в C++, но запускается вручную. Его можно переопределить, так же как и конструктор в C++;

new - посути это тоже самое, что alloc и init в одном флаконе.

А теперь, как выделить память для какого либо объекта класса? Возьмем к примеру строковой класс NSMutableString.

Способ 1 - NSMutableString *str = [[ NSMutableString alloc] init];

Способ 2 - NSMutableString *str = [ NSMutableString new];

Проведем аналогию, по сравнению с выделением памяти (созданием объекта) в C++. Используем стандартную библиотеку классов;

string *str = new string(); - по сути похоже на второй способ в Objective-C.

Освобождение памяти

Конечно же память надо освободить после использования. Какие есть методы для освобождения памяти? Продолжим пример, написанный выше.

Способ 1 - [str release]; - моментальное освобождение памяти. Если указатель хотите использовать далее, то рекомендуется после этого выполнить следующую конструкцию:

str = nil;

Способ 2 - [str autorelease]; - а вот этот способ требует более детального рассмотрения. Для каждого потока формируется определенный пул, куда записываются методы, память которых вы не хотите освобождать моментально. То есть после выполнения этого оператора, объект какое то время будет доступен.

А теперь как узнать, какое это время? Тут есть два варианта:

1) пока не выполниться команда [pool drain] (в версиях XCode < 4), или пока не выйдете из блока @autorelease pool{ ... };

2) Либо на следующем витке цикла EventMessage. То есть каждый виток цикла сообщений ведет к просмотру объектов в пуле, обозначенных для удаления методом autorelease.

К сожалению ко второму случаю, аналогию в чистом языке C++ со стандартной библиотекой я привести не могу, так как такого пула нет, но скорее всего есть какие-либо сторонние библиотеки, которые реализуют подобный механизм.

А вот аналог первого варианта в C++ всем известен:

delete str;

Так же при создании объекта, можно сделать его изначально как autorelease.

Например можно создать объект следующей строкой:

NSMutableStirng *str1 = [[[NSMutableStirng alloc] init] autorelease];//Такой способ выделит память под новый объект, инициализирует его, а затем изначально сообщит в пул, что удаление объекта кладется на плечи autorelease пула, после чего передаст указатель на объект переменной str1.

В итоге, объект, находящийся по адрессу, записанному в str1 автоматический уничтожится по условию освобождения пула.

Кстати, после такой записи, если вызвать [str1 release]; - то объект сразу же будет уничтожен, но при попытке освободить пул, получим исключение, и последующий вылет из программы с ошибкой удаления несуществующего объекта.

Так же есть такой стандартизированный способ создания авторелизнутых объектов классов как применение Convenience конструкторов.

Еще хотелось бы порекомендовать, по возможности всегда использовать release вместо autorelease при программировании под IOs. Конечно, проще было бы создать объект изначально авторелизнутым и забыть про удаление объекта, эдакий почти сборщик мусора). Но не забывайте, что если пул очищается очень редко, то при программировании для мобильных устройств, где количество памяти ограничено, можно столкнутся с проблемой нехватки памяти еще до то того, как пул будет очищен.

Еще один момент связанный с autorelease pool - если в основном потоке, каждый новый виток циклов сообщений (Event Message) очищает пул, то в потоках реализация переодической очистки пула ложиться на плечи программиста. Поэтому не забывайте иногда делать [pool drain];.
P.s я так понимаю, что в версии XCode начиная с 4.2 [pool drain] делать уже не обязательно, так как там пул освобождается автоматический при выходе операторов за скобки @autorelease pool{ текст программы }.

Cчетчик указателей на объект

Еще одно отличие классов C++ от классов в Objective-C в том, что каждый объект класса имеет счетчик ссылок указателей, которые ссылаются на данный объект. И если мы создаем объект, счетчик увеличивается на еденицу, если мы присваиваем объект еще какому то указателю, используя retain, то счетчик ссылок еще увеличивается на +1, но тут есть строгое правило. Объект не будет удален (при вызовах release или autorelease) до тех пор, пока счетчик ссылок не равен 0. Такой механизм наследуется от супер-класса NSObject (загляните в его заголовочный файл).
В C++ такого механизма нет, если даже 10 указателей ссылаются на объект, то после вызова delete объект будет уничтожен, а попытки обратиться к нему, грозят вызовом исключения.

Но на самом деле не все так просто... Давайте разберем эту особенность по подробнее.

Существуют два способа работы с чужим объектом (по другому это можно назвать - легкая копия (shallow copy):

  1. Взять объект во владение (retain);

  2. Получить просто адресс объекта (assign).

Что из себя это представляет? Первый способ отличается от второго только тем, что увеличивает счетчик ссылок указателей на объект. Второй способо ничего такого не делает и просто присваивает указателю адресс на объект.

Пример:

NSMutableString *str1 = [[NSMutableString alloc]init]; //Создаем объект типа NSMutableString и присваиваем указатель на него переменной str1.

NSMutableString *str2 = [str1 retain]; //Присваиваем указатель на существующий объект переменной str2 и увеличиваем счетчик указателей в объекте на +1;

NSMutableString *str3 = [str1 assign];// Присваиваем указатель на существующий объект переменной str3.

В итоге получается, чтобы объект был уничтожен, нужно вызвать следующие методы:

//Счетчик объекта = 2;

[str1 release];//Отнимаем еденицу от счетчика = -1;

[str2 release];//Отнимаем еще еденицу от счетчика = -1;

//В итоге счетчик == 0, объект немедленно уничтожается.

[str3 release];//Приведет к ошибке, так как счетчик при (assign) не увеличивался.

А теперь приведем аналогию относительно C++:

NSMutableString *str1 = [[NSMutableString alloc]init] - аналогично string *str1 = new string();

NSMutableString *str2 = [str1 retain]; - в C++ такое возможно только при использовании стороннего механизма;

NSMutableString *str3 = [str1 assign]; - аналогично string *str3 = str1; Кстати в Objective-C возможно присвоить указатель и таким образом NSMutableString *str3 = str1; так как этот язык всетаки является расширением обычного Си, и все средства Си вполне доступны.

На счет метода assign сделаю небольшое поясленние. Далеко не все классы реализуют этот метод, так как по сути это обычное присвоение указателя другой переменной, поэтому очень часто проще сделать NSMutableString *str3 = str1;.

Копирование объектов

Метод копирования объектов или по другому - полная копия (deep copy), во многом похож, на копирование объектов в C++, но существуют некоторые особенности.

Метод копирования, как и другие методы управления памятью, описан в супер-классе NSObject как протокол NSCopying, но реализацию этого протокола каждый класс, наследуемый от NSObject должен выполнять самостоятельно. Большинство стандартных классов в библиотеках от Apple поддерживают методы копирования объектов внутри себя. В случае же, если вы хотите дать возможность копирования объектов своего класса (пользовательского), то необходимо самостоятельно реализовать метод копирования по протоколу NSCopying. Реализация очень похожа на C++, так как для копирования пользовательских объектов класса так же реализацию необходимо прописывать самому.

Метод копирования синтаксический записывается как copy.

Пример копирования объекта строки в Objective-C:

NSMutableString *str1 = [[NSMutableString alloc]init];//Создаем объект от класса NSMutableString. Присваиваем указатель на объект переменной str1.

NSMutableString *str2 = [str1 copy]; //В результате этой строчки, в объекте str1 запуститься уже реализованный метод copy (он реализован в классе NSMutableString), который создаст совершенно новый объект типа NSMutableString в памяти, затем перепишет содержимое объекта из str1 в новый объект, а затем уже передаст указатель на объект переменной str2. В итоге str1 и str2 будут совершенно двумя разными объектами в памяти, но имеющими одни данные, то есть, str2 будет в прямом смысле слова копией str1.

Соответственно такие объекты необходимо будет в конце удалить.

[str1 release];

[str2 release];

А теперь хочу обратить Ваше внимание на еще одну отличительную вещь реализации копирования в стандартных фреймворках от Apple. Существуют такие понятия как mutable и immutable объекты. Из названий наверняка понятно, что одни объекты являются изменяемыые, другие нет. Только имеется ввиду изменяемость не самих объектов, а изменяемость данных, которые содержаться в этих объектах.

Например, возьмем два стандартных строковых класса - NSMutableString, я его часто использовал в примерах и NSString. Из названия понятно, что NSMutableString - позволяет изменять данные внутри (имеет методы для этого), а NSString является статичным по отношению к данным. А теперь, к чему я это?

Очень важным фактором является то, что обычно, если идет копирование не изменяемых объектов, то в реализации копирования идет следующая строчка:

-(id)copy

{

return [self retain];

}

Это говорит о том, что полное копирование замещают легким копированием, с захватом владения объектом. Наверное в этом есть смысл. Поэтому операция copy от retain в данном случае не отличается, для неизменяемых объектов. Лучше всего конечно смотреть документацию, чтобы убедиться в способе копирования, реализуемым классом.

Ниже в примере, я покажу на практике эти отличия.

Модернизированное классическое управление

Эта новая фишка появилась в среде программирования XCode относительно недавно, начиная с версии XCode 4.2. В большинстве случаев она включается автоматический при создании нового проекта.

Изначально разрабатывалась для того, чтобы новички программирования под Objective-C забыли про то, что память нужно удалять (вспомним ту же Java).

Главная особенность использования ARC состоит в том, что мы по прежнему создаем объекты сами в ручную, вызывая alloc - init или new, но мы теперь не можем использовать такие методы как release, autorelease или переопределять -(void)dealloc. ARC это будет делать за нас. Механизм конечно не такой как в сборщике мусора, но результаты похожие, хотя даже не знаю, стоит ли их сравнивать. В общем, основываясь на том, сколько вы создали объектов, сколько раз скопировали их, сколько раз вызывали retain, счетчик все это считает, и в коде за нас сам прописывает где нужно освобождение памяти, когда объектами уже никто не пользуется.
По идеи разработчиков, такой метод должен облегчить программирование, и уменьшить ошибки, связанные с утечкой памяти, но по факту, по скольку контроль за уничтожение объектов отдается полностью на откуп компилятору, мне трудно представить, как он понимает, где нужно подставить строчки по осовобождению памяти, так как подставляются они условно.

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

А что же делать, если код ранее был написан в предыдущих версиях среды? Он же теперь не будет компилироваться? Но для этого, разработчики предусмотрели специальную опцию меню, которая облегчит переход с не-ARC, на ARC-ориентированную программу.
Насколько я понимаю, так же ARC можно применять, чуть ли не пофайлово, что дает плавный переход каждого файла исходных текстов программы с промежуточным тестированием. Так же такой подход позволяет использовать ARC в своем проекте, и использовать не-ARC исходники каких либо сторонних библиотек, например API - Cocos2D.

Относительно же C/C++, в стандартных библиотеках и средах таких механизмов я не наблюдал, но возможно сторонние библиотеки позволяют использовать аналогичные механизмы. Поэтому аналогию тут провести не смогу.



  • 1
Ну вообще по хорошему утечек там быть не должно, единственный вариант, когда может быть утечка, это если два объекта зациклить друг на друге, и удалить ссылки на них. Тогда да, будет лик.
Вообще ссылки действительно можно проверить через Leak.
Вот еще рекомендую по этому поводу почитать тут, в частности коменты:
http://www.mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html

Edited at 2012-12-18 12:50 pm (UTC)

спасибо. буду смотреть.
я начинающий.

  • 1
?

Log in

No account? Create an account