Туториал по инструментам на Swift

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

Даже если вы уже создали множество iOS приложений, или если это ваш первый опыт в их создании, вы, несомненно, познакомитесь с новыми интересными возможностями или сделаете ваши приложения еще лучше.

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

Этот туториал покажет вам, как использовать самые нужные возможности инструментов, которые идут в Xcode как Instruments. Они позволят проверить ваш код на производительность, память, безопасные циклы, и решить другие проблемы.

В этом уроке вы узнаете:

  • Как определить "горячие точки" в вашем коде, используя инструмент Time Profiler для того, чтобы сделать ваш код более эффективным
  • Как обнаружить и устранить проблемы управления памятью, такие как: сильная ссылка циклов в вашем коде, используя инструмент Allocations (распределения).

Заметка

Это руководство предполагает, что вы знакомы со Swift и iOS программированием. Если вы полный новичок в программировании iOS, то вы можете начать с изучения основ программирования на Swift или с изучения базового курса.

Готовы? Давайте погрузимся в увлекательный мир Инструментов! :]

Поехали!

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

Скачайте стартовый проект, распакуйте его и откройте в Xcode.

Этот образец приложения использует Flickr API для поиска изображений. Для использования API вам потребуется ключ API. Для демо проектов, вы можете сгенерировать пробный ключ на сайте Flickr. Задайте любой поиск на http://www.flickr.com/services/api/explore/?method=flickr.photos.search и скопируйте ключ API из URL в нижней части - он следует за "&api_key="  и вплоть до следующего "&".

Например, если ссылка:

http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key.......

То ключ API это: 6593783efea8e7f6dfc6b70bc03d2afb.

Вставьте его в верхнюю часть файла FlickrSearcher.swift, заменив существующий ключ API.

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

Запустите приложение, задайте поиск, нажмите на результат, и вы увидите что-то вроде следующего:

Изучите приложение и проверьте основные функции. Можно подумать, что если готов интерфейс, то приложение готово к продаже. Тем не менее, вы увидите разницу, которую привносят инструменты в ваше приложение.

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

Время для профилирования

Первый инструмент, который Вы рассмотрите - это Time Profiler. В подлежащих измерению интервалах, Instruments остановят выполнение программы и примут трассировку стека в каждом работающем потоке. Считайте, что это то же самое, когда вы нажимаете кнопку паузы в дебаггере Xcode.

Вот предварительный просмотр Time Profiler:

Этот экран отображает Call Tree. Call Tree показывает время, потраченное на выполнение различных методов в приложении. Каждая строка представляет собой метод, который следовал по пути выполнения программы. Время, потраченное на каждый метод может быть определено из числа остановок профайлера в каждом методе.

Например, если в 1 миллисекундный интервал получено 100 сэмплов образцов, и конкретный метод оказывается наверху стека в 10 из этих образцов, то вы можете сделать вывод, что этот метод занимает примерно 10% от общего времени выполнения, т.е. 10 мс - было потрачено на выполнение этого метода. Это довольно грубый подсчет, но он работает!

Заметка

Всегда тестируйте ваше приложение на реальном устройстве, а не симуляторе. Симулятор iOS имеет все "лошадиных силы" вашего Mac, в то время как устройство будет иметь ограничения мобильного оборудования. Ваше приложение может работать нормально на симуляторе, но проблема производительности может появиться, когда оно будет запущено на реальном устройстве.

Пришло время "инструментировать" ваш код!

На панели меню Xcode выберите Product\Profile или нажмите ⌘I.. Это создаст приложение и запустит Instruments. Откроется окно, которое выглядит вот так:

Все это разные шаблоны, которые идут вместе с инструментами.

Выберите инструмент Time Profiler и нажмите Choose. Это откроет новый документ Instruments. Нажмите красную кнопку записи слева сверху, чтобы начать запись и запустить приложение. Вам может быть предложено ввести пароль для авторизации Instruments, чтобы была возможность проанализировать другие процессы - не бойтесь, в данном случае это безопасно! :]

В окне Инструменты, вы можете видеть подсчет времени, маленькая стрелка движется слева направо над диаграммой (графиком) в центре экрана. Это означает, что приложение работает.

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

