Пользователь
0,0
рейтинг
2 октября 2011 в 22:02

Разработка → Жизненный цикл UIViewController'a

Большинство прикладных приложения под iOS таким или иным образом используют UIViewController'ы. Там где UIKit фрэймворк — там и UIViewController'ы. Их много, они повсюду, они сидят в засадах и выглядывают из-за каждого угла. Поэтому, любой программист под iOS — будь он зеленым новичком, едва ступившим на тропу программирования, либо матерым профессионалом своего дела, должны знать о UIViewController'aх все.

Причиной написания данной поста является то, что, как оказалось, можно преспокойно программировать под iOS полгода, и не знать полностью о жизненном цикле UIViewcontroller'ов. И на небольших проектах это даже получается. Однако, когда приходится иметь дело с серьезным, достаточно большим проектом, то появляются определенные проблемы с нехваткой памяти, «неправильной» и «непонятной» работой контроллеров, пропажей данных, и еще со многими типичными проблемами, о которых будет написано ниже.

Так вот. В данном посте, я еще раз расскажу о жизненном цикле UIViewController'ов, расскажу о том, что и где стоит делать, и в каком случае. Пост ориентирован на разработчиков разных уровней, так что кто-то узнает для себя что-то новое, а кто-то найдет повод отпинать моменты, на которые стоит обратить внимание Junior'ов в команде.

Всех заинтересовавшихся, прошу под кат

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

О UIViewController'е


Является Controller'ом согласно шаблону проектирования MVC. Обеспечивает взаимосвязь модели и отображения. В iOS на него возлагаются задачи, связанные с контролем жизненного цикла view, и отображением UIView в различных ориентациях устройства. Общую информацию можно найти в той же документации.

Жизненный цикл UIViewController'a и его UIView


Сконцентрируемся на методах, которые отвечают именно за жизненный цикл UIViewControllera:

Создание
  • init
  • initWithNibName:

Создание view
  • (BOOL)isViewLoaded
  • loadView
  • viewDidLoad
  • (UIView*) initWithFrame:(CGRect)frame
  • (UIView*) initWithCoder:(NSCoder *)coder

Обработка изменения состояния view
  • viewDidLoad
  • viewWillAppear:(BOOL)animated
  • viewDidAppear:(BOOL)animated
  • viewWillDisappear:(BOOL)animated
  • viewDidDisappear:(BOOL)animated
  • viewDidUnload

Обработка memory warning
  • didReceiveMemoryWarning

Уничтожение
  • viewDidUnload
  • dealloc

Некоторые методы попали сразу в две секции, о них будет сказано отдельно. Еще пару методов относятся к созданию UIView. Им тоже будет уделено немного внимания.

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

Создание


Для создания контроллера, вернее для его инициализации существует два основных метода — init и initWithNibName:(NSString *)nibNameOrNil. На самом деле, метод init, вызовет initWithNibName:, так что можно рассматривать только его.

Логика метода достаточно проста — мы либо находим .xib (.nib) файл и ассоциируем его с UIViewController'ом, либо не ассоциируем. На данном этапе просто запоминается название xib файла, из которого, в случае чего надо подгрузить view.

Есть одно исключение, связанное с наследниками UITableViewController, о котором надо знать (спасибо хабраюзеру muryk):

@interface HabrTestProjectTableViewController : UITableViewController {
}
@end

/* Даже если в проекте есть HabrTestProjectTableViewController.xib
 То он не будет загружен */
HabrTestProjectTableViewController * tableViewController = 
 [[HabrTestProjectTableViewController alloc] init];

/* В случае с наследниками UITableViewController, 
 надо явно указывать имя xib файла */
HabrTestProjectTableViewController * tableViewController = 
 [[HabrTestProjectTableViewController alloc] initWitNib:@"HabrTestProjectTableViewController" bundle:nil];


Важно: На этом этапе еще нет ни самой view, ни аутлетов(Outlets).

@interface HabrTestProjectViewController : UIViewController {
   UIButton * _likeButton;
   NSDictionary * _userDictionary;
}

@property(nonatomic, retain) IBOutlet UIButton * likeButton;

- (IBAction)buttonClicked:(id)sender;

@end

