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

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

У меня есть небольшой пример, чтобы проиллюстрировать проблему, где я добавил функцию 'wait in millisecs', чтобы смоделировать время обработки и показать задержку обновления пользовательского интерфейса.

В этом примере 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 используется для обновления пользовательского интерфейса в основном потоке.

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

4 ответа

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

Но то, что вы делаете, не совсем правильно. Вы также выполняете обработку в основном потоке (функция 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

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

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

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

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

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

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

Вы пробовали CADisplayLink? По умолчанию он вызывает обновление экрана каждые 60 секунд (это решило некоторые из моих проблем): https://developer.apple.com/documentation/quartzcore/cadisplaylink

      let displayLink = CADisplayLink(target: self, selector: #selector(updateUI))
displayLink.add(to: .current, forMode: .common)

А потом:

      @objc func updateUI() {
    print("Updating UI!")
}
Другие вопросы по тегам