Как просто подождать какого-либо макета в iOS?

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

Только UIKit.

view.addItemsA()
view.addItemsB()
view.addItemsC()

Допустим, на iPhone 6s

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

Это произойдет:

один большой шаг

ОНИ ВИДЯТ ВСЕ ОДИН РАЗ. Повторим, экран просто зависает на 3 секунды, в то время как UIKit выполняет огромную работу. Тогда они все появляются сразу.

Но скажем, я хочу, чтобы это произошло:

ОНИ ПОЯВЛЯЮТСЯ ПРОГРЕССИВНО. Экран просто зависает на 1 секунду, пока UIKit его строит. Кажется. Он снова зависает, пока строит следующий. Кажется. И так далее.

(Обратите внимание, что "одна секунда" - просто простой пример для ясности. Более полный пример см. В конце этого поста.)

Как вы делаете это в iOS?

Вы можете попробовать следующее. Это не похоже на работу.

view.addItemsA()

view.setNeedsDisplay()
view.layoutIfNeeded()

view.addItemsB()

Вы можете попробовать это:

 view.addItemsA()
 view.setNeedsDisplay()
 view.layoutIfNeeded()_b()
 delay(0.1) { self._b() }
}

func _b() {
 view.addItemsB()
 view.setNeedsDisplay()
 view.layoutIfNeeded()
 delay(0.1) { self._c() }...
  • Обратите внимание, что если значение слишком мало - этот подход просто и, очевидно, ничего не делает. UIKit просто продолжит работать. (Что еще это будет делать?). Если значение слишком велико, это бессмысленно.

  • Обратите внимание, что в настоящее время (iOS10), если я не ошибаюсь: если вы попробуете этот трюк с трюком с нулевой задержкой, он работает в лучшем случае беспорядочно. (Как и следовало ожидать.)

Отключить цикл бега...

view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()

RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()

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

(то есть, UIKit от Apple теперь достаточно сложен, чтобы размазать работу UIKit за пределы этой "хитрости".)

Мысль: есть ли способ, в UIKit, получить обратный вызов, когда он, в основном, собрал все сложенные вами представления? Есть ли другое решение?

Похоже, одно из решений заключается в том, чтобы поместить подпредставления в контроллеры, чтобы получить обратный вызов didAppear и отслеживать их. Это кажется инфантильным, но, может быть, это единственная модель? Это действительно сработает? (Только одна проблема: я не вижу никакой гарантии, что didAppear гарантирует, что все подпредставления были нарисованы.)


В случае, если это все еще не ясно...

Пример повседневного использования:

• Скажите, что есть, возможно, семь разделов.

• Скажем, для построения каждого UIKit обычно требуется от 0,01 до 0,20 (в зависимости от того, какую информацию вы показываете).

• Если вы просто "отпустите все целиком за один удар", это часто будет нормально или приемлемо (общее время, скажем, от 0,05 до 0,15) ... но...

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

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

5 ответов

Решение

Оконный сервер имеет окончательный контроль над тем, что появляется на экране. iOS только отправляет обновления на сервер окна, когда текущий CATransaction стремится. Чтобы это произошло, когда это необходимо, iOS регистрирует CFRunLoopObserver для .beforeWaiting активность в цикле выполнения основного потока. После обработки события (предположительно путем вызова вашего кода) цикл выполнения вызывает наблюдателя до того, как он ожидает прибытия следующего события. Наблюдатель фиксирует текущую транзакцию, если она есть. Передача транзакции включает в себя запуск прохода макета, прохода отображения (в котором ваш drawRect методы), и отправка обновленного макета и содержимого на сервер окна.

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

Один из способов сделать это - позвонить CATransaction.flush(), Разумный случай для использования CATransaction.flush() когда вы хотите поставить новый CALayer на экране, и вы хотите, чтобы анимация была немедленно. Новый CALayer не будет отправлено на оконный сервер, пока транзакция не будет зафиксирована, и вы не сможете добавить анимацию, пока она не появится на экране. Итак, вы добавляете слой в иерархию слоев, вызываете CATransaction.flush(), а затем добавьте анимацию к слою.

Ты можешь использовать CATransaction.flush чтобы получить эффект, который вы хотите. Я не рекомендую это, но вот код:

@IBOutlet var stackView: UIStackView!

@IBAction func buttonWasTapped(_ sender: Any) {
    stackView.subviews.forEach { $0.removeFromSuperview() }
    for _ in 0 ..< 3 {
        addSlowSubviewToStack()
        CATransaction.flush()
    }
}

func addSlowSubviewToStack() {
    let view = UIView()
    // 300 milliseconds of “work”:
    let endTime = CFAbsoluteTimeGetCurrent() + 0.3
    while CFAbsoluteTimeGetCurrent() < endTime { }
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 44).isActive = true
    view.backgroundColor = .purple
    view.layer.borderColor = UIColor.yellow.cgColor
    view.layer.borderWidth = 4
    stackView.addArrangedSubview(view)
}