HabrTestProjectViewController * controller =  [[HabrTestProjectViewController alloc] init];
/* Аутлеты и вью еще не создались */
NSLog(@"%@", controller.likeButton); /* (null)  */

/* Супер проверка начинающего программиста*/
if (controller.view) {
  NSLog(@"View was created"); /* Всегда будет выводиться, т.к. controller.view создаст view */
}

/* Более правильная проверка */
if ([controller isViewLoaded]) {
  NSLog(@"View was created"); /* Только в том случае, если view загружена */
}



Созданный UIViewController может, находится в состоянии «без view» достаточно долго, вплоть до удаления его из памяти. Создание view произойдет только после того, как будет вызван метод [viewController view] (viewController.view). Это можете сделать вы, или это может за вас сделать UINavigationController, UITabBarController и многие другие.

Примером, когда UIViewController находится в состоянии «без view», может быть вариант с использованием UITabBarController'а, когда изначально он содержит в себе ссылки на N контроллеров, и только у того, который на данный момент показан на экране, будет загружен view. Все остальные будут ждать, пока пользователь не переключит табу, либо пока криворукий неопытный программист вызовет nonVisibleViewcontroller.view.

Ошибка #1(l): Доступ к аутлетам до загрузки view
Ошибка #2(p): Загрузка view до того, как она на самом деле необходимо, игнорирование метода isViewLoaded
Ошибка #3(p): Создание визуальных компонент в методе initWithNibName

Создание UIView


После того, как произошел доступ к view, возможны три варианта развития событий
  • вызов переопределенного метода loadView
  • вызов не переопределенного метода loadView который либо загрузит view из xib файла, либо создаст пустой UIView

И только после этого, наконец-то будет вызван метод viewDidLoad.

Прежде, чем перейти к следующему пункту, необходимо сказать пару слов о создании UIView, которые находятся внутри xib'ов. Представим ситуацию, что в xib'е находится созданный UIPrettyView, который при своей инициализации должен поставить себе цвет фона в розовый. Так вот, чтобы не тянуть никого ни за какие части тела, скажу сразу — если UIView будет загружена из xib'а, то будет вызван метод инициализации initWithCoder:, в противном случае (при создании в коде) обычно будет вызван метод initWithFrame:
/* не выполнится при загрузке из xib'а */
- (id)initWithFrame:(CGRect)frame {
   ...
   [self setBackgroundColor:[UIColor pinkColor]];
  // [self performInitializations];
   ...
}

/* Выполнится при загрузке из xib'а */
- (id)initWithCoder:(NSCoder *)coder {
   ...
   [self setBackgroundColor:[UIColor pinkColor]];
  // [self performInitializations];
   ...
}

/* Лучше вызвать один метод в обоих случаях */
- (void)performInitializations {
   [self setBackgroundColor:[UIColor pinkColor]];
}


После того, как view загрузилась из xib'а, хорошим местом для «допиливания» элементов дизайна, является метод viewDidLoad. Именно здесь стоит создавать визуальные компоненты, которые, по каким-то причинам, не попали в xib или в метод loadView.

Ошибка #4(l): Путаница с initWithFrame: и initWithCoder:.

Обработка изменения состояния view


Как уже было сказано, viewDidLoad, является наилучшим местом для продолжения инициализации контроллера. С этого метода у начинающих, и не только, программистов начинаются проблемы. Очень часто в виду незнания/непонимания жизненного цикла контроллера, можно увидеть следующий код:
- (void)viewDidLoad {
   [super viewDidLoad];
   _userDictionary = [[NSDictionary alloc] init]; /* Инициализация полей пользователя */
}

- (void)viewDidUnload {
   [super viewDidUnload];
}

- (void)dealloc{
   [_userDictionary release], _userDictionary = nil;
   [super dealloc];
}



А теперь, пора посмотреть на условную диаграмму жизни UIViewController


Как видно (надеюсь) из диаграммы, метод viewDidLoad в процессе жизни контроллера может быть вызван более одного раза. В результате, приведенный выше код может привести утечкам памяти, при каждом новом вызове viewDidLoad

Ошибка #5(l): viewDidLoad вызывается один раз при жизни контроллера.

На самом деле, очень полезно использовать данный метод для восстановления состояния view контроллера. Так, очень часто в примерах можно встретить установку позиции у UIScrollView, установка визуальных компонент в актуальное состояние (active/inactive).