Но вам повезло, потому что вы это исправите! Но в первую очередь давайте найдем в Инструментах, то что нам нужно.

Во-первых, убедитесь, что на переключателе view, на правой стороне панели инструментов, выбраны обе опции, как показано:

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

  1. Это управление записью. Красная кнопка "record" остановит и запустит профилируемое в данный момент приложение. Кнопка паузы делает именно то, что вы от нее ожидаете: приостанавливает выполнение текущего приложения.
  2. Это таймер работы приложения. Таймер отсчитывает, как долго профилируемое приложение уже запущено, и сколько раз оно запущено. Если вы остановите и перезапустите приложение, используя элементы управления записью, то запустится новый запуск, и затем на дисплее будет показано Run (Запуск) 2 из 2.
  3. Это называется треком. В случае использования шаблона Time Profiler, который вы выбрали, вы используете здесь только один инструмент, поэтому у вас только один трек. Вы узнаете больше об особенностях указанного графика в этом туториале чуть позже.
  4. Это панель деталей. Она показывает основную информацию о конкретном инструменте, который вы используете. В этом случае, она показывает самые "горячие" методы - то есть, те, которые больше всего нагружают CPU. Если вы щелкните по панели сверху, на которой стоит Call Tree (слева), и выберете Sample List, то вы увидите другой вариант представления данных. Этот вид показывает каждый образец по отдельности. Нажмите на несколько образцов, и вы увидите, что появится трассировка стека для захвата в инспекторе Extended Detail (справа).
  5. Это панель инспектора. Есть три инспектора: Record Settings (настройки записи), Display Settings (настройки дисплея), и Extended Detail (детализациия). Скоро вы рассмотрите некоторые из них.

Теперь исправим наш неуклюжий интерфейс! :]

Погружаемся!

Выполните поиск изображений, и "погрузитесь" в список результатов. Я лично поискал бы "собак" (dogs), но вы можете искать все, что угодно, но возможно вы один из тех самых любителей "кошек" (cats)? :]

Теперь прокрутите скроллом вверх и вниз по списку несколько раз, чтобы в вашем Time Profiler появилось достаточное количество данных. Вы должны заметить, что в середине экрана меняются цифры и заполняется диаграмма. Это говорит о том, что в настоящее время используется циклы CPU.

Вы действительно скорее всего никогда не видели таких неуклюжих пользовательских интерфейсов, как этот! Table view не будет готов к отправке, пока он не будет прокручиваться, как по маслу! Чтобы помочь определить проблему, необходимо установить некоторые опции.

На правой стороне, выберите инспектор Display Settings (или нажмите ⌘ + 2). В инспекторе, под разделом Call Tree, выберите Separate by Thread, Invert Call Tree и Hide System Libraries. Это будет выглядеть так:

Вот как эти опции воздействуют на данные, отображаемые в таблице слева:

  1. Separate by Thread: Каждый поток должен рассматриваться отдельно. Это позволяет понять, какие потоки отвечают за наибольшее количество использования CPU.
  2. Invert Call Tree: С помощью этой опции, трассировка стека происходит сверху вниз. Это, как правило, именно то, что вам нужно, когда вы хотите увидеть самые глубокие методы, в которых участвует CPU.
  3. Hide System Libraries: Когда выбрана эта опция, то будут отображается только идентификаторы вашего собственного приложения. Часто бывает полезно выбирать эту опцию, поскольку, как правило, вас интересует только то, где CPU тратит время в вашем собственном коде - вы ведь не можете повлиять на то, как много CPU используют системные библиотеки!
  4. Flatten Recursion: Эта опция имеет дело с рекурсивными функциями (те, которые вызывают сами себя) в виде одной записи в каждой трассировки стека, а не нескольких.
  5. Top Functions: Включение этой функции дает возможность Инструментам рассмотреть общее время, проведенное в функции в виде суммы времени, потраченного непосредственно внутри этой функции, а также время, проведенное в функциях, вызываемых этой функцией. Таким образом, если функция А вызывает Б, то время А передается как время, потраченное в А плюс время, проведенное в Б. Это может быть полезным, так как это позволяет выбрать самый крупный отрезок времени, каждый раз, когда вы спускаетесь в стек вызовов, обнуляя ваши самые трудоемкие методы.
  6. Если вы работаете в Objective-C, тут тоже есть опция Show Obj-C Only. Если ее выбрать, то будут показаны только методы Objective-C, а не функции С или C ++. Если вы смотрите на приложения OpenGL, то вы, например, можете видеть некоторые методы или функции из C ++, хотя в самом приложении этого языка и вовсе нет.