И вот результат:

CATransaction.flush demo

Проблема с вышеуказанным решением состоит в том, что он блокирует основной поток, вызывая Thread.sleep, Если ваш основной поток не отвечает на события, пользователь не только разочаровывается (потому что ваше приложение не реагирует на его прикосновения), но в конечном итоге iOS решит, что приложение зависло, и уничтожит его.

Лучший способ - просто запланировать добавление каждого представления, когда вы хотите, чтобы оно появилось. Вы утверждаете, что "это не инжиниринг", но вы ошибаетесь, и ваши причины не имеют смысла. iOS обычно обновляет экран каждые 16⅔ миллисекунд (если вашему приложению не требуется больше времени для обработки событий). Пока задержка, которую вы хотите, по крайней мере, так велика, вы можете просто запланировать запуск блока после задержки, чтобы добавить следующий вид. Если вы хотите задержку менее 16⅔ миллисекунд, вы вообще не можете ее иметь.

Итак, вот лучший, рекомендуемый способ добавить подпредставления:

@IBOutlet var betterButton: UIButton!

@IBAction func betterButtonWasTapped(_ sender: Any) {
    betterButton.isEnabled = false
    stackView.subviews.forEach { $0.removeFromSuperview() }
    addViewsIfNeededWithoutBlocking()
}

private func addViewsIfNeededWithoutBlocking() {
    guard stackView.arrangedSubviews.count < 3 else {
        betterButton.isEnabled = true
        return
    }
    self.addSubviewToStack()
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
        self.addViewsIfNeededWithoutBlocking()
    }
}

func addSubviewToStack() {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 44).isActive = true
    view.backgroundColor = .purple
    view.layer.borderColor = UIColor.yellow.cgColor
    view.layer.borderWidth = 4
    stackView.addArrangedSubview(view)
}

И вот (идентичный) результат:

DispatchQueue asyncAfter demo

TLDR

Принудительное изменение пользовательского интерфейса на сервере рендеринга с CATransaction.flush() или разделить работу на несколько кадров, используя CADisplayLink (пример кода ниже).


Резюме

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

нет

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

Есть ли другое решение?

Да, iOS может отображать изменения только один раз за кадр, но ваше приложение не то, что делает это отображение. Процесс оконного сервера есть.
Ваше приложение выполняет макет и рендеринг, а затем фиксирует свои изменения в layerTree на сервере рендеринга. Это будет сделано автоматически в конце цикла запуска, или вы можете принудительно отправить ожидающие транзакции на сервер визуализации. CATransaction.flush(),

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

Возможные решения

Это та часть, которая вас интересует.

1. Сделайте как можно больше в фоновой очереди и улучшите производительность.
Серьезно, iPhone 7 является третьим самым мощным компьютером (не телефоном) в моем доме, только побежденный моим игровым ПК и Macbook Pro. Это быстрее, чем любой другой компьютер в моем доме. Это не должно занять 3 секунды паузы для визуализации интерфейса ваших приложений.

2: очистка в ожидании CATransaction
РЕДАКТИРОВАТЬ: Как указал Роб Майофф, вы можете заставить CoreAnimation отправить ожидающие изменения на сервер визуализации, вызвав CATransaction.flush()