Необходимо заметить, что на данном этапе жизненного цикла контроллера, размеры view не актуальны, т.е. не такие, какими они будут после вывода на экран. Поэтому, использовать вычисления, основанные на ширине / высоте view, в методе viewDidload не рекомендуется.

Ошибка #6(u): viewDidload возвращать интерфейс в исходное состояние.
Ошибка #7(l): Определение размеров компонентов, расчеты, использование ширины / высоты view

viewWillAppear и viewDidAppear

Методы, которые вызываются перед и после появления view на экране.
В случае анимации(появление контроллера в модальном окне, или переход в UINavigationController'e), viewWillAppear будет вызван до анимации, а viewDidAppear — после.
При вызове viewWillAppear, view уже находится в иерархии отображения (view hierarchy) и имеет актуальные размеры, так, что здесь можно производить расчеты, основанные на ширине / высоте view.

viewWillDisappear и viewDidDisappear

Методы, которые очень нечасто используются программистами. Работают так же само, как и viewWillAppear и viewDidAppear, только наоборот ;)

viewDidUnload

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

Метод вызывается когда, view был выгружен из памяти.
При вызове этого метода аутлеты все еще находятся в памяти, НО по сути, они не актуальны, т.к. не находятся в иерархии отображения, а при следующем viewDidLoad будут переписаны новыми.

Так что, для корректной работы данного метода и программы в целом, необходимо:
— Обнулить все аутлеты.
— По возможности сохранить состояние view, чтобы восстановить его в следующем вызове viewDidLoad.
— Не вызывать методы, которые приведут к загрузке view.
— Не использовать аутлеты для хранения состояния контроллера.

Задача проста и ясна, но 90% начинающих разработчиков про нее не знают и забывают.

- (void)viewDidUnload {
  /* Освободите ВСЕ аутлеты */
  [super viewDidUnload];

  /* Хорошее начинание, но ужасное исполнение.
    При вызове этого метода будет заново загружен view */
  UIView * observer = self.view;
  [[NSNotificationCenter defaultCenter] removeObserver:view];
}

Ошибка #8(p) Метод viewDidUnload пуст, и выглядит как [super viewDidUnload]
Ошибка #9(p) Метод viewDidUnload не освобождает аутлеты
Ошибка #10(u) Метод viewDidUnload не сохраняет состояние контроллера
Ошибка #11(l) Использование аутлетов для хранения состояния контроллера

Обработка memory warning


Метод didReceiveMemoryWarning вызывается системой, при нехватке памяти.
По умолчанию, реализация этого метода для контроллера, который не находится в видимой области, вызовет, освободит view (после этого _view == nil), что, в свою очередь приведет к вызову viewDidUnload.
В этом методе необходимо освободить как можно больше памяти, если нельзя освободить — то сбросить в кэш (в файл, например)

Ошибка #12(p) Хранение в памяти ресурсов, которые можно свободно сбросить в кэш в файл.

Уничтожение


Из особенностей dealloc у контроллера, необходимо отметить то, что не факт, что перед этим будет вызван viewDidUnload.
В реализациях некоторых библиотек сторонних разработчиков, например, в Three20, можно встретить вызов viewDidUnload напрямую в dealloc.
В остальном — руководствуйтесь принципами, изложенными Memory Management Programming Guide.

На этом почти все.
Дальше будут даны некоторые пояснения по поводу часто допускаемых ошибок.

Пояснения к частым ошибкам


