Туториал по Grand Central Dispatch для Swift: Часть 2/2

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

Туториал по Grand Central Dispatch (GCD) в Swift: часть вторая.

Добро пожаловать во вторую и заключительную часть данной серии туториалов по Grand Central Dispatch!

В первой части вы узнали о согласованности, потоках и как именно работает GCD. Вы сделали потокобезопасный синглтон PhotoManager для чтения и записи фотографий с помощью комбинации dispatch_barrier_async и dispatch_sync. В дополнение ко всему, вы повысили UX (User Experience - восприятие пользователем) приложения через задержку и появление подсказки с помощью dispatch_after и разгрузили загрузку view controllera для выполнения ресурсоемких задач с dispatch_async.

Если вы уже прошли проект в первой части, то продолжайте с того места, где вы остановились. Если же вы не делали проект из Части 1, то вы можете скачать готовый проект в этом уроке, просто чтобы его продолжить.

Пришло время изучить еще несколько GCD!

Коррекция Преждевременного Появления

Вы, возможно, заметили, что, когда вы пытаетесь добавить фотографии с опцией Le Internet, то окошко оповещения всплывает задолго до того, как изображения закончит загружаться, как это показано на скриншоте ниже:

Неисправность заключается в PhotoManager в методе downloadPhotosWithCompletion, который воспроизведен ниже:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
    var storedError: NSError?
    for address in [OverlyAttachedGirlfriendURLString,
                    SuccessKidURLString,
                    LotsOfFacesURLString] {
      let url = NSURL(string: address)
      let photo = DownloadPhoto(url: url!) {
        image, error in
        if error != nil {
          storedError = error
        }
      }
      PhotoManager.sharedManager.addPhoto(photo)
    }

    if let completion = completion {
      completion(error: storedError)
    }
  }

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

Метод экземпляра класса DownloadPhoto начинает загрузку файла с URL и мгновенно возвращается до завершения загрузки. Другими словами, downloadPhotosWithCompletion вызывает в конце свое завершающее замыкание так, как будто его собственное тело метода было прямой синхронной линией кода, и каждый завершенный вызов метода уже закончил свою работу.

Тем не менее, DownloadPhoto(url:) является асинхронным и мгновенно возвращается - так что этот подход не будет работать.

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

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

К счастью, этот тип множественного асинхронного завершающего контроля именно то, для чего предназначены dispatch groups (группы отправки).

Dispatch Groups (Группы Отправки)

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

GCD API обеспечивает два способа получения уведомлений, когда все события в группе завершены.

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

Откройте PhotoManager.swift и замените downloadPhotosWithCompletion следующей реализацией:

  func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
    dispatch_async(GlobalUserInitiatedQueue) { // 1
      var storedError: NSError!
      let downloadGroup = dispatch_group_create() // 2
      
      for address in [OverlyAttachedGirlfriendURLString,
        SuccessKidURLString,
        LotsOfFacesURLString]
      {
        let url = NSURL(string: address)
        dispatch_group_enter(downloadGroup) // 3
        let photo = DownloadPhoto(url: url!) {
          image, error in
          if let error = error {
            storedError = error
          }
          dispatch_group_leave(downloadGroup) // 4
        }
        PhotoManager.sharedManager.addPhoto(photo)
      }
      
      dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER) // 5
      dispatch_async(GlobalMainQueue) { // 6
        if let completion = completion { // 7
          completion(error: storedError)
        }
      }
    }
  }

Теперь рассмотрим пошагово:

  1. Так как вы используете синхронный dispatch_group_wait, который блокирует текущий поток, вы используете dispatch_async, чтобы разместить весь метод в фоновой очереди, для того чтобы убедиться, что вы не блокируете основной поток.
  2. Это создает новую группу отправки (dispatch group), которая ведет себя отчасти как счетчик числа незавершенных задач.
  3. dispatch_group_enter вручную уведомляет группу, что выполнение задачи началось. Вы должны сбалансировать количество вызовов dispatch_group_enter числом вызовов dispatch_group_leave или ваше приложение упадет.
  4. Здесь вы вручную уведомляете группу, что эта работа сделана. Опять же, вы приводите в баланс все входы и выходы из группы.
  5. dispatch_group_wait ждет, пока либо все задачи завершатся, либо до истечения времени. Если время истекает до завершения всех событий, то функция вернет ненулевой (non-zero) результат. Вы можете поместить это в условное замыкание, чтобы проверить, истек ли период ожидания. Однако, в этом случае вы указали, что нужно ждать вечно, используя DISPATCH_TIME_FOREVER . Что означает, и это неудивительно, что он будет ждать вечно! Это нормально, потому что создание фото всегда будет в итоге завершено.
  6. На этом этапе все image задачи гарантированно уже завершены или их время истекло. Затем вы делаете обратный вызов в главную очередь для запуска завершающего замыкания. Это добавит работу главному потоку, но она будет выполнена позже.
  7. Наконец, запустите завершающее замыкание, если оно было необходимо.

Запустите приложение, попытайтесь загрузить несколько изображений и обратите внимание, как ваше приложение ведет себя с завершающим замыканием.

Заметка

Если сетевые подключения происходят слишком быстро, чтобы различить, когда следует вызвать завершающее замыкание и вы работаете с приложением на устройстве, вы можете убедиться, что оно действительно работает, переключив некоторые параметры сети в разделе Developer Section в настройках Settings. Просто зайдите в раздел Network Link Conditioner, включите его, и выберите профиль. Нужно выбрать “Very Bad Network” (Очень плохое подключение).

Если вы работаете на симуляторе, то вы можете использовать Network Link Conditioner included in the Hardware IO Tools for Xcode, чтобы изменить скорость работы сети. Это хороший инструмент в вашем арсенале, потому что вы сможете понять, что может произойти с вашим приложением, когда скорость соединения будет меньше, чем оптимальная.

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

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

  • Custom Serial Queue (Пользовательская Последовательная Очередь): является хорошим кандидатом для уведомлений, когда группа задач завершена.
  • Main Queue (Serial) (Главная Очередь (Последовательная): в этом сценарии этот тип очереди тоже является хорошим кандидатом. Вы должны быть осторожны с использованием групп в главной очереди, если вы ожидаете синхронности для завершения всех работ для того, чтобы не задерживать основной поток. Тем не менее, асинхронная модель является привлекательным способом для обновления пользовательского интерфейса, как только будут завершены несколько “долго-выполняемых” задач, таких как сетевые вызовы.
  • Concurrent Queue (Согласованная Очередь): этот тип очереди также является хорошим кандидатом для групп отправки и уведомлений о завершении.

Группы отправки. Второй способ

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

Найдите downloadPhotosWithCompletion в PhotoManager.swift и замените его следующей реализацией:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
    // 1
    var storedError: NSError!
    let downloadGroup = dispatch_group_create()
    
    for address in [OverlyAttachedGirlfriendURLString,
      SuccessKidURLString,
      LotsOfFacesURLString]
    {
      let url = NSURL(string: address)
      dispatch_group_enter(downloadGroup)
      let photo = DownloadPhoto(url: url!) {
        image, error in
        if let error = error {
          storedError = error
        }
        dispatch_group_leave(downloadGroup)
      }
      PhotoManager.sharedManager.addPhoto(photo)
    }
    
    dispatch_group_notify(downloadGroup, GlobalMainQueue) { // 2
      if let completion = completion {
        completion(error: storedError)
      }
    }
  }

Вот как работает ваш новый асинхронный метод:

  1. В этой новой реализации вам не нужно окружать метод в вызове dispatch_async, так как вы не блокируете основной поток.
  2. dispatch_group_notify служит асинхронным завершающим замыканием. Код будет выполняться, когда больше не останется элементов в группе отправки, и придет очередь завершающего замыкания. Вам нужно указать в какой очереди запустить ваш завершающий код. Здесь основной очередью будет та, которую вы захотите.

Это более «чистый» способ справиться с этой конкретной работой и не блокировать потоки.

Опасности «Перебора» Согласованности

С учетом всех этих новых инструментов в вашем распоряжении, вы должно быть уже «запутались», не так ли?

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

Это работа для dispatch_apply.

dispatch_apply действует как цикл for, который выполняет согласованно различные итерации. Эта функция синхронна - так же, как обычный цикл for, dispatch_apply возвращается только тогда, когда вся работа будет сделана.

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

Когда целесообразно использовать dispatch_apply?

  • Custom Serial Queue (Пользовательская Последовательная Очередь): последовательная чередь будет полностью отрицать применение dispatch_apply. Вы можете просто использовать нормальный цикл for.
  • Main Queue (Serial) (Главная Очередь (Последовательная): Так же, как и в примере выше, использование в последовательной очереди является плохой идеей. Просто используйте нормальный цикл for.
  • Concurrent Queue (Согласованная Очередь): Это хороший выбор для согласованного зацикливания, особенно если вам нужно следить за ходом выполнения ваших задач.

Вернитесь к downloadPhotosWithCompletion и замените его следующей реализацией:

func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
    var storedError: NSError!
    let downloadGroup = dispatch_group_create()
    let addresses = [OverlyAttachedGirlfriendURLString,
      SuccessKidURLString,
      LotsOfFacesURLString]
    
    dispatch_apply(addresses.count, GlobalUserInitiatedQueue) {
      i in
      let index = Int(i)
      let address = addresses[index]
      let url = NSURL(string: address)
      dispatch_group_enter(downloadGroup)
      let photo = DownloadPhoto(url: url!) {
        image, error in
        if let error = error {
          storedError = error
        }
        dispatch_group_leave(downloadGroup)
      }
      PhotoManager.sharedManager.addPhoto(photo)
    }
    
    dispatch_group_notify(downloadGroup, GlobalMainQueue) {
      if let completion = completion {
        completion(error: storedError)
      }
    }
  }

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

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

Запустите приложение, а затем добавьте несколько фотографий из Le Internet. Заметили, что изменилось?

Запуск этого нового кода на устройстве будет время от времени чуть улучшать производительность. И стоило ли тогда этим заниматься?

На самом деле, в этом случае не стоило. И вот почему:

  • Вы, наверное, создали энергозатратные задачи, запуская параллельные потоки, а не использовали в первую очередь цикл for. Вы должны использовать dispatch_apply для итерации очень больших сетов вместе с соответствующей длиной шага (stride).
  • Время на создание приложения не резиновое - не тратьте время на предварительную оптимизацию кода, который вы не знаете, что сломан. Если вы и собираетесь что-то оптимизировать, оптимизируйте что-то заметное и что стоит вашего времени. Найдите методы с самым долгим временем выполнения путем профилирования вашего приложения в Инструментах.
  • Как правило, оптимизация кода делает его более сложным для себя и для других разработчиков, следующих за вами. Убедитесь, что улучшения того стоят.

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

Отмена блоков Отправки

На iOS 8 и OS X Yosemite появились dispatch block objects. Они реализуются как wrapper (обертка) вокруг обычных замыканий и ведут себя так же. Блоки отправки объектов могут делать несколько вещей, например устанавливать Quality of Service (QoS) для каждого объекта для внутренних приоритетов в очереди, но в первую очередь это возможность отменить выполнение блоков. Но знайте, что блоки могут быть отменены только до момента, как они достигнут начала очереди и начнется их выполнение.

Давайте продемонстрируем это: начнем закачивать задания для нескольких копий изображений с Le Internet, а затем отменим некоторых из них. Выберите PhotoManager.swift и замените downloadPhotosWithCompletion следующим:

  func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
    var storedError: NSError!
    let downloadGroup = dispatch_group_create()
    var addresses = [OverlyAttachedGirlfriendURLString,
      SuccessKidURLString,
      LotsOfFacesURLString]
    addresses += addresses + addresses // 1
    var blocks: [dispatch_block_t] = [] // 2
    
    for i in 0 ..< addresses.count {
      dispatch_group_enter(downloadGroup)
      let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) { // 3
        let index = Int(i)
        let address = addresses[index]
        let url = NSURL(string: address)
        let photo = DownloadPhoto(url: url!) {
          image, error in
          if let error = error {
            storedError = error
          }
          dispatch_group_leave(downloadGroup)
        }
        PhotoManager.sharedManager.addPhoto(photo)
      }
      blocks.append(block)
      dispatch_async(GlobalMainQueue, block) // 4
    }
    
    for block in blocks[3 ..< blocks.count] { // 5
      let cancel = arc4random_uniform(2) // 6
      if cancel == 1 {
        dispatch_block_cancel(block) // 7
        dispatch_group_leave(downloadGroup) // 8
      }
    }
    
    dispatch_group_notify(downloadGroup, GlobalMainQueue) {
      if let completion = completion {
        completion(error: storedError)
      }
    }
  }
  1. Массив addresses расширен и содержит уже три адреса.
  2. Этот массив будет содержать созданные экземпляры блоков для последующего использования.
  3. dispatch_block_create создает новый экземпляр блок. Первый параметр является флажком (flag) определения различных особенности (Traits) блоков. Используемый здесь флаг, заставляет блок наследовать класс QoS из очереди, в которую он отправлен. Второй параметр - это определение блока в виде замыкания.
  4. Здесь блок отправляется асинхронно в глобальную основную очередь. Для этого примера, использование основной очереди облегчает отмену выбранных блоков, так как это последовательная очередь. Код, который настраивает отправку блоков уже выполняется в главной очереди, так что вы гарантированно получите выполнение загруженных блоков через некоторое время.
  5. Первые три загрузки мы не трогаем, и массив нарезается, чтобы получить остальную часть.
  6. arc4random_uniform обеспечивает целочисленное значение между 0 и верхней границей (не включительно). Используя 2 для верхней границы, вы получите либо 0 либо 1, тут как повезет.
  7. Если случайное число будет 1, то блок будет отменен. То есть, если блок все еще находится в очереди и еще не началось выполнение. Блоки не могут быть отменены в середине исполнения.
  8. Поскольку все блоки будут добавлены в группу отправки (dispatch group), не забудьте удалить, те которые отменены.

Запустите приложение, и добавьте изображения из Le Internet. Вы увидите, что приложение теперь загружает по три каждого изображения и рандомно еще какое-то количество изображений дополнительно. Остальные были отменены после того, как они были отправлены в очередь. Это довольно надуманный пример, но он показывает, как блок отправки объектов используются и легко отменяются.

Блоки отправки объектов могут гораздо больше, так что поизучайте документацию.

Разные GCD удовольствия

Но постойте! Есть еще кое что! Существуют некоторые дополнительные функции, которые отходят чуть дальше от «проторенных» путей. Хотя вы не будете использовать эти инструменты особенно часто, но они могут быть чрезвычайно полезными в нужных ситуациях.

Тестирование асинхронного кода

Это может звучать по-сумасшедшему, но вы знаете, что Xcode имеет функцию тестирования? :]. Написание и выполнение тестов важный этап при строительстве сложных взаимоотношений в коде.

Тестирование в Xcode осуществляется на подклассах XCTestCase и прогоняет любой метод в своей подписи метода, который начинается с test. Тестирование измеряется в главном потоке, так что вы можете предположить, что каждый тест происходит в последовательном (serial) порядке.

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

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

Давайте кратко рассмотрим на две распространенные техники тестирования асинхронного кода: один с использованием semaphores и один с использованием expectations.

Semaphores

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

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

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

Откройте GooglyPuffTests.swift и замените downloadImageURLWithString следующей реализацией:

func downloadImageURLWithString(urlString: String) {
  let url = NSURL(string: urlString)
  let semaphore = dispatch_semaphore_create(0) // 1
  let photo = DownloadPhoto(url: url!) {
    image, error in
    if let error = error {
      XCTFail("\(urlString) failed. \(error.localizedDescription)")
    }
    dispatch_semaphore_signal(semaphore) // 2
  }
 
  let timeout = dispatch_time(DISPATCH_TIME_NOW, DefaultTimeoutLengthInNanoSeconds)
  if dispatch_semaphore_wait(semaphore, timeout) != 0 { // 3
    XCTFail("\(urlString) timed out")
  }
}

Вот как семафор работает в выше написанном коде:

  1. Создается семафор. Параметр указывает значение семафора, с которого он начинает. Это число является количеством элементов, которые могут получить доступ к семафору без необходимости сначала инкрементировать что-то (обратите внимание, что инкрементирование семафора известно как его сигнализиривание ).
  2. В завершающем замыкании вы сообщаете семафору, что вам больше не нужен ресурс. Это увеличивает (инкрементирует) значение счетчика (сount) семафора и появляется сигнал о том, что семафор доступен для других ресурсов, которым он нужен.
  3. Происходит ожидание семафора, с заданным тайм-аутом. Этот вызов блокирует текущий поток до момента, пока семафор не подаст сигнал. Ненулевое возвращаемое значение из этой функции означает, что тайм-аут был достигнут. В этом случае, тест не пройдет, потому что считается, что ожидание сети не должно занимать более 10 секунд чтобы вернуться - справедливо!

Запустите ваш тест, выбрав Product / Test в меню или используйте ⌘+U , если у вас стоит привязка к клавиатуре по умолчанию. Все они должны оперативно сработать.

Отключите соединение и запустите снова тестирование. Если вы работаете на устройстве, переведите его в режим полета. Если вы работаете на тренажере, то просто выключите соединение. Тест завершится неудачей через 10 секунд. Отлично, работает!

Это довольно тривиальные испытания, но если вы работаете с командой сервера, то эти основные тесты могут предотвратить выяснение, кто виноват в последнем вопросе по сети.

Ожидания (Expectations)

Фреймворки XCTest предлагают другое решение проблемы тестирования асинхронного кода в виде expectations (ожиданий). Эта функция позволяет настроить ожидание - то, что вы ожидаете, что произойдет - и после этого начать асинхронный задачу. Тогда вы можете заставить test runner ждать, пока асинхронная задача не отметит ожидания как выполненные.

Перейдите к GooglyPuffTests.swift и замените downloadImageURLWithString следующим кодом:

  func downloadImageURLWithString(urlString: String) {
    let url = NSURL(string: urlString)
    let downloadExpectation = expectationWithDescription("Image downloaded from \(urlString)") // 1
    let photo = DownloadPhoto(url: url!) {
      image, error in
      if let error = error {
        XCTFail("\(urlString) failed. \(error.localizedDescription)")
      }
      downloadExpectation.fulfill() // 2
    }
    
    waitForExpectationsWithTimeout(10) { // 3
      error in
      if let error = error {
        XCTFail(error.localizedDescription)
      }
    }
  }

Вот как это работает:

  1. Создается ожидание с expectationWithDescription. Test runner будет отображать строковый параметр в тестовом журнале после неудачи, так что вы описываете то, что вы ожидаете должно произойти.
  2. Вызываете fulfill в замыкании, которое выполняется асинхронно, чтобы отметить его как выполненное.
  3. Вызывающий поток ждет, что ожидания будут выполнены, вызывая waitForExpectationsWithTimeout. Если время ожидания выходит, то это считается ошибкой.

Запустите тестирования. Конечным результатом является практически тоже, что и при использовании семафора, но используя фреймворк XCTest является более «чистым» и читаемым решением.

Работа с Источниками Отправки

Особенностью GCD являются Dispatch Sources, которые представляют собой в основном сборную солянку низко уровневой функциональности, которые помогут вам отвечать или контролировать сигналы Unix, файловые дескрипторы (file descriptors), Mach порты, VFS узлов и другихе неясные вещи. Все это далеко выходит за рамки данного руководства, но вы получите небольшое представление об этом, путем реализации источника объекта отправки (dispatch source object) и использования его в довольно своеобразных целях.

Те пользователи, которые первый раз используют источники отправки, могут не понять, как использовать источник, поэтому первое, что вам нужно - это понять как работает dispatch_source_create. Это прототип функции для создания источника:

func dispatch_source_create(
  type: dispatch_source_type_t,
  handle: UInt,
  mask: UInt,
  queue: dispatch_queue_t!) -> dispatch_source_t!

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

Здесь вы будете следить за DISPATCH_SOURCE_TYPE_SIGNAL. Вот что сказано в документации:

Источник отправки, который отслеживает текущий процесс для отправки сигналов. Обработчик представляет собой число сигналов (int). Маска не используется (передается ноль на данный момент).

Список этих сигналов Unix может найти в файле заголовка signal.h. В топе стоят группа нескольких #define. Из этого списка сигналов, вы будете отслеживать сигнал SIGSTOP. Этот сигнал посылается, когда процесс получает указания о приостановке. Это тот же самый сигнал, который посылается, когда вы отлаживаете ваше приложение, используя LLDB дебаггер.

Перейдем к PhotoCollectionViewController.swift и добавим следующий код рядом с viewDidLoad. Вам будет нужно добавить эти два частных свойства в класс, и этот новый блок кода в начало viewDidLoad, после вызова суперкласса, но до существующей строчки, которая получает ALAssetLibrary:

#if DEBUG
private var signalSource: dispatch_source_t!
private var signalOnceToken = dispatch_once_t()
#endif
 
override func viewDidLoad() {
  super.viewDidLoad()
 
  #if DEBUG // 1
  dispatch_once(&signalOnceToken) { // 2
    let queue = dispatch_get_main_queue()
    self.signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL,
                                               UInt(SIGSTOP), 0, queue) // 3
    if let source = self.signalSource { // 4
      dispatch_source_set_event_handler(source) { // 5
        NSLog("Hi, I am: \(self.description)")
      }
      dispatch_resume(source) // 6
    }
  }
  #endif
 
  // Остальной код метода
}

Запустите ваше приложение; поставьте на паузу дебаггер и немедленно возобновите работу приложения. Проверьте консоль, и вы увидите что-то вроде этого в дебаггере:

  1. Этот код тоже принимает небольшое участие, так что давайте рассмотрим пошагово что происходит:
  2. Лучше всего просто компилировать этот код в режиме отладки (DEBUG mode), так как это может дать "заинтересованным сторонам" глубокое понимание происходящего :] DEBUG определяется путем добавления -D DEBUG в Project Settings -> Build Settings -> Swift Compiler – Custom Flags -> Other Swift Flags -> Debug.
  3. Используйте dispatch_once для выполнения настройки источников отправки.
  4. Здесь вы подтверждаете экземпляр переменной signalSource. Вы указываете, что заинтересованы в мониторинге сигнала и используете сигнал SIGSTOP в качестве второго параметра. Кроме того, вы можете использовать основную очередь для обработки полученных событий - скоро вы поймете почему.
  5. Объект источника отправки не будет создан, если вы предоставляете искаженные параметры. По этому, вы должны убедиться, что у вас есть действительный объект источника отправки прежде чем начать с ним работать.
  6. dispatch_source_set_event_handler регистрирует замыкание обработчика событий, который вызывается, когда вы получаете сигнал, который вы мониторили.
  7. По умолчанию, все источники находятся в подвешенном состоянии. Вы должны сказать исходному объекту, что хотите продолжить, когда вам нужно начать мониторить для получения событий.
2016-01-13 11:10:31.601 GooglyPuff[757:21324] Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7faf8070b3a0>

Теперь ваше приложение оповещено о возможных ошибках. Прекрасно! Но как это можно использовать в реальной жизни?

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

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

Только задумайтесь на секунду об этой ситуации. Когда вы внезапно останавливаете дебаггер, то вы почти никогда не окажетесь на нужном фрейма стека. Теперь вы можете остановить дебаггер в любое время и сделать так, что код будет выполняться в нужном месте. Это очень полезно, когда вы хотите выполнить код в каком-то месте вашего приложения, которое утомительно "отлавливать" через дебаггер. Обязательно попробуйте!

Установите брейкпоинт на выражение NSLog в viewDidLoad в обработчике событий, которое вы только что добавили. Поставьте на паузу дебаггер и снова запустите. Теперь приложение остановится на брейкпоинте, который вы добавили. Теперь вы находитесь глубоко в недрах вашего метода PhotoCollectionViewController. Теперь вы можете получить доступ к экземпляру PhotoCollectionViewController. Довольно удобно!

Заметка

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

В консоле дебаггера, введите следующую команду:

Дебаггер Xcode иногда может отказаться от сотрудничества. Если вы получите сообщение

error: use of unresolved identifier 'self'

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

(lldb) e let $vc = unsafeBitCast(0x7fd0b3e22bc0, GooglyPuff.PhotoCollectionViewController.self)
(lldb) po $vc.navigationItem.prompt = "WOOT!"

Затем возобновите выполнение приложения. Вы увидите следующее:

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

Скачать конечный проект.

Что дальше?

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

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

Источник урока: Источник