Хотя некоторые значения могут у вас немного отличаться, но после того, как вы установите вышеописанные опции, порядок записей должен быть похож на тот, что в таблице ниже:

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

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

А вот это интересно, не правда ли?! applyTonalFilter() является методом, добавленным в UIImage в расширении, и почти 100% от времени, проведенного в нем, проводится для создания вывода CGImage, после применения фильтра на изображение.

На самом деле, мы не можем ускорить этот процесс: создание изображения является достаточно интенсивным процессом, и занимает столько времени, сколько потребуется. Давайте вернемся на шаг назад и посмотрим откуда вызывается applyTonalFilter(). Кликните на Call Tree в хлебных крошках в верхней части, чтобы вернуться на предыдущий экран:

Теперь нажмите на маленькую стрелку слева от applyTonalFilter в верхней части таблицы. Это развернет Call Tree и покажет вызывавшего applyTonalFilter. Вам, возможно, нужно будет развернуть и следующий ряд. Во время профилирования в Swift могут появиться повторяющиеся (продублированные) строки в Call Tree, с префиксом @objc. Вам нужен первый ряд, у которого есть префикс в виде имени таргета вашего приложения (InstrumentsTutorial):

В этом случае, эта строка относится к результатам коллекции view cellForItemAtIndexPath. Дважды щелкните по строке, чтобы увидеть соответствующий код из проекта.

Теперь вы уже видите, что проблема есть. Исполнение метода, применяющего тональный фильтр, занимает много времени и вызывается он непосредственно из cellForItemAtIndexPath, который будет блокировать основной поток (и, следовательно, весь пользовательский интерфейс) каждый раз, когда нужно будет наложить фильтр на изображение.

Разгрузка Работы

Решить эту проблему нужно в два этапа: первый - перенесем изображение с фильтром в фоновый поток с помощью dispatch_async. Затем кэшируйте каждое изображение после того как оно сгенерируется. Существует небольшой, простой класс кэширования изображений (с броским названием ImageCache), он включен в стартовый проект, который просто хранит изображения в памяти и извлекает их на основе данного ключа.

Теперь вам нужно перейти в Xcode и вручную найти исходный файл, который вам нужен в Инструментах, но прямо перед вашими глазами есть удобная кнопка Open in Xcode. Найдите ее на панели чуть выше кода и нажмите:

Вот так! Xcode открывается как раз в нужном месте!

Теперь, внутри collectionView(_:cellForItemAtIndexPath:) замените вызов на loadThumbnail() следующим:

      flickrPhoto.loadThumbnail { image, error in
        if cell.flickrPhoto == flickrPhoto {
          if flickrPhoto.isFavourite {
            cell.imageView.image = image
          } else {
            if let cachedImage = ImageCache.sharedCache.imageForKey("\(flickrPhoto.photoID)-filtered") {
              cell.imageView.image = cachedImage
            } else {
              dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
                if let filteredImage = image?.applyTonalFilter() {
                  ImageCache.sharedCache.setImage(filteredImage, forKey: "\(flickrPhoto.photoID)-filtered")
                  dispatch_async(dispatch_get_main_queue(), {
                    cell.imageView.image = filteredImage
                  })
                }
              })
            }
          }
        }
      }

Первый раздел кода остается таким же, как был раньше, и связан он с загрузкой в Flickr Photo эскизного (уменьшенного) изображения из Интернета. Если фотографию добавили в избранное ("лайкнули"), то она ее уменьшенное (эскизное) изображение в ячейке будет отображается как есть. Если фото не понравилось (его не добавили в избранное), то будет применен тональный фильтр.

Эта часть кода, где произошли изменения: во-первых, код проверяет, есть ли отфильтрованное изображение для этого фото в кэше. Если да, то здорово, и изображение отображается в image view. Если нет, то вызов для применения тонального фильтра к изображению отправляется в фоновую очередь. Это позволит интерфейсу оставаться способным реагировать (responsive) в то время как изображение будет фильтроваться. Когда фильтр применен, изображение будет сохранено в кэше, и image view обновится в главной очереди.

Мы позаботились об отфильтрованных изображениях, но ведь есть еще оригинальные эскизы Flickr. Откройте FlickrSearcher.swift и найдите loadThumbnail(_:). Замените его следующим образом:

  func loadThumbnail(completion: ImageLoadCompletion) {
    if let image = ImageCache.sharedCache.imageForKey(photoID) {
      completion(image: image, error: nil)
    } else {
      loadImageFromURL(URL: flickrImageURL("m")) { image, error in
        if let image = image {
          ImageCache.sharedCache.setImage(image, forKey: self.photoID)
        }
        completion(image: image, error: error)
      }
    }
  }

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

Перезапустите приложение в Instruments через Product\Profile (или ⌘I - эти горячие клавиши реально сэкономят вам время).

Обратите внимание, что на этот раз вас не спросили, какой использовать инструмент. Это потому, что у вас до сих пор открыто окно приложения, и Instruments предполагает, что вы хотите его запустить снова с теми же опциями.

Выполните несколько поисковых запросов, и обратите внимание, что на этот раз интерфейс не совсем уж так неуклюж! Фильтр изображения в настоящее время применяется асинхронно и изображения кэшируются в фоновом режиме. Вы увидите ряд dispatch_worker_threads в Call Tree - они управляют обработкой тяжелыми графическими фильтрами.

Выглядит отлично! Можно закругляться? Еще нет! :]

Allocations, Allocations, Allocations (Распределение ресурсов)

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

Чтобы создать новый профиль инструментов, выйдите из приложения Instruments. На этот раз, запустите приложение, и откройте Debug Navigator на Navigators area. Затем кликните на Memory для отображения диаграмм использования памяти в главном окне:

Эти диаграммы полезны для получения представления о том, как работает ваше приложение. Но вашему приложению нужна еще мощность. Нажмите на кнопку Profile in Instruments, а затем Transfer для того, чтобы довести эту сессию до Инструментов. Инструмент Allocations будет запущен автоматически.

В этот раз вы увидите две дорожки. Одна из них называется Allocations (Распределение), другая Leaks (Утечки). О дорожке Allocations мы поговорим позднее, а дорожка Leaks более полезна для Objective-C, и мы не будем рассматривать ее в этом туториале.

Так что какую ошибку вы собираетесь дальше отслеживать? :]

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

  1. True memory leaks (действительные утечки памяти), где на объект больше никто не ссылается, но он по-прежнему занимает место - это означает, что память не может быть использована повторно. Даже в Swift и ARC, наиболее распространенный вид утечки памяти это retain cycle (сохраненный цикл) или strong reference cycle (цикл сильных ссылок). Этот цикл образуется, когда два объекта удерживают сильные ссылки друг на друга, так что каждый объект сохраняет другого от того, чтобы стать освобожденным. Это означает, что их память никогда не освободиться!
  2. Unbounded memory growth (Неограниченный рост памяти), где память по-прежнему распределяется и не освобождается. Если это продолжается вечно, то в какой-то момент система памяти будет заполнена, и у вас возникнет проблема с памятью. В iOS это означает, что приложение будет убито системой.

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

Вы должны были заметить, что диаграмма в треке Allocations начала расти. Это говорит о том, что начала распределяться память. Этот график вам говорит о том, что вы, вероятно, нашли утечку тип "неограниченного роста памяти".

То, что вы собираетесь сделать, это "generation analysis" ("генерационный анализ"). Для этого нажмите кнопку под названием Mark Generation. Вы найдете кнопку в верхней части инспектора Display Settings:

Нажмите ее и вы увидите как на треке появится красный флажок, вот такой:

Целью генерационного анализа является выполнение действия несколько раз подряд для того, чтобы можно было увидеть, начинает ли расти память постоянно и неограниченно расти. Зайдите в поиск, подождите несколько секунд для того, чтобы все изображения загрузились, а затем вернитесь на главную страницу. Снова нажмите Mark Generation. Сделайте это несколько раз для различных поисковых запросов.

После того, как вы несколько раз запустите поиск, Инструменты будут выглядеть следующим образом:

Здесь вы должны почувствовать неладное. Обратите внимание, что строчка, выделенная синим, идет вверх с каждым поисковым запросом. Это, безусловно, не очень хорошо. Но подождите, что же с "предупреждениями о проблемах с памятью"? Вы ведь знаете о таких, верно? "Предупреждения о проблемах с памятью" являются способом iOS сообщить приложению о том, что памяти не хватает или она ограничена, и вы должны ее почистить.

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

Сымитируйте предупреждение о проблемах с памятью, выбрав Instrument\Simulate Memory Warning на панели Инструментов, или Hardware\Simulate Memory Warning на панели симулятора. Вы заметите, что использование памяти ослабевает совсем немного, или, возможно, вообще остается без изменений. Конечно, мы ждали не этого. Так что ищите неограниченный рост памяти где-то еще.

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

Поговорим о генерации

В каждой генерации, вы увидите все объекты, которые были созданы в тот момент, когда мы создали генерацию. Последующие генерации будут содержать только те объекты, которые были созданы после предыдущей генерации.

Посмотрите на колонку Growth (роста), и вы увидите, что, безусловно, он где-то происходит. Откройте одну из генераций, и вы увидите следующее:

Ничего себе, так много объектов! С чего же начать?
К сожалению, в Swift происходит большее "загромождение" этого view, чем было в Objective-C, так как в нем содержатся внутренние типы данных, о которых вам и знать не нужно. Вы можете его немного почистить, переключив Allocation Type на All Heap Allocations. Кроме того, нажмите на заголовок Growth для сортировки по размеру.

Прямо сверху находится ImageIO_jpeg_Data, и это, безусловно, то, с чем имеет дело ваше приложение. Нажмите на стрелку слева от ImageIO_jpeg_Data для отображения полного списка. Выберите одну строчку, а затем выберите Extended Detail инспектор (или нажмите ⌘ + 3):

Здесь показана трассировка стека в тот момент, когда был создан конкретный объект. Части стека, окрашенные в серый цвет относятся к системным библиотекам, части, окрашенные в черный относятся к коду вашего приложения. Чтобы получить больше контекста для этой трессировки, кликните внизу двойным щелчком на вторую черную рамку. Та единственная строка с префиксом "InstrumentsTutorial" и указывает, что это из Swift кода. Двойной клик приведет вас к коду для этого метода - вашему "старому другу" collectionView(_:cellForItemAtIndexPath:).

Инструменты очень полезны, но в нашем случае они уже нам больше не помогут! Вам сейчас придется работать в коде самостоятельно, чтобы понять, что там происходит.

Взгляните на метод, и вы увидите, что он вызывает setImage(_:forKey:). Как вы уже видели, когда вы имели дело с Time Profiler, этот метод кэширует изображение в случае, если он снова будет использовать его в приложении. Ах! Звучит, как проблема! :]

Опять, нажмите кнопку Open in Xcode, чтобы перейти обратно в Xcode. Откройте ImageUtilities.swift и взгляните на реализацию setImage(_:forKey:):

  func setImage(image: UIImage, forKey key: String) {
    images[key] = image
  }

Этот код добавляет изображение в словарь, ключом которого является фото ID в Flickr photo. Но если вы посмотрите на код, вы увидите, что изображение никогда не удаляется из этого словаря!

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

Чтобы устранить эту проблему, все, что вам нужно сделать, это заставить ImageCache воспринимать уведомления о проблемах с памятью от UIApplication. Когда ImageCache получает их, он должен очистить кэш.

Для того, чтобы ImageCache прининамал уведомления, добавьте следующий инициализатор и деинициализатор к классу:

init() {
  NSNotificationCenter.defaultCenter().addObserverForName(
    UIApplicationDidReceiveMemoryWarningNotification,
    object: nil, queue: NSOperationQueue.mainQueue()) { notification in
      self.images.removeAll(keepCapacity: false)
  }
}
 
deinit {
  NSNotificationCenter.defaultCenter().removeObserver(self,
    name: UIApplicationDidReceiveMemoryWarningNotification,
    object: nil)
}

Это регистрирует наблюдателя UIApplicationDidReceiveMemoryWarningNotification для выполнения замыкания, которое ранее очистит изображения.

Все, что нужно сделать коду, это удалить все объекты из кэша. Это будет гарантией того, что больше ничего не держит изображения, и они будут удалены.

Чтобы внести это изменение, снова запустите Инструменты (из Xcode нажатием ⌘+I) и повторите шаги, которые вы прошли ранее. Не забывайте, сымитировать предупреждение о проблемах с памятью в конце!

Заметка

Убедитесь, что вы заходите из Xcode, запуская фазы билда, а не просто нажимая красную кнопку в Инструментах, для того, чтобы быть уверенными, что вы используете последнюю версию кода. Так же лучше сначала запустите приложение через Xcode, прежде чем запускать его через Instruments, потому как Xcode иногда, похоже, не обновляет запуск приложения на симуляторе до последней версии, если вы просто под Profile.

На этот раз генерационный анализ должен выглядеть следующим образом:

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

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

Отлично сработано! Еще один вопрос улажен! Но остался еще один вопрос утечек первого типа, который мы еще не рассмотрели.

Циклы с сильными ссылками

Наконец, вы найдете цикл сильных ссылок в Flickr Search app. Как упоминалось ранее, такой цикл происходит, когда два объекта удерживают друг на друга сильные ссылки, и не могут быть освобождены, и "съедают" память. Вы можете обнаружить такой цикл с помощью инструмента Allocations, используя его по-другому.

Заметка

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

Закройте Инструменты, вернитесь в Xcode, и убедитесь, что для запуска приложения выбрано реальное устройство. Выберите еще раз Product\Profile, и выберите Allocations.

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

Чтобы отфильтровать объекты, представляющие для нас интерес, введите "Instruments" в качестве фильтра в поле над Allocations Summary. Теперь вам будут показаны только объекты, у которых в имени типа будет слово "Инструменты". Поскольку образец приложения называется "InstrumentsTutorial", то Allocations теперь покажет только типы, определенные как часть этого проекта. Уже проще!

Два столбца, которые стоит отметить в Инструментах это # Persistent и # Transient. Столбец Persistent (постоянный) содержит счетчик количества объектов каждого типа, которые находятся постоянно в памяти. Столбец Transient (временный, переходный) показывает количество объектов, которые были в памяти, но были удалены. Persistent объекты используют память, transient объекты освобождают память.

Вы должны заметить, что здесь есть постоянный (persistent) экземпляр ViewController - это имеет смысл, потому что это экран, на который вы в настоящее время смотрите. Также есть AppDelegate и экземпляр Flickr API client.

Вернемся в приложение! Выполним поиск и рассмотрим результаты. Обратите внимание, что появилась куча дополнительных объектов в Инструментах: FlickrPhotos, которые были созданы при парсинге результатов поиска, SearchResultsViewController, и ImageCache среди других. Экземпляр ViewController еще постоянен (persistent), потому что он нужен своему навигационному контроллеру.

Теперь нажмите кнопку "назад" для возврата в приложение, SearchResultsViewController теперь удален из стека навигации, поэтому он должен быть удален (освобожден). Но он все еще находится в # Persistent count 1 в отчете Allocations! Почему он все еще там?

Попробуйте выполнить еще два поисковых запроса и нажмите кнопку назад после каждого из них. Теперь 3 SearchResultsViewControllers?! Тот факт, что эти view контроллеры "висят" в памяти, означает, что что-то держит сильную ссылку на них. Похоже, у вас есть цикл с сильной ссылкой!

Ваша главная подсказка в этой ситуации это то, что не только SearchResultsViewController является постоянным, но и все SearchResultsCollectionViewCells. Вполне вероятно, что цикл с ссылкой как раз между этими двумя классами.

К сожалению, на момент написания туториала, вывод Инструментов для Swift еще не полностью полезен в некоторых случаях. Инструменты могут только дать вам подсказку, где находится проблема, и показать вам, где распределяются объекты; а ваша работа выяснить, в чем проблема.

Давайте углубимся в код. Наведите указатель мыши на InstrumentsTutorial.SearchResultsCollectionViewCell в колонке Category и нажмите на маленькую стрелку справа от него. Следующий view покажет вам все allocations SearchResultsCollectionViewCells во время работы приложения. А их довольно много - по одному для каждого результата поиска!

Измените Инспектор на инспектор Extended Detail, нажав третью иконку в верхней части панели. Это инспектор показывает трассировку стека выбранного распределения (allocation). Как и в предыдущей трассировке стека, черные части- это код. Дважды щелкните по верхней черной строке (которая начинается с "InstrumentsTutorial"), чтобы увидеть, где располагаются ячейки.

Ячейки размещаются в верхней части collectionView(cellForRowAtIndexPath:). Несколько строк вниз, и вы увидите, что это (без помощи Инструментов, к сожалению!):

cell.heartToggleHandler = { isStarred in
  self.collectionView.reloadItemsAtIndexPaths([ indexPath ])
}

Это замыкание, которое обрабатывает прикосновение на одну из кнопок-сердечек в collection view cells. Это там и находится цикл с сильной ссылкой, но его трудно обнаружить, если вы не сталкивались с ним ранее.

Замыкание ячеек ссылается на SearchResultsViewController, используя self, что создает сильную ссылку. Замыкание захватывает само себя. Swift фактически заставляет вас явно использовать слово self в замыканиях (в то время как вы можете, как правило, не использовать его, обращаясь к методам и свойствам текущего объекта). И вы можете не понять, что вы захватываете его. SearchResultsViewController также имеет сильную ссылку на ячейки через collection view.

Чтобы разорвать цикл с сильной ссылкой, вы можете определить список захвата (capture list) как часть определения замыкания. Этот список может быть использован для объявления экземпляров, захваченных замыканиями, либо как weak или unowned:

  • Weak должен быть использован, когда захватывающая ссылка может быть nil в будущем. Если объект, на который ссылаются, освобождается, то ссылка становится nil. По сути, они опционального типа.
  • Unowned ссылки должны быть использованы, когда замыкание и объект, на который оно ссылается, имеют один и тот же срок жизни, и должны быть удалены в одно и то же время. Unowned ссылки никогда не могут стать nil.

Для того, чтобы исправить этот цикл с сильной ссылкой, нажмите еще раз кнопку Open in Xcode и добавьте capture list (список захвата) в свойство heartToggleHandler в SearchResultsViewController.swift:

cell.heartToggleHandler = { [weak self] isStarred in
  if let strongSelf = self {
    strongSelf.collectionView.reloadItemsAtIndexPaths([ indexPath ])
  }
}

Объявляя self как weak означает, что SearchResultsViewController может быть освобожден, даже если collection view cells содержат ссылку на него, так как они теперь лишь просто слабые ссылки. И освобождение SearchResultsViewController освободит свое collection view и, затем ячейки.

Внутри Xcode, используйте ⌘+I снова запустите приложение Instruments.

Посмотрите на приложение снова из Инструментов, используя инструмент Allocations, как вы делали это раньше (помните о фильтрации результатов, чтобы оставить только классы, являющиеся частью стартового проекта). Выполните поисковый запрос, перейдите в его результаты, и обратно. Вы должны увидеть, что SearchResultsViewController и его ячейки освобождаются в тот момент, когда вы возвращаетесь обратно. Они показывают только переходные (transient) экземпляры, а не постоянные (persistent ).

Цикл разбит!

Конечный проект вы можете скачать здесь.

Что дальше?

Дальше, вы можете продолжить изучать наши туториалы по мере их появления, а также, параллельно читать перевод официальной книги по языку программирования Swift. И, для более подробного изучения языка, вы можете пройти наши курсы!

Урок подготовил: Иван Акулов

Источник урока: http://www.raywenderlich.com/97886/instruments-tutorial-with-swift-getting-started