addItems1()
CATransaction.flush()
addItems2()
CATransaction.flush()
addItems3()

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

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

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

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

Документация Apple - "Лучшая документация для + [CATransaction flush]".

3:dispatch_after()
Просто задержите код до следующего цикла выполнения. dispatch_async(main_queue) не сработает, но вы можете использовать dispatch_after() без задержки.

addItems1()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
  addItems2()

  DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
    addItems3()
  }
}

Вы упоминаете в своем ответе, что это больше не работает для вас. Тем не менее, он отлично работает в тестовом Swift Playground и в приложении iOS, которое я включил в этот ответ.

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

DisplayQueue.sharedInstance.addItem {
  addItems1()
}
DisplayQueue.sharedInstance.addItem {
  addItems2()
}
DisplayQueue.sharedInstance.addItem {
  addItems3()
}

Нужен этот вспомогательный класс для работы (или аналогичный).

// A queue of item that you want to run one per frame (to allow the display to update in between)
class DisplayQueue {
  static let sharedInstance = DisplayQueue()
  init() {
    displayLink = CADisplayLink(target: self, selector: #selector(displayLinkTick))
    displayLink.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
  }
  private var displayLink:CADisplayLink!
  @objc func displayLinkTick(){
    if let _ = itemQueue.first {
      itemQueue.remove(at: 0)() // Remove it from the queue and run it
      // Stop the display link if it's not needed
      displayLink.isPaused = (itemQueue.count == 0)
    }
  }
  private var itemQueue:[()->()] = []
  func addItem(block:@escaping ()->()) {
    displayLink.isPaused = false // It's needed again
    itemQueue.append(block) // Add the closure to the queue
  }
}

5: вызовите runloop напрямую.
Мне не нравится это из-за возможности бесконечного цикла. Но я признаю, что это маловероятно. Я также не уверен, если это официально поддерживается, или инженер Apple собирается прочитать этот код и выглядеть в ужасе.

// Runloop (seems to work ok, might lead to infitie recursion if used too frequently in the codebase)
addItems1()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
addItems2()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
addItems3()

Это должно работать, если (при ответе на события runloop) вы не сделаете что-то еще, чтобы заблокировать завершение этого вызова runloop, поскольку CATransaction отправляются на сервер окна в конце runloop.


Пример кода

Демонстрационный проект Xcode и игровая площадка Xcode (Xcode 8.2, Swift 3)


Какой вариант я должен использовать?

Мне нравятся решения DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) а также CADisplayLink лучшее. Тем не мение, DispatchQueue.main.asyncAfter не гарантирует, что он будет работать на следующем тике runloop, так что вы можете не захотеть доверять ему?

CATransaction.flush() заставит вас вносить изменения в пользовательский интерфейс на сервер рендеринга, и это использование, кажется, соответствует комментариям Apple для класса, но сопровождается некоторыми предупреждениями.

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


Детальное объяснение

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


Обзор макета и рендеринга UIKit

CADisplayLink совершенно не связан с UIKit и runloop.

Не совсем. Пользовательский интерфейс iOS представляет собой графический процессор, похожий на 3D-игру. И пытается сделать как можно меньше. Поэтому многие вещи, такие как макет и рендеринг, происходят не тогда, когда что-то меняется, а когда это необходимо. Вот почему мы называем 'setNeedsLayout', а не подпредставления макета. Каждый кадр макет может меняться несколько раз. Тем не менее, iOS будет пытаться только позвонить layoutSubviews один раз за кадр вместо 10 раз setNeedsLayout возможно, был назван.

Тем не менее, довольно много происходит на процессоре (макет, -drawRect:и т.д...) так как же все это сочетается.

Обратите внимание, что все это упрощено и пропускает множество вещей, например, CALayer на самом деле является реальным объектом, который отображается на экране, а не UIView, и т. Д.

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

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

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

LayoutSubviews и DrawRect

-setNeedsLayout делает недействительными представления текущего макета и помечает его как необходимый макет.
-layoutIfNeeded будет ретранслировать представление, если оно не имеет правильного макета

-setNeedsDisplay пометит представления как нуждающиеся в перерисовке. Ранее мы говорили, что каждый вид отображается в виде текстуры / изображения вида, который может перемещаться и управляться графическим процессором без необходимости перерисовки. Это заставит его перерисовать. Розыгрыш делается по телефону -drawRect: на процессоре и так медленнее, чем возможность полагаться на графический процессор, который он может делать большинство кадров.

И важно отметить, что эти методы не делают. Методы верстки не делают ничего визуального. Хотя, если мнения contentMode установлен в redrawизменение рамки представлений может сделать недействительным отображение представлений (триггер -setNeedsDisplay).

Вы можете попробовать следующий весь день. Это не похоже на работу:

view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsC()
view.setNeedsDisplay()
view.layoutIfNeeded()

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

В вашем примере view.addItemsA() добавляет 100 подвидов. Это отдельные несвязанные слои / текстуры в графическом процессоре, пока графический процессор не объединит их вместе в следующий кадровый буфер. Единственное исключение из этого, если CALayer имеет shouldRasterize установите в true. В этом случае он создает отдельную текстуру для представления, а также его подвиды и визуализирует (в виде на GPU) вид и его подвиды в одну текстуру, эффективно кэшируя компоновку, которую он должен выполнять в каждом кадре. Это дает преимущество в производительности, так как не требует компоновки всех своих подпредставлений в каждом кадре. Тем не менее, если представление или его подпредставления часто изменяются (например, во время анимации), это приведет к снижению производительности, так как будет часто делать недействительной кэшированную текстуру, требующую ее перерисовки (аналогично частым вызовам). -setNeedsDisplay).


Теперь любой игровой инженер просто сделает это...

view.addItemsA()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsB()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())

view.addItemsC()

Теперь, похоже, это работает.

Но почему это работает?

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

Так что основной цикл выполнения в UIKit, вероятно, выглядит примерно так.

-(void)runloop
{
    //... do touch handling and other events, etc...

    self.windows.recursivelyCheckLayout() // effectively call layoutIfNeeded on everything
    self.windows.recursivelyDisplay() // call -drawRect: on things that need it
    GPU.recompositeFrame() // render all the layers into the frame buffer for this frame and displays it on screen
}

Итак, вернемся к исходному коду.

view.addItemsA() // Takes 1 second
view.addItemsB() // Takes 1 second
view.addItemsC() // Takes 1 second

Так почему же все 3 изменения появляются сразу через 3 секунды, а не по одному с интервалом в 1 секунду?

Хорошо, если этот бит кода выполняется в результате нажатия кнопки или подобного, он выполняет синхронную блокировку основного потока (поток UIKit требует внесения изменений в пользовательский интерфейс), и поэтому блокирует цикл выполнения в строке 1, даже обрабатывающая часть. По сути, вы заставляете эту первую строку метода runloop возвращаться через 3 секунды.

Однако мы определили, что макет не будет обновляться до 3-й строки, отдельные представления не будут отображаться до 4-й строки, и никакие изменения не будут отображаться на экране до последней строки метода runloop, строка 5.

Причина того, что откачка runloop работает вручную, заключается в том, что вы в основном вставляете вызов в runloop() метод. Ваш метод выполняется в результате вызова из функции runloop

-runloop()
   - events, touch handling, etc...
      - addLotsOfViewsPressed():
         -addItems1() // blocks for 1 second
         -runloop()
         |   - events and touch handling
         |   - layout invalid views
         |   - redraw invalid views
         |   - tell GPU to composite and display a new frame
         -addItem2() // blocks for 1 second
         -runloop()
         |   - events // hopefully nothing massive like addLotsOfViewsPressed()
         |   - layout
         |   - drawing
         |   - GPU render new frame
         -addItems3() // blocks for 1 second
  - relayout invalid views
  - redraw invalid views
  - GPU render new frame

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


КОНЕЦ


Ниже этот пункт просто уточнение.


Дополнительная информация о том, что здесь происходит


CADisplayLink и NSRunLoop

Если я не ошибаюсь, KH, по-видимому, в принципе вы считаете, что "цикл выполнения" (то есть: этот: RunLoop.current) - это CADisplayLink.

Runloop и CADisplayLink - это не одно и то же. Но CADisplayLink подключается к runloop для того, чтобы работать.
Я немного ошибся ранее (в чате), когда сказал, что NSRunLoop вызывает CADisplayLink каждый тик. Это не так. Насколько я понимаю, NSRunLoop - это, по сути, цикл while(1), работа которого заключается в том, чтобы поддерживать поток в рабочем состоянии, обрабатывать события и т. Д. Во избежание проскальзывания я попытаюсь подробно процитировать цитату из собственной документации Apple для следующих битов.

Цикл выполнения очень похож на звуки. Это цикл, в который ваш поток входит и использует для запуска обработчиков событий в ответ на входящие события. Ваш код предоставляет операторы управления, используемые для реализации фактической части цикла цикла выполнения - другими словами, ваш код обеспечивает while или же for цикл, который управляет циклом выполнения. Внутри вашего цикла вы используете объект цикла выполнения для "запуска" кода обработки событий, который получает события и вызывает установленные обработчики.
Анатомия цикла выполнения - Руководство по программированию потоков - developer.apple.com

CADisplayLink использования NSRunLoop и должен быть добавлен к одному, но отличается. Чтобы процитировать заголовочный файл CADisplayLink:

"Если не сделать паузу, он будет запускать каждую vsync, пока не будет удален".
От: func add(to runloop: RunLoop, forMode mode: RunLoopMode)

И из preferredFramesPerSecond документация по свойствам.

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

Так что если вы хотите сделать что-нибудь, приуроченное к обновлению экрана CADisplayLink (с настройками по умолчанию) это то, что вы хотите использовать.

Представляем сервер рендеринга

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

Не совсем. Причина, по которой мы должны касаться только UIView из основного потока, заключается в том, что UIKit не является потокобезопасным и работает в основном потоке. Если вы заблокируете основной поток, вы заблокировали поток, на котором работает UIKit.

Работает ли UIKit "как вы говорите" {... "отправьте сообщение, чтобы остановить видеокадры. Сделайте всю нашу работу! Отправьте другое сообщение, чтобы снова запустить видео!"}

Это не то, что я говорю.

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

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

Как уже говорилось в разделе "Обзор макета и рендеринга UIKit" в моем ответе, UIKit старается не работать заранее. -setNeedsLayout а также -setNeedsDisplay может вызываться столько раз, сколько вы хотите. Они только делают недействительным макет и представление, если он уже был недействительным в этом кадре, то второй вызов ничего не делает. Это означает, что если все 10 изменений сделают недействительным макет представления, UIKit все равно нужно будет только заплатить стоимость перерасчета макета один раз (если вы не использовали -layoutIfNeeded между -setNeedsLayout звонки).

То же самое относится и к -setNeedsDisplay, Хотя, как обсуждалось ранее, ни один из них не относится к тому, что появляется на экране. layoutIfNeeded обновляет фрейм представлений и displayIfNeeded обновляет текстуру визуализации видов, но это не связано с тем, что появляется на экране. Представьте себе, что каждый UIView имеет переменную UIImage, которая представляет его резервное хранилище (на самом деле он находится в CALayer или ниже и не является UIImage. Но это иллюстрация). Перерисовка этого представления просто обновляет UIImage. Но UIImage - это всего лишь данные, а не графика на экране, пока она не будет что-то нарисована на экране.

Так как же нарисовать UIView на экране?

Ранее я написал псевдокод UIKit для основного рендера runloop. До сих пор в своих ответах я игнорировал значительную часть UIKit, не все это работает внутри вашего процесса. Удивительное количество материалов UIKit, связанных с отображением вещей, на самом деле происходит в процессе сервера рендеринга, а не в процессе ваших приложений. Сервер рендеринга / оконный сервер был SpringBoard (интерфейс домашнего экрана) до iOS 6 (с тех пор BackBoard и FrontBoard стали использовать в SpringBoard больше функций, связанных с ядром ОС, что позволило ему сосредоточиться на том, чтобы быть основным интерфейсом операционной системы. экран / экран блокировки / центр уведомлений / центр управления / переключатель приложений / и т. д.).

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

-(void)runloop
{
    //... do touch handling and other events, etc...

    UIWindow.allWindows.layoutIfNeeded() // effectively call layoutIfNeeded on everything
    UIWindow.allWindows.recursivelyDisplay() // call -drawRect: on things that need to be rerendered

    // Sends all the changes to the render server process to actually make these changes appear on screen
    // CATransaction.flush() which means:
    CoreAnimation.commit_layers_animations_to_WindowServer()
} 

Это имеет смысл, одно зависание iOS-приложения не может заморозить все устройство. Фактически мы можем продемонстрировать это на iPad с двумя приложениями, работающими рядом. Когда мы заставляем одного заморозить другого, это не затрагивается.

Это 2 пустых шаблона приложения, которые я создал и вставил в оба кода. Оба должны текущее время в метке в середине экрана. Когда я нажимаю заморозить это вызывает sleep(1) и зависает приложение. Все останавливается Но iOS в целом в порядке. Другое приложение, центр управления, центр уведомлений и т. Д. Все это не затрагивает.

Работает ли UIKit "как вы говорите" {... "отправьте сообщение, чтобы остановить видеокадры. Сделайте всю нашу работу! Отправьте другое сообщение, чтобы снова запустить видео!"}

В приложении нет UIKit stop video frames команда, потому что ваше приложение не имеет никакого контроля над экраном вообще. Экран будет обновляться со скоростью 60FPS, используя любой кадр, который ему дает сервер окон. Оконный сервер скомпонует новый кадр для отображения со скоростью 60FPS, используя последние известные позиции, текстуры и деревья слоев, с которыми ваше приложение дало ему работать.
Когда вы замораживаете основной поток в вашем приложении, CoreAnimation.commitLayersAnimationsToWindowServer() линия, которая идет последней (после вашей дорогой add lots of views код), заблокирован и не запускается. В результате, даже если есть изменения, оконный сервер еще не отправил их, и поэтому просто продолжает использовать последнее состояние, которое было отправлено для вашего приложения.

Анимации - это еще одна часть UIKit, которая не работает в оконном сервере. Если до sleep(1) в этом примере приложения мы сначала запускаем анимацию UIView, видим, как она запускается, затем метка останавливается и перестает обновляться (потому что sleep() побежал). Однако, несмотря на то, что основной поток приложений заморожен, анимация будет продолжаться независимо.

func freezePressed() {
    var newFrame = animationView.frame
    newFrame.origin.y = 600

    UIView.animate(withDuration: 3, animations: { [weak self] in
        self?.animationView.frame = newFrame
    })

    // Wait for the animation to have a chance to start, then try to freeze it
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        NSLog("Before freeze");
        sleep(2) // block the main thread for 2 seconds
        NSLog("After freeze");
    }
}

Это результат:

На самом деле мы можем пойти лучше.

Если мы изменим freezePressed() метод к этому.

func freezePressed() {
    var newFrame = animationView.frame
    newFrame.origin.y = 600

    UIView.animate(withDuration: 4, animations: { [weak self] in
        self?.animationView.frame = newFrame
    })

    // Wait for the animation to have a chance to start, then try to freeze it
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
        // Do a lot of UI changes, these should completely change the view, cancel its animation and move it somewhere else
        self?.animationView.backgroundColor = .red
        self?.animationView.layer.removeAllAnimations()
        newFrame.origin.y = 0
        newFrame.origin.x = 200
        self?.animationView.frame = newFrame
        sleep(2) // block the main thread for 2 seconds, this will prevent any of the above changes from actually taking place
    }
}

Сейчас без sleep(2) Вызовите анимацию, которая будет работать в течение 0,2 секунд, затем она будет отменена, и представление будет перемещено в другую часть экрана другого цвета. Однако неактивный вызов блокирует основной поток на 2 секунды, что означает, что ни одно из этих изменений не отправляется на сервер окон до тех пор, пока большая часть анимации не будет пройдена.

И просто для подтверждения здесь результат с sleep() строка закомментирована.

Надеюсь, это должно объяснить, что происходит. Эти изменения похожи на UIView, который вы добавляете в свой вопрос. Они ставятся в очередь для включения в следующее обновление, но поскольку вы блокируете основной поток, отправляя так много за один раз, вы останавливаете отправляемое сообщение, что приводит к их включению в следующий кадр. Следующий кадр не блокируется, iOS создаст новый кадр, отображающий все обновления, полученные от SpringBoard и других приложений iOS. Но поскольку ваше приложение по-прежнему блокирует свой основной поток, iOS не получала никаких обновлений от вашего приложения и поэтому не будет показывать никаких изменений (если только у него нет изменений, таких как анимации, уже помещенных в очередь на оконном сервере).

Итак, подведем итог

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

Это своего рода решение. Но это не инженерия.

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

3 метода, которые могут работать ниже. Во-первых, я мог бы заставить его работать, если подпредставление также добавляет контроллер, если оно не находится непосредственно в контроллере представления. Второе - это настраиваемое представление:) Кажется, вы задаетесь вопросом, когда layoutSubviews завершен для представления для меня. Этот непрерывный процесс - то, что замораживает показ из-за 1000 подпредставлений плюс последовательно. В зависимости от вашей ситуации вы можете добавить представление childviewcontroller и опубликовать уведомление, когда viewDidLayoutSubviews() будет завершен, но я не знаю, подходит ли это для вашего варианта использования. Я протестировал с 1000 подпрограмм на добавляемом представлении viewcontroller, и это сработало. В этом случае задержка 0 сделает именно то, что вы хотите. Вот рабочий пример.

 import UIKit
 class TrackingViewController: UIViewController {

    var layoutCount = 0
    override func viewDidLoad() {
        super.viewDidLoad()

          // Add a bunch of subviews
    for _ in 0...1000{
        let view = UIView(frame: self.view.bounds)
        view.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        view.backgroundColor = UIColor.green
        self.view.addSubview(view)
    }

}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    print("Called \(layoutCount)")
    if layoutCount == 1{
        //finished because first call was an emptyview
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }
    layoutCount += 1
} }

Затем в вашем главном View Controller, который вы добавляете подпредставления, вы можете сделать это.

import UIKit

class ViewController: UIViewController {

    var y :CGFloat = 0
    var count = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        NotificationCenter.default.addObserver(self, selector: #selector(ViewController.finishedLayoutAddAnother), name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4, execute: {
            //add first view
            self.finishedLayoutAddAnother()
        })
    }

    deinit {
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "kLayoutFinished"), object: nil)
    }

    func finishedLayoutAddAnother(){
        print("We are finished with the layout of last addition and we are displaying")
        addView()
    }

    func addView(){
        // we keep adding views just to cause
        print("Fired \(Date())")
        if count < 100{
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {

                // let test = TestSubView(frame: CGRect(x: self.view.bounds.midX - 50, y: y, width: 50, height: 20))
                let trackerVC = TrackingViewController()
                trackerVC.view.frame = CGRect(x: self.view.bounds.midX - 50, y: self.y, width: 50, height: 20)
                trackerVC.view.backgroundColor = UIColor.red
                self.view.addSubview(trackerVC.view)
                trackerVC.didMove(toParentViewController: self)
                self.y += 30
                self.count += 1
            })
        }
    }
}

Или есть ДАЖЕ сумасшедший путь и, возможно, лучший путь. Создайте свое собственное представление, которое в некотором смысле сохраняет свое время и перезванивает, когда хорошо не пропустить кадры Это неполированный, но будет работать.

завершение GIF

import UIKit
class CompletionView: UIView {
    private var lastUpdate : TimeInterval = 0.0
    private var checkTimer : Timer!
    private var milliSecTimer : Timer!
    var adding = false
    private var action : (()->Void)?
    //just for testing
    private var y : CGFloat = 0
    private var x : CGFloat = 0
    //just for testing
    var randomColors = [UIColor.purple,UIColor.gray,UIColor.green,UIColor.green]


    init(frame: CGRect,targetAction:(()->Void)?) {
        super.init(frame: frame)
        action = targetAction
        adding = true
        for i in 0...999{
            if y > bounds.height - bounds.height/100{
                y -= bounds.height/100
            }

            let v = UIView(frame: CGRect(x: x, y: y, width: bounds.width/10, height: bounds.height/100))
            x += bounds.width/10
            if i % 9 == 0{
                x = 0
                y += bounds.height/100
            }

            v.backgroundColor = randomColors[Int(arc4random_uniform(4))]
            self.addSubview(v)
        }

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    func milliSecCounting(){
        lastUpdate += 0.001
    }

    func checkDate(){
        //length of 1 frame
        if lastUpdate >= 0.003{
            checkTimer.invalidate()
            checkTimer = nil
            milliSecTimer.invalidate()
            milliSecTimer = nil
            print("notify \(lastUpdate)")
            adding = false
            if let _ = action{
                self.action!()
            }
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        lastUpdate = 0.0
        if checkTimer == nil && adding == true{
            checkTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(CompletionView.checkDate), userInfo: nil, repeats: true)
        }

        if milliSecTimer == nil && adding == true{
             milliSecTimer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(CompletionView.milliSecCounting), userInfo: nil, repeats: true)
        }
    }
}


import UIKit

class ViewController: UIViewController {

    var y :CGFloat = 30
    override func viewDidLoad() {
        super.viewDidLoad()
        // Wait 3 seconds to give the sim time
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: {
            [weak self] in
            self?.addView()
        })
    }

    var count = 0
    func addView(){
        print("starting")
        if count < 20{
            let completionView = CompletionView(frame: CGRect(x: 0, y: self.y, width: 100, height: 100), targetAction: {
                [weak self] in
                self?.count += 1
                self?.addView()
                print("finished")
            })
            self.y += 105
            completionView.backgroundColor = UIColor.blue
            self.view.addSubview(completionView)
        }
    }
}

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

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {
  //code})

Использование NSNotification добиться необходимого эффекта.

Первый - зарегистрировать наблюдателя на главном экране и создать обработчик наблюдателя.

Затем инициализируйте все эти объекты A,B,C... в отдельном потоке (например, в фоне), например, self performSelectorInBackground

Затем - опубликовать уведомление от подпредставлений и последнее - performSelectorOnMainThread добавить подпредставление в желаемом порядке с необходимыми задержками.
Чтобы ответить на вопросы в комментариях, скажем, у вас есть UIViewController, который был показан на экране. Этот объект - не предмет обсуждения, и вы можете решить, куда поместить код, контролирующий внешний вид представления. Код для объекта UIViewController (так что он сам). View - некоторый объект UIView, рассматриваемый как родительский вид. ViewN - одно из подпредставлений. Это можно масштабировать позже.

[[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(handleNotification:) 
    name:@"ViewNotification"
    object:nil];

За этим зарегистрировался наблюдатель - нужно было общаться между потоками. ViewN * V1 = [[ViewN alloc] init]; Здесь - могут быть размещены подпредставления - пока не показаны.

- (void) handleNotification: (id) note {
    ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"];
    [self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE];
}

Этот обработчик позволяет получать сообщения и размещать UIViewвозражать против родительского представления. Выглядит странно, но суть в том, что вам нужно выполнить метод addSubview в основном потоке, чтобы эффект вступил в силу. performSelectorOnMainThread позволяет начать добавление подпредставления в основном потоке, не блокируя выполнение приложения.

Теперь - мы создаем метод, который будет размещать подпредставления на экране.

-(void) sendToScreen: (id) obj {
    NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil];
[[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess];
}

Этот метод отправит уведомление из любого потока, отправив объект как элемент NSDictionary с именем ViewArrived.
И наконец - просмотры, которые должны быть добавлены с задержкой в ​​3 секунды:

-(void) initViews {
    ViewN * V1 = [[ViewN alloc] init];
    ViewN * V2 = [[ViewN alloc] init];
    ViewN * V3 = [[ViewN alloc] init];
    [self performSelector: @selector(sendToScreen:) withObject: V1 afterDelay: 3.0];
    [self performSelector: @selector(sendToScreen:) withObject: V2 afterDelay: 6.0];
    [self performSelector: @selector(sendToScreen:) withObject: V3 afterDelay: 9.0];
}

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

Другие вопросы по тегам