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

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

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

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

class ViewController: UIViewController {

    @IBOutlet weak var label1: UILabel!

    @IBOutlet weak var label2: UILabel!

    override func viewDidLoad() {
       super.viewDidLoad()

        DispatchQueue.main.async {
           os_log("before")
           self.label1.text = "updated label 1 1111"
           self.label2.text = "updated label 2 2222"
           self.waitForMilliSecs(MilliSecs: 300)
           os_log("after")
       }

}

func waitForMilliSecs(MilliSecs millisecs: Int) -> Void {
    var date = NSDate()
    let firstTime = Int64(date.timeIntervalSince1970 * 1000)
    var currentTime = firstTime
    while currentTime - firstTime < millisecs {
        date = NSDate()
        currentTime = Int64(date.timeIntervalSince1970 * 1000)
    }
}

Реальный вариант использования заключается в том, что я очищаю HTML-страницу для данных, а затем обновляю пользовательский интерфейс некоторым содержимым страницы. Обработчик завершения вызывается из URLSession.shared.dataTask в фоновом потоке, поэтому замыкание DispatchQueue.main.async используется для обновления пользовательского интерфейса в основном потоке.

Есть ли лучший способ обновить пользовательский интерфейс? Есть ли способ принудительно обновить события в основном потоке?


person BobS    schedule 05.01.2018    source источник
comment
Обновления пользовательского интерфейса выполняются в основном потоке. Все, что занимает достаточно времени для замедления обновлений пользовательского интерфейса, может и должно выполняться в альтернативных потоках. Обработайте свою HTML-страницу в фоновом потоке, затем обновите элементы пользовательского интерфейса в основном потоке.   -  person FryAnEgg    schedule 05.01.2018
comment
Спасибо за отзыв. Я поместил код извлечения html-страницы в поток DispatchQueue.global(qos: .userInitiated).async, и теперь я получаю лучшие результаты.   -  person BobS    schedule 06.01.2018
comment
Если это новая задержка и вы используете последние версии iOS, возможно, вы видите замедление, добавленное Apple в результате проблемы с батареей. Мы видели несколько проблем с последней версией ОС, поэтому вы можете столкнуться с ними.   -  person BonanzaDriver    schedule 07.01.2018
comment
Я использую последнюю версию ОС.   -  person BobS    schedule 10.01.2018


Ответы (3)


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

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

override func viewDidLoad() {
   super.viewDidLoad()
   DispatchQueue.global().async {
       print("before")
       //this function is doing some real work and produces some results.
       self.waitForMilliSecs(MilliSecs: 3000)
       print("after")

       DispatchQueue.main.async {
            self.label1.text = "updated label 1 1111"
            self.label2.text = "updated label 2 2222"
       }
    }
}

репозиторий github, показывающий весь пример: https://github.com/jurajantas/TestOfBackgroundProcessing.git

person Juraj Antas    schedule 06.01.2018
comment
Я поместил этот код в свое тестовое приложение, и у него все еще была случайная задержка от нескольких до многих секунд при обновлении меток в пользовательском интерфейсе. Я ожидаю, что после задержки ожидания в X миллисекунд пользовательский интерфейс обновится. Но это не так. Неверно ли мое предположение о том, когда должно произойти обновление? - person BobS; 10.01.2018
comment
обновление пользовательского интерфейса происходит после того, как вы увидите после в консоли журнала. - person Juraj Antas; 10.01.2018
comment
если у вас есть последняя версия xcode (9.2), есть новая функция под названием Thread sanitizer. Он предупредит вас, когда вы обновите пользовательский интерфейс из фонового потока. Если вы обновляете пользовательский интерфейс из фонового потока, вы можете видеть, что обновление происходит медленнее (также оно не является потокобезопасным). Подробнее здесь: developer.apple.com/documentation/code_diagnostics/ - person Juraj Antas; 10.01.2018
comment
Обновление этикетки происходит через несколько секунд после печати «после». Я ожидаю, что этикетка будет обновляться в то же время, когда происходит печать «после». Я использую xcode 9.2 и видел предупреждение об обновлении пользовательского интерфейса в неправильном потоке. Однако этого больше не происходит с изменениями в фоновом потоке заземления. Кстати, спасибо за ваши комментарии - person BobS; 11.01.2018
comment
Я создал репозиторий github, который показывает, как это работает. Пользовательский интерфейс обновляется немедленно. github.com/jurajantas/TestOfBackgroundProcessing.git - person Juraj Antas; 12.01.2018
comment
Спасибо за пример кода и информацию о том, что это работает для вас. Я скачал тест, запустил его и, к сожалению, получил те же результаты: обновление метки занимает несколько секунд. Операторы печати указывали время выполнения, а обновление метки произошло через несколько секунд после оператора печати «после». Может ли эта проблема быть аппаратной проблемой или проблемой конфигурации Mac OS? - person BobS; 13.01.2018
comment
трудно сказать, в чем проблема, но это явно не код. Убедитесь, что вы загружаете подлинное программное обеспечение от Apple. Был момент, когда скачал Xcode с 3-го. партия включала вредоносное ПО. Я был бы удивлен, если бы это произошло снова, и теперь он майнит некоторую криптовалюту в фоновом режиме. Если вы используете сторонние библиотеки, убедитесь, что вы понимаете, что они делают. Иногда вы можете получить больше, чем просили. - person Juraj Antas; 15.01.2018
comment
спасибо за вклад и ваше время. Если найду причину, отпишусь. - person BobS; 15.01.2018

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

Что касается вашего реального варианта использования - весь пользовательский интерфейс должен был быть обновлен в основном потоке (в противном случае могут произойти некоторые непредсказуемые артефакты, включая частичные обновления и неожиданные изменения цвета). GCD (DispatchAsyn) — наиболее естественный способ сделать это, однако доступно множество сторонних разработчиков для упрощения асинхронных операций. Как https://cocoapods.org/pods/ResultPromises

person MichaelV    schedule 05.01.2018
comment
Спасибо за информацию о сторонних инструментах для помощи в этом типе действий. я посмотрю на это - person BobS; 06.01.2018

Когда вы используете значение 300 в функции waitForMilliSecs, оно кажется мгновенным, но это не так. Достаточно небольшого времени, чтобы вы не заметили, что пользовательский интерфейс заблокировался, пока ваш код вращается в основном потоке.

Причина, по которой текст не обновляется сразу после вызова self.label1.text = "updated label 1 1111", заключается в том, что изменения пользовательского интерфейса не происходят немедленно. Пользовательский интерфейс обновляется с определенной частотой (60 Гц или 120 Гц). Каждое сделанное вами изменение станет видимым на экране в следующем цикле рендеринга.

Проверьте использование ЦП во время среза 300 мс, когда он находился в ожидании. Вы бы увидели, как он приближается к 100%, что очень плохо.

Что вы в конечном итоге пытаетесь сделать?

person Anurag    schedule 06.01.2018
comment
Я пытаюсь получить веб-страницу, извлечь данные со страницы, а затем установить данные в некоторые метки и обновить таблицу. Внутри метода viewDidLoad() я помещаю URLSession.dataTask(with: request, completeHandler:...) в замыкание DispatchQueue.global(qos:...).async(), чтобы гарантировать, что запрос был в фоновом режиме . Обработчик завершения имеет DispatchQueue.main.async{} вокруг вызовов UILabels и table.reloadData(). - person BobS; 10.01.2018
comment
Однако обновление пользовательского интерфейса имеет задержки, которые занимают от нескольких до нескольких секунд для обновления пользовательского интерфейса. Я смотрел потоки и вызовы в отладчике, и вызовы, казалось, были на правильном фоне и в основных потоках. Почему я попробовал цикл ожидания, чтобы проверить, когда эта проблема возникает. Мой поиск страницы и обработка HTML занимает около 0,5-0,7 миллисекунды, и я пытался увидеть, была ли это проблема времени. - person BobS; 10.01.2018
comment
исправление: должно быть «0,5-0,7 секунды», а не 0,5-0,7 миллисекунды - person BobS; 15.01.2018