Ошибки условно разделены на три типа:
p — perfromance error (ошибка, приводящая к утечкам памяти, снижению скорости работы программы)
l — logic error (ошибка которая может привести к неправильной работе программы, вследствие неправильных допущений/убеждений)
u — user unfriendly error (Не совсем ошибка, скорее просто напоминание о том, что пользовательский интерфейс должен быть дружелюбен к пользователю)

  • Ошибка #2(p): Загрузка view до того, как она на самом деле необходимо.
    Часто, особенно начинающие разработчики, имеют нехорошую привычку хранить состояние не в отдельном объекте состояния, а во view контроллера(см. Ошибку #11). По этим причинам можно наблюдать код следующего вида:
       // Получаем настройки
       SettingsController * settings = 
       [self.tabBarController.viewControllers objectAtIndex:3];
       
       // Узнаем введен ли пароль
       if (settings.view.password.text &&
           [settings.view.password length]) {
          
       }
    
       // Settings view. Загружен и находится в памяти
       // Хотя пользователь даже не заходил на него и не видел
    

    Кроме этого, часто можно слышать оправдательные утверждения вида: «Settings view» должен быть всегда в памяти, т.к. в ином случае, пользователю прийдется ждать пока он загрузится. И переключение по табам будет очень медленным! Как вариант всегда можно и нужно рассматривать вариант постепенной загрузки view и в крайнем случае ставить «Loading».
  • Ошибка #3(p): Создание визуальных компонент в методе initWithNibName
    Прямой удар ниже пояса по производительности. Т.к. инициализация контроллера еще не обещает того, что его view будет отображена на экране, и визуальные компоненты — тоже. Ни за что ни про что используется память (под невидимые компоненты), и уходит время на их создание
  • Ошибка #6(u): на viewDidLoad возвращать интерфейс в исходное состояние.
    Ошибка #10(u): Метод viewDidUnload не сохраняет состояние контроллера
    Можно рассматривать такой пример — пользователь, читал книгу, остановился на странице N, мы его при следующем запуске возвращаем на первую страницу. Пользователь «будет весьма обрадован» данному событию. Такие мелочи и различают приложения, которые «почему-то» хочется запускать снова и снова от приложений «потыкал и забыл».
    Не всегда можно вернуть пользователя в то самое место, где он был, и не всегда это необходимо. Но там где это возможно — это очень желательно сделать (В конце концов, iPhone — не Dandy и не Sega MegaDrive).
  • Ошибка #11(l) Использование аутлетов для хранения состояния контроллера
    Ошибка свидетельствует об архитектурных пробелах в приложении. Нельзя/не надо/не стоит хранить состояние приложения в различного вида UISwitchView, UITextField, UITextView. После окончания пользовательского ввода, необходимо сохранить/обновить данные в объекте состояния (Если это состояние котроллера — то можно хранить в свойствах контроллера). Но не используйте для этого визуальную часть.
  • Ошибка #12(p) Хранение в памяти ресурсов, которые можно свободно сбросить в кэш в файл.
    Надо освободить как можно больше памяти. Все это понимают. И кивают головами, когда их спрашивают, понимают ли они, что такое освободить как можно больше памяти. И оставляют 10-ки изображений в памяти, у контроллера, который не виден пользователю, потому что «а вдруг пользователь зайдет на контроллер?». Старайтесь не допускать таких ситуаций. Используйте различные кэшеры, хотя бы тот же NSURLCache.


Пожалуй, на первое время хватит. Надеюсь, пост был информативным, и поможет разработчикам iOS делать по-настоящему хорошие, не только снаружи, но и внутри приложения.

Всем спасибо за внимание.

UPDATE. Подправил описание метода viewDidUnload
UPDATE. Рассмотрено исключение для наследников UITableViewController в методе initWithNibName:bundle: (Cпасибо хабраюзеру muryk)
UPDATE. По просьбам трудящихся, выкладываю небольшой проект для закрепления материала
Павел Тайкало @Kilew
карма
59,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (43)

  • +3
    честно говоря сколько программирую на iPhone/iPad а пару ошибок из списка у себя в проекте нашел
    (особенно 6 и 10) так что спасибо
  • +2
    можно преспокойно программировать под iOS полгода, и не знать полностью о жизненном цикле UIViewcontroller'ов

    Интервьюировал около 30-40 mac/ios разработчиков. Опыт разный, от полугода до двух лет, все с готовыми проектами.

    Первый же вопрос рассказать про run loop вводил людей в ступор, лишь несколько человек рассказывали что-то правдоподобное. Полностью никто так ничего и не рассказал.
    Второй вопрос — когда удаляются autorelease объекты — тут хотя бы фантазируют, а не просто молчат, попадают в точку опять же единицы.
    Дальше уже обычные вопросы про категории, ассоциативные массивы к ним, прокси, распределенные обьекты для коммуникации между процессами, posix потоки, зачем нужна и как использовать авторизацию, основы gdb, библиотеки, использование intruments хотя бы в gui режиме… В общем, так и не нашел никого у нас :(
    • +3
      А с опытными ифонерами/маководами везде напряг. Обычно наблюдаю две ситуации:
      а. Ничего не знают.
      б. Ничего не знают и много хотят.
      З.Ы. А Вы откуда?
      • +1
        Да, вы абсолютно правы :) Буквально месяц назад приходил к нам 18-летний юнец на должность джуниора. Раньше нигде не работал, подготовки ноль — показал примеры своих работ — о боже, лучше бы не показывал… Когда речь зашла о зарплате и ему был задан вопрос о желаемом окладе он, хитро прищурившись, сказал что знает, сколько должен получать джуниор. 2000$! Я чуть не поперхнулся :) И таких не мало… Куда катиться мир. :)
        • 0
          К дефолту :D
    • 0
      это еще хорошо что с готовыми проектами, у нас вообще большинство резюме
      1) либо имеют только опыт Windows программирования (.NET/Java) приписывая «быстро учусь»
      2) либо выложили пару аппстор приложений уровня сэмпл из сдк
      вакансия на 5 члена команды висит уже 7 месяцев
      • +3
        А вы не думали опустить планку, взять человека с более низкими требованиями и обучить всему чему нужно?

        Работодатели тоже странные бывают, все им гуру программистов подавай! Вот поэтому подобные вакансии и висят по году, ни кому не нужные.
        • 0
          да спокойно, только все хотят полную зарплату за пол-года обучения =)
    • 0
      Надеюсь, у Вас не на Juniora такие требования ;)
      Ушел читать про ассоциативные массивы к категориям.
    • 0
      Имхо лучше на собеседовании про жизненный цикл UIViewController-а спрашивать чем то что вы. Еще бы спросили как работает objc_msgSend.
      • 0
        У любого интервьювера со временем набегает ряд «любимых» вопросов, ответы на которые он знает на зубок. Обычно, это что-то достаточно специфическое и редко используемое в повседневной практике. Уж не знаю почему так происходит, но замечал подобное и за собой в том числе. Даже пытался понять — зачем я спрашиваю то, что для работы особо не пригодится или то, что при необходимости можно узнать за 10 минут чтения документации? Может, немного самоутверждаюсь, когда на сложный вопрос никто не может ответить а я могу? Не знаю. Но бороться с собой надо.
        • 0
          Я ниже раскрыл каким образом эти вопросы сформировались, для каждого человека они почти индивидуальны, кроме базовых. У меня не было цели кого-то завалить, это действительно на самом деле рядовые вещи которые как оказалось практически никто не знает и которые на самом деле не узнать за 10 минут в документации. Если брать большинство тех примеров которые я привел (их конечно больше, просто не стал все расписывать), то они как минимум требуют недель изучения, для хорошего понимания тонкостей которые я кстати не спрашиваю — это уже месяцы.

          А те несколько простых вопросы просто показывают читал ли вообще человек документацию по языку на котором пишет или нет. Это действительно вещи которые узнаются за 10 минут, но если человек не читал этих вещей, он просто о них не знает и даже не станет искать. Я тоже про них не смогу рассказать все в подробностях, хотя и использовал в проектах. Но я про них как минимум читал, знаю для чего используются и всегда найду за 5 минут. Другое дело когда люди садятся писать на obj-c через неделю листания доков — они не знаю про язык в действительности ничего, такие не нужны. Изучить хорошо obj-c и паттерны cocoa, это минимум полгода, в общих представлениях — пара месяцев. Люди же приходят с недельными знаниями.
      • 0
        Ну тут следует уточнить, что во-первых я искал мак разработчиков (просто их так мало, что откликаются в большинстве случаев iOS), во-вторых на самом деле я вообще бы ничего не спрашивал. Мне всегда было сложно сдавать устные экзамены, болталка не подвешена, поэтому я предпочитаю просто спросить код, чтобы не мучать напрасно людей. Если всё в коде хорошо, то вопросов и не будет. Здесь же так получается, что ни проект, так разные велосипеды или костыли, ну или просто ошибки. Поэтому я начинаю задавать вопросы исключительно по этим местам, это как студента пытаться вытащить задавая ему дополнительный вопрос. Ведь вдруг человек на самом деле знает как нужно сделать правильно, но по разным причинам не сделал этого.

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

        Ну и последний шанс — базовые вещи, к которым UIViewController и Ко. на мой взгляд не относятся, они частности, а вот умение найти утечку памяти или понимание MVC — уже относится. Можно выучить первое, но в чуть другом уже начать плавать. А можно просто понимать второе и всегда писать правильно. Поэтому вопросы вполне обычные и нормальные. Вы не поверите, но половина в этом плавает. И ведь я спрашивал самые поверхностные и простые вещи, я никого не заставляю писать probes для dtrace, но когда даже gui не владеют… :/

        p.s.
        Кстати и objc_msgSend тоже спрашивал, впрочем интереса ради уже. У меня ПО часто выходит за пределы прикладного уровня. Вот сейчас например нужно писать инжект для finder'а чтобы в Lion можно было свою иконку в sidebar повесить. Мои ребята к сожалению в этом не очень, приходится мне заниматься.
  • 0
    Есть один интересный момент. Я часто наблюдал, что метод viewDidUnload далеко не всегда вызывается, когда обнуляется view контроллера.
    Поэтому я всегда перегружаю метод:
    [code]
    — (void)setView:(UIView) value {
    super.view = value;
    if (![self isViewLoaded]) {
    [self viewDidUnload];
    }
    }
    [/code]
    В данном случае важно понимать, что метод viewDidUnload может быть вызван несколько раз и он должен быть написан так, чтобы не возникало багов с этим связанных.
    • +2
      А с чего это он должен вызываться, когда обнуляется view?
      • 0
        1. Автор об этом пишет.
        2. По чистой логике viewDidUnload является парным методом viewDidLoad. А значит и должен вызываться как минимум по одному разу на каждый вызов viewDidLoad в тречении жизни VC. Это так же подтверждается докой: «This method is called as a counterpart to the viewDidLoad method» VC Reference.
        Хотя, нигде не говорится явно, что он должен вызываться когда обнуляется view (но сказано, что если он вызывается то view == nil). Это главная лажа :). Если честно, то Apple тут слажало хорошо. Например, во 2 версии сдк его вообще не было, как и isViewLoaded (вернее они был приватным).
        • 0
          Автор не пишет, что он должен вызываться, когда пользователь обнуляет view. Этот метод вызывается системой, когда она сама выгружает view, для оповещения. Если он вызван, то view действительно == nil. Не вижу никакой лажи.
          • +2
            А вот еще нашел:
            «If the view controller releases its view, it calls its viewDidUnload method. You can override this method to perform any additional cleanup required for your views and view hierarchy» — VC Guide.
            Лажа в том, что метод хоть и заявлен парным для viewDidLoad, таковым не является. И когда программист расчитывает на то что его вьюхи созданные в loadView/viewDidLoad будут зарелизены во viewDidLoad, а на практике этого не происходит легко получаем как минимум memory leak после повторного вызова loadView/viewDidLoad.
        • 0
          Спасибо за замечание. Подправил часть с viewDidLoad в dealloc. Действительно можно было подумать, что self.view вызовет viewDidUnload.
          • 0
            И как теперь должно быть?
            • +1
              self.view = nil НЕ вызовет viewDidUnload
              self.view = nil Лишь обнулит переменную _view, что в свою очередь при следующем доступе к self.view приведет к повторному вызову loadView.

              А dealloc в библиотеке Three20 выглядит примерно так

              - (void)dealloc {
              ...
              // You would think UIViewController would call this in dealloc, but it doesn't!
              // I would prefer not to have to redundantly put all view releases in dealloc and
              // viewDidUnload, so my solution is just to call viewDidUnload here.
              [self viewDidUnload];

              [super dealloc];
              }
              • 0
                Спасибо
              • 0
                Кстати, делаю аналогично.
  • +1
    Спасибо за статью. Как раз вплотную подошел к выкуриванию ликов в UIViewController'ах.
    А нет ли у Вас наглядного примера проекта с иллюстрацией изложенный положений?
    • +1
      Нет, такого проекта с иллюстрациями нет — все ошибки брались из памяти, по горячим следам.
      Хорошее предложение. Постараюсь добавить к посту проект с примерами.
      • 0
        Если вас не затруднит.
        А то понимание изложенного материала есть, а вот как его применить на практике — тут местами проблема…
        Особенно интересует инициализация/деинициализация аутлетов и их корректная последующая выгрузка
      • 0
        Было бы очень здорово увидеть в примере сохранение состояния контроллера.
    • +1
      Написал небольшой проект для укрепления материала.
      Надеюсь, поможет.
      • 0
        Спасибо, изучим
  • +1
    К вопросу о init vs initWithNib. Все верно, за одним исключением — UITableViewController и его наследники.

    Если я правильно помню, [UITableViewController new] не работает аналогично [[UITableViewController alloc] initWithNib:nil ...] — в том смысле, что первый вариант игнорирует соответствующий xib/nib файл, если он есть.
    • 0
      Да. Есть такое. Внесу изменения. Спасибо за замечание.

  • 0
    Ошибка #3(p): Создание визуальных компонент в методе initWithNibName

    Вы указали на ошибку, но не дали решения.
    Допустим я хочу создать UIView (UIlabel, UIImage). Если я буду использовать viewDidLoad, то получается нужно делать release в viewDidUnload, что выглядит не совсем правильно.
    • +1
      Это выглядит правильно, только надо дублировать release еще и в dealloc (соответственно в viewDidUnload мало того, что делать [_label release] но также и _label = nil)

      Однако это все имеет смысл только если оставлять созданный в viewDidLoad label retain'ed. В прочем большинстве случаев достаточно делать

      — (void)viewDidLoad {

      _label = [[UILabel alloc] initWithFrame:...]
      [self.view addSubview:_label];
      [_label release]; // вот тут освобождать
      }

      то label освободится сам при выгрузке вьюхи и явно ничего чистить не надо
      • 0
        Я так понял, _label это поле класса?
        В этом случае данный подход это распространенная ошибка. Заключается она в том, что когда self.view обнулится _label будет содержать мусор. И первое же обращение к этому полю приведет к крешу.
        Чтобы этого избежать надо всегда пользоваться простым правилом «поля класса никогда не должны быть авторелизными или заретейнеными не самим классом» (естественно, что week reference исключение отсюда).
        • 0
          Формально Вы правы. Однако есть теория, а есть практика. А практика заключается в том, чтобы не нагромождать сущности сверх нормы в попытке описать все возможные случаи. Давайте, например, в программу еще вставим проверку качества микросхем памяти (мало ли что случится, никто ведь не застрахован от аппаратных сбоев)? Можно напихать кода, отслеживающего все возможные случаи во всех комбинациях. А можно делать так:

          1) Следить за неутечкой ресурсов (что я и описал в предыдущем комментарии)

          2) Обращаться с UI элементами (вьюхами) только методами обслуживающего их контроллера, что собственно прямо предусмотрено парадигмой MVC. А контроллер всегда знает (isViewLoaded), загружен ли его view или нет и соответственно, легальны ли все его члены _label.
          • 0
            Как раз практика и показывает, что пока не программист не научит себя простым правилам он обречен постоянно иметь проблемы из-за неверного управления памятью. Не один десяток раз я сталкивался с такими на разных проектах.
            Вот Вы сами себе противоречите. Говорите что не надо нагромождать сущности, но при этом говорите о дополнительных методах обслуживания. Вы, что при каждом обращении к какой-нибудь вьюшке контроллера проверяете isViewLoaded?
            • 0
              Разумеется не при каждой. Давайте разберемся, при каких обстоятельствах self.view может стать nil? Только в двух —

              1) если в коде программы явно прописан такой вызов (я делаю так крайне редко и эти случаи обслуживаются очень отдельно с учетом всех последствий)

              2) Программа получила предупреждение о нехватке памяти и выгрузила те из вьюшек, которые либо не были в данный момент видны (лежат на соседней вкладке tabbarcontrollerа, лежат на нижнем уровне стека UINavigationController, нигде не лежат — т.е. self.view.superview == nil (это заметим, не случается с модальными контроллерами, в которых и осуществляется основная привязка данных к вьюхам — тут можно вообще особо не стараться с отслеживанием isViewLoaded — практически шанс того, что вьюха модального контроллера будет выгружена из памяти стремится к нулю). Причем если контроллер был создан на основе xib или контроллер переопределил loadView то при этом будет вызван viewDidUnload — в нем можно все обнулить.

              Вот как раз второй случай и рассмотрим. А точнее рассмотрим те из вьюшек, обращения к которым надо проверять с isViewLoaded. При внимательном рассмотрении оказывается, что таких контролов до обидного мало. В большинстве случаев они либо статичны — текст на них не меняется с момента создания (кнопки, лейблы) либо они сами умеют себя обслуживать — таблицы через делегаты или срабатывания кнопок — пока кнопки и таблицы живы они сами о себе позаботятся.

              Ситуаций, когда валидность именно этого член класса (_label) к именно этому контроллеру имеет смысл проверять оказывается на практике ну очень мало. Настолько мало, что тут не имеет смысл городить более сложный огород. Да и вообще, как правило, вся информация на форме меняется в одном месте и делается через что-то вроде

              -(void)updateData {
              if (self.isViewLoaded) {

              }
              }
              // кстати этот вариант сработал бы на любом языке, а вариант с обнулением _label
              // сработает только в Obj-C, где средствами языка разработчику предоставляются
              // поблажка — конструкция [nil somemethod] не является фатальной.
              // Если бы этой поблажки не было бы, то пришлось бы еще и проверять на _label != nil
              // – вас это тоже возмущало бы?

              Безусловно, всякие случаи бывают и по всякому нужен свой подход. Вы — за системность подхода. Я больше склоняюсь практичности. Не такое это сложное дело — отловить это проблемное место, если где что и позабыл вдруг.
              • 0
                В том то и дело, что такой индивидуальный подход в нашем меняющемся мире зачастую неудобен. Сегодня контроллер модальный, завтра не модальный. Сегодня вы работате над проектом, завтра пришло 5 джуниоров, послезавтра они целый день дебажат и не могут понять откуда у них BAD_ACCESS. Ведь в чем суть проблемы то? В том, что в поле в определенный момент времени будет хранится мусор. И кто-нибудь, когда-нибудь этот мусор словит.
                А реализуя системный подход, который предлагает автор, всего этого можно избежать.
  • 0
    Добавлю от себя еще одну ошибку которую сам часто допускал: определение координат у subviews в контроллере. Этим должен заниматься свой собственный производный от view объект. К примеру нужно красиво отобразить текст чтоб в зависимости от длинны все поднималось и опускалось, это нужно делать в методе -(void)layoutSubviews у view. А со стороны контрооллера просто вызывать метод у view типа showObject:. В итоге такой подход даст следующие преимущества: автоматическая анимация при перевороте контроллера, уменьшение связности между view и контроллером т/к/ взаимодействие с view идет не через IBOutlet а с помощью методов, все IBOutlet-ы хранятся во view не как проперти. Описание протокола даст возможность легко заменить view на другой, но это уже если потребуется. В итоге получится что xib будет в основном описывать не controller а view.

    Общий код dealloc и viewDidUnload предпочитаю выносить в спец метод: -(void)cleanup.
  • 0
    благодарю за статью, очень сильный и полезный материал. спасибо!
  • 0
    Спустя 3 года наткнулся на вашу статью и хоть по прежнему не изобретена телепортация и не так уж и много звездолетов бороздят космические просторы, но наша жизнь чуточку стала лучше и теперь появились storyboards. И при создании UIViewController посылается сообщение — (id)initWithCoder:(NSCoder*)aDecoder а
    init
    initWithNibName:
    для программного создания UIViewController уже не катят.

    И чтобы не освобождать в viewDidUnload оутлеты можно создавать их с помощью слабой ссылочки __weak IBOutlet UILabel* label; (аналогично в пропертях)

    Вдруг кому пригодится.
  • 0
    Хорошая статья — жаль что устарела слегка ведь viewDidUnload использовать вообще не стоит (ибо работает не так как планируется, да и слава богу устарел с iOS 6) + теперь есть идеальное место теперь где view.frame соответствует реальному значению это viewWillLayoutSubviews
    • 0
      еще одно дополнение: возможны ситуации когда отработавший viewDidAppear не является поводом вызова viewWillDisappear при убирании view (конкретно при плавном изменении прозрачности этот метод не вызывается), да и здесь считают, что на парность этих функций рассчитывать не стоит.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.