Закрытие против шаблона делегирования

Я работаю с классами сетевых запросов, и я обеспокоен сбоями. Например, работа с закрытием очень проста, поскольку вы передаете метод обратного вызова функции:

// some network client
func executeHttpRequest(#callback: (success: Bool) -> Void) {
    // http request

    callback(true)
}

// View Controller
func reload() {
    networkClient.executeHttpRequest() { (success) -> Void in
        self.myLabel.text = "it succeeded" // NOTE THIS CALL
    }
}

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

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

Итак, когда обратный вызов, наконец, запускается, self.myLabel.text может привести к сбою, поскольку диспетчер представлений, с которым self ссылался, уже мог быть освобожден.

До этого момента. Правильно ли я или быстро внедряю что-то внутренне, чтобы этого не происходило?

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

// some network client

// NOTE this variable is an OPTIONAL and it also a WEAK REFERENCE
weak var delegate: NetworkClientDelegate?

func executeHttpRequest() {
    // http request

    if let delegate = self.delegate {
        delegate.callback(success: true)
    }
}

Обратите внимание, что self.delegate, так как это weak reference, он будет указывать на nil, если диспетчер представлений (который реализует протокол NetworkClientDelegate) будет освобожден, и в этом случае вызов не будет вызван.

Мой вопрос будет: у закрытий есть что-то особенное, что делает их хорошим выбором в сценариях, подобных этому, вместо того, чтобы возвращаться к шаблону делегирования? Было бы неплохо, если бы были приведены примеры замыканий (которые не будут попадать в аварийные ситуации из-за указателя nil). Спасибо.

Ответ 1

Итак, когда обратный вызов, наконец, запущен, self.myLabel.text может привести к сбою, поскольку контроллер просмотра, к которому я обращался, уже может быть освобожден.

Если self было импортировано в закрытие в качестве сильной ссылки, гарантируется, что self не будет освобожден до завершения завершения закрытия. То есть контроллер просмотра все еще жив, когда замыкание вызвано - даже если его вид не отображается в это время. Заявление self.myLabel.text = "it succeeded" будет выполнено, но даже метка не будет видна, это не сработает.

Есть, однако, тонкая проблема, которая может привести к краху при определенных обстоятельствах:

Предположим, что замыкание имеет последнюю и только сильную ссылку на контроллер вида. Закрытие заканчивается и впоследствии освобождается, что также освобождает последнюю сильную ссылку на контроллер вида. Это неизбежно вызовет метод dealloc контроллера вида. Метод dealloc будет выполняться в том же потоке, где будет выполняться закрытие. Теперь, когда диспетчер представлений является объектом UIKit, он ДОЛЖЕН быть гарантирован, что все методы, отправленные этому объекту, будут выполняться в основном потоке. Таким образом, IFF dealloc будет фактически выполнен на каком-то другом потоке, ваш код может упасть.

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

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

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

Смотрите также: Использование слабой функции self в функции dispatch_async

Edit:

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

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

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

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

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

Ответ 2

Если бы я был вами, я бы использовал закрытие, так как они более удобны и гибки, чем делегирование в этом сценарии.

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

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

viewController.isViewLoaded && viewController.view.window

Что касается приложения, входящего в фоновый режим/переднего плана, вы можете приостановить/возобновить операции, переопределив applicationDidEnterBackground и applicationWillEnterForeground или зарегистрировавшись для соответствующих уведомлений: UIApplicationWillEnterForegroundNotification/UIApplicationDidEnterBackgroundNotification

Я настоятельно рекомендую вам использовать AFNetworking, так как API предлагает все, что я упоминал выше, и многое другое.