Pull to refresh

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

Reading time 10 min
Views 154K
Большинство прикладных приложения под 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. По просьбам трудящихся, выкладываю небольшой проект для закрепления материала
Tags:
Hubs:
+58
Comments 43
Comments Comments 43

Articles