ios - apple notes windows
¿Cómo, simplemente, esperar cualquier diseño en iOS? (5)
Antes de comenzar, tenga en cuenta que esto no tiene nada que ver con el procesamiento en segundo plano. No hay un "cálculo" involucrado que uno haría fondo.
Sólo UIKit .
view.addItemsA()
view.addItemsB()
view.addItemsC()
Digamos en un iPhone 6s
CADA uno de ellos toma un segundo para que UIKit lo construya.
Esto es lo que va a ocurrir:
Aparecen todos a la vez. Para repetir, la pantalla simplemente se cuelga durante 3 segundos, mientras que UIKit realiza una gran cantidad de trabajo. Entonces todos aparecen a la vez.
Pero digamos que quiero que esto suceda:
APARECEN PROGRESIVAMENTE. La pantalla simplemente se cuelga durante 1 segundo mientras UIKit construye uno. Aparece. Se cuelga de nuevo mientras se construye el siguiente. Aparece. Y así.
(Tenga en cuenta que "un segundo" es solo un ejemplo simple para mayor claridad. Vea el final de esta publicación para un ejemplo más completo).
¿Cómo lo haces en iOS?
Puedes probar lo siguiente. No parece funcionar .
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
view.addItemsB()
Puedes probar esto:
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() }...
Tenga en cuenta que si el valor es demasiado pequeño, este enfoque simplemente, y obviamente, no hace nada . UIKit seguirá trabajando. (¿Qué más haría?). Si el valor es demasiado grande, no tiene sentido.
Tenga en cuenta que actualmente (iOS10), si no me equivoco : si prueba este truco con el truco de un retardo de cero, en el mejor de los casos funciona de forma errática. (Como probablemente esperaría).
Disparar el bucle de ejecución ...
view.addItemsA()
view.setNeedsDisplay()
view.layoutIfNeeded()
RunLoop.current.run(mode: .defaultRunLoopMode, before: Date())
view.addItemsB()
view.setNeedsDisplay()
view.layoutIfNeeded()
Razonable. Pero nuestra reciente prueba de la vida real muestra que esto parece que NO funciona en muchos casos.
(es decir, el UIKit de Apple ahora es lo suficientemente sofisticado como para difuminar el trabajo de UIKit más allá de ese "truco").
Pensamiento: ¿hay tal vez una forma, en UIKit, de obtener una devolución de llamada cuando, básicamente, ha trazado todas las vistas que ha acumulado? ¿Hay otra solución?
Una solución parece ser ... coloque las subvistas en los controladores , de modo que obtenga una devolución de llamada "didAppear" y realice un seguimiento de ellas. Eso parece infantil , pero tal vez sea el único patrón? ¿Realmente funcionaría de todos modos? (Solo un problema: no veo ninguna garantía de que didAppear garantice que todas las subvistas hayan sido dibujadas).
En caso de que esto todavía no esté claro ...
Ejemplo de uso cotidiano:
• Decir que quizás hay siete de las secciones .
• Supongamos que cada uno normalmente toma 0.01 a 0.20 para que UIKit lo construya (dependiendo de la información que esté mostrando).
• Si simplemente "deja que todo salga de una vez" , a menudo estará bien o será aceptable (tiempo total, digamos 0.05 a 0.15) ... pero ...
• a menudo habrá una pausa tediosa para el usuario cuando aparezca la "nueva pantalla". ( .1 a .5 o peor ).
• Mientras que si haces lo que te pido, siempre aparecerá en la pantalla, un fragmento a la vez, con el mínimo tiempo posible para cada fragmento.
Es una especie de solución. Pero no es ingeniería.
En realidad, sí lo es. Al agregar la demora, está haciendo exactamente lo que dijo que quería hacer: está permitiendo que se complete el runloop y se realice el diseño, y vuelve a ingresar en el hilo principal tan pronto como se hace. Eso, de hecho, es uno de mis usos principales de delay
. (Es posible que incluso puedas usar una delay
de cero).
TLDR
Forzar cambios de IU pendientes en el servidor de renderizado con CATransaction.flush()
o dividir el trabajo en múltiples marcos utilizando CADisplayLink
(código de ejemplo a continuación).
Resumen
¿Hay tal vez una forma, en UIKit, de obtener una devolución de llamada cuando haya trazado todas las vistas que ha acumulado?
No
iOS actúa como un juego que cambia de representación (no importa cuántos gane) a lo sumo una vez por fotograma. La única forma de garantizar que se ejecute un fragmento de código después de que los cambios se hayan procesado en la pantalla es esperar al siguiente fotograma.
¿Hay otra solución?
Sí, iOS solo puede generar cambios una vez por fotograma, pero su aplicación no es lo que hace esa representación. El proceso del servidor de la ventana es.
Su aplicación hace su diseño y representación y luego confirma sus cambios en su layerTree al servidor de representación. Hará esto automáticamente al final del runloop, o puede forzar que las transacciones pendientes que se envíen al servidor de procesamiento CATransaction.flush()
a CATransaction.flush()
.
Sin embargo, el bloqueo del hilo principal es malo en general (no solo porque bloquea las actualizaciones de IU). Así que si puedes debes evitarlo.
Soluciones posibles
Esta es la parte que te interesa.
1: Haga todo lo posible en una cola de fondo como pueda y mejore el rendimiento.
En serio, el iPhone 7 es la tercera computadora más poderosa (no el teléfono) de mi casa, solo superada por mi PC de juegos y Macbook Pro. Es más rápido que cualquier otra computadora en mi casa. No debería tomar una pausa de 3 segundos para representar la interfaz de usuario de tus aplicaciones.
2: Vaciar las CATacacciones pendientes
EDITAR: Como lo señaló rob mayoff , puede forzar a CoreAnimation a enviar los cambios pendientes al servidor de procesamiento llamando a CATransaction.flush()
addItems1()
CATransaction.flush()
addItems2()
CATransaction.flush()
addItems3()
Esto realmente no representará los cambios allí mismo, pero envía las actualizaciones de la interfaz de usuario pendientes al servidor de Windows, lo que garantiza que se incluyan en la próxima actualización de pantalla.
Esto funcionará, pero viene con estas advertencias en la documentación de Apple para ello.
Sin embargo, debe tratar de evitar llamar a la descarga explícitamente. Al permitir que se ejecute flush durante el runloop ... ... y las transacciones y animaciones que funcionan de transacción a transacción continuarán funcionando.
Sin embargo, el archivo de encabezado CATransaction
incluye esta cita, lo que parece implicar que, aunque no les guste, este es un uso oficialmente compatible.
En algunas circunstancias (es decir, sin bucle de ejecución, o el bucle de ejecución está bloqueado) puede ser necesario usar transacciones explícitas para obtener actualizaciones oportunas del árbol de procesamiento.
Documentación de Apple : "Mejor documentación para + [CATransaction flush]" .
3: dispatch_after()
Solo retrasa el código hasta el próximo runloop. dispatch_async(main_queue)
no funcionará, pero puede usar dispatch_after()
sin demora.
addItems1()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
addItems2()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
addItems3()
}
}
Mencionas en tu respuesta que esto ya no funciona para ti. Sin embargo, funciona bien en la prueba Swift Playground y en la aplicación de ejemplo de iOS que he incluido con esta respuesta.
4: Utilice CADisplayLink
CADisplayLink recibe llamadas una vez por fotograma y le permite garantizar que solo se ejecute una operación por fotograma, lo que garantiza que la pantalla podrá actualizarse entre las operaciones.
DisplayQueue.sharedInstance.addItem {
addItems1()
}
DisplayQueue.sharedInstance.addItem {
addItems2()
}
DisplayQueue.sharedInstance.addItem {
addItems3()
}
Necesita esta clase de ayudante para trabajar (o similar).
// 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: Llama al runloop directamente.
No me gusta por la posibilidad de un bucle infinito. Pero, admito que es poco probable. Tampoco estoy seguro de si esto es oficialmente compatible o si un ingeniero de Apple leerá este código y se verá horrorizado.
// 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()
Esto debería funcionar, a menos que (al responder a los eventos de runloop) usted haga algo más para bloquear la finalización de la llamada de runloop, ya que las transacciones de CAT se envían al servidor de la ventana al final del runloop.
Código de ejemplo
Demostración Xcode Project y Xcode Playground (Xcode 8.2, Swift 3)
¿Qué opción debo usar?
Me gustan las soluciones DispatchQueue.main.asyncAfter(deadline: .now() + 0.0)
y CADisplayLink
la mejor. Sin embargo, DispatchQueue.main.asyncAfter
no garantiza que se ejecutará en el próximo tick de runloop, por lo que es posible que no quiera confiar en él.
CATransaction.flush()
forzará que los cambios en la interfaz de usuario se CATransaction.flush()
al servidor de procesamiento y este uso parece ajustarse a los comentarios de Apple para la clase, pero viene con algunas advertencias adjuntas.
En algunas circunstancias (es decir, sin bucle de ejecución, o el bucle de ejecución está bloqueado) puede ser necesario usar transacciones explícitas para obtener actualizaciones oportunas del árbol de procesamiento.
Explicación detallada
El resto de esta respuesta es información básica sobre lo que está pasando dentro de UIKit y explica por qué las respuestas originales intentan usar view.setNeedsDisplay()
y view.layoutIfNeeded()
no hicieron nada.
Descripción general de UIKit Layout & Rendering
CADisplayLink no tiene ninguna relación con UIKit y el runloop.
No exactamente. La interfaz de usuario de iOS es GPU renderizada como un juego en 3D. Y trata de hacer lo menos posible. Así que muchas cosas, como el diseño y la renderización, no ocurren cuando algo cambia, pero cuando es necesario. Es por eso que llamamos ''setNeedsLayout'' y no subvistas de diseño. Cada cuadro del diseño puede cambiar varias veces. Sin embargo, iOS solo intentará llamar a layoutSubviews
una vez por fotograma, en lugar de las 10 veces que se haya llamado a setNeedsLayout
.
Sin embargo, suceden muchas cosas en la CPU (layout, -drawRect:
etc ...) así que, ¿cómo encaja todo esto?
Tenga en cuenta que todo esto está simplificado y se salta muchas cosas, como que CALayer es realmente el objeto de vista real que se muestra en pantalla, no UIView, etc.
Cada vista UIV se puede considerar como un mapa de bits, una textura de imagen / GPU. Cuando la pantalla se procesa, la GPU compone la jerarquía de vistas en el marco resultante que vemos. Compone las vistas, representando las subvistas de texturas sobre las vistas anteriores en el renderizado final que vemos en la pantalla (de manera similar a un juego).
Esto es lo que ha permitido a iOS tener una interfaz tan fluida y fácilmente animada. Para animar una vista a través de la pantalla, no tiene que redirigir nada. En el siguiente cuadro, la vista de la textura se compone en un lugar en la pantalla ligeramente diferente al de antes. Ni ella, ni la vista que tenía por necesidad de tener sus contenidos reenviados.
En el pasado, una sugerencia de rendimiento común solía ser reducir el número de vistas en la jerarquía de vistas al renderizar las celdas de la vista de tabla completamente en drawRect:
Este consejo fue para acelerar el proceso de compostaje de la GPU en los primeros dispositivos iOS. Sin embargo, las GPU son tan rápidas en los dispositivos iOS modernos, ya que esto ya no está muy preocupado.
LayoutSubviews y DrawRect
-setNeedsLayout
invalida el diseño actual de las vistas y lo marca como que necesita diseño.
-layoutIfNeeded
retransmitirá la vista si no tiene un diseño válido
-setNeedsDisplay
marcará las vistas como que necesitan volver a dibujarse. Anteriormente dijimos que cada vista se representa en una textura / imagen de la vista que la GPU puede mover y manipular sin necesidad de volver a dibujarla. Esto lo activará para volver a dibujar. El dibujo se realiza llamando a -drawRect:
en la CPU y, por lo tanto, es más lento que poder confiar en la GPU, que puede hacer la mayoría de los marcos.
Y lo importante a notar es lo que estos métodos no hacen. Los métodos de diseño no hacen nada visual. Aunque si el modo de visualización de las vistas está configurado para redraw
a redraw
, cambiar el marco de las vistas podría invalidar el procesamiento de las vistas (desencadenador- -setNeedsDisplay
).
Puedes probar lo siguiente todo el día. No parece funcionar:
view.addItemsA() view.setNeedsDisplay() view.layoutIfNeeded() view.addItemsB() view.setNeedsDisplay() view.layoutIfNeeded() view.addItemsC() view.setNeedsDisplay() view.layoutIfNeeded()
De lo que hemos aprendido, la respuesta debería ser obvia por qué esto no funciona ahora.
view.layoutIfNeeded()
no hace más que recalcular los marcos de sus subvistas.
view.setNeedsDisplay()
simplemente marca la vista como la que necesita volver a dibujar la próxima vez que UIKit recorra las jerarquías de vista actualizando las texturas de la vista para enviarlas a la GPU. Sin embargo, no afecta a las subvistas que intentó agregar.
En su ejemplo, view.addItemsA()
agrega 100 vistas secundarias. Estas son capas / texturas separadas no relacionadas en la GPU hasta que la GPU las componga juntas en el siguiente framebuffer. La única excepción a esto es si CALayer tiene shouldRasterize
establecido en true. En cuyo caso, crea una textura separada para la vista y sus subvistas y renderiza (en el modo de pensar en la GPU) la vista y sus subvistas en una única textura, almacenando efectivamente la composición que tendría que hacer cada fotograma. Esto tiene la ventaja de rendimiento de no tener que componer todas sus subvistas en cada fotograma. Sin embargo, si la vista o sus subvistas cambian con frecuencia (como durante una animación) será una penalización en el rendimiento, ya que invalidará la textura almacenada en caché que requiere que se vuelva a dibujar (similar a llamar frecuentemente a -setNeedsDisplay
).
Ahora, cualquier ingeniero de juegos simplemente haría esto ...
view.addItemsA() RunLoop.current.run(mode: .defaultRunLoopMode, before: Date()) view.addItemsB() RunLoop.current.run(mode: .defaultRunLoopMode, before: Date()) view.addItemsC()
Ahora sí, eso parece funcionar.
Pero ¿por qué funciona?
Ahora -setNeedsLayout
y -setNeedsDisplay
no activan una salida de retransmisión ni un redibujado, sino que simplemente marcan la vista como necesaria. A medida que UIKit llega a través de la preparación para renderizar el siguiente fotograma, genera vistas con texturas o diseños no válidos para volver a dibujar o retransmitir. Una vez que todo está listo, envía a la GPU que componga y muestre el nuevo marco.
Así que el bucle de ejecución principal en UIKit probablemente se parece a esto.
-(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
}
Así que volvamos a su código original.
view.addItemsA() // Takes 1 second view.addItemsB() // Takes 1 second view.addItemsC() // Takes 1 second
Entonces, ¿por qué los 3 cambios aparecen a la vez después de 3 segundos en lugar de uno a la vez con 1 segundo de diferencia?
Bueno, si este bit de código se está ejecutando como resultado de presionar un botón, o similar, se está ejecutando el bloqueo principal del subproceso de manera sincrónica (el subproceso en el que UIKit requiere que se realicen los cambios de la interfaz de usuario) y bloquea el bucle de ejecución en la línea 1, incluso parte procesadora En efecto, estás haciendo que la primera línea del método runloop tarde 3 segundos en volver.
Sin embargo, hemos determinado que el diseño no se actualizará hasta la línea 3, las vistas individuales no se procesarán hasta la línea 4 y no aparecerán cambios en la pantalla hasta la última línea del método runloop, línea 5.
La razón por la que el bombeo del runloop funciona manualmente es porque básicamente está insertando una llamada al método runloop()
. Su método se está ejecutando como resultado de ser llamado desde dentro de la función 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
Esto funcionará, siempre y cuando no se use muy a menudo, ya que se trata de recurrir. Si se usa con frecuencia, cada llamada al -runloop
podría desencadenar otra que lleve a una recursión fuera de control.
EL FIN
Debajo de este punto es solo una aclaración.
Información extra sobre lo que está pasando aquí.
CADisplayLink y NSRunLoop
Si no me equivoco, KH parece que, fundamentalmente, usted cree que "el bucle de ejecución" (es decir, este: RunLoop.current) es CADisplayLink.
El runloop y CADisplayLink no son lo mismo. Pero CADisplayLink se adjunta a un runloop para que funcione.
Me equivoqué un poco antes (en el chat) cuando dije que NSRunLoop llama a CADisplayLink cada tic, No lo hace. A mi entender, NSRunLoop es básicamente un bucle (1) de tiempo que es una tarea mantener el hilo vivo, procesar eventos, etc. Para evitar errores, voy a intentar citar extensamente la documentación de Apple para los próximos bits.
Un bucle de ejecución es muy parecido a su nombre suena. Es un bucle que tu hilo ingresa y usa para ejecutar controladores de eventos en respuesta a eventos entrantes. Su código proporciona las declaraciones de control utilizadas para implementar la parte del bucle real del bucle de ejecución; en otras palabras, su código proporciona el bucle
while
ofor
que impulsa el bucle de ejecución. Dentro de su bucle, utiliza un objeto de bucle de ejecución para "ejecutar" el código de procesamiento de eventos que recibe eventos y llama a los controladores instalados.
Anatomía de un bucle de carrera: Guía de programación de subprocesos - developer.apple.com
CADisplayLink
usa NSRunLoop
y debe agregarse a uno pero es diferente. Para citar el archivo de cabecera de CADisplayLink:
"A menos que esté en pausa, disparará cada vsync hasta que se elimine".
Desde:func add(to runloop: RunLoop, forMode mode: RunLoopMode)
Y a partir de la documentación de propiedades preferredFramesPerSecond
.
El valor predeterminado es cero, lo que significa que el enlace de la pantalla se activará en la cadencia nativa del hardware de la pantalla.
...
Por ejemplo, si la frecuencia de actualización máxima de la pantalla es de 60 cuadros por segundo, también es la velocidad de cuadros más alta que establece el enlace de pantalla como la velocidad de cuadros real.
Por lo tanto, si desea hacer algo sincronizado con la pantalla, CADisplayLink
(con la configuración predeterminada) es lo que desea usar.
Presentando el Servidor Render
Si sucede que bloquea un hilo, eso no tiene nada que ver con cómo funciona UIKit.
No exactamente. La razón por la que solo debemos tocar UIView desde el subproceso principal es porque UIKit no es seguro para subprocesos y se ejecuta en el subproceso principal. Si bloquea el hilo principal, ha bloqueado el hilo en el que se ejecuta UIKit.
Si UIKit funciona "como usted dice" {... ", envíe un mensaje para detener los cuadros de video. ¡Haga todo nuestro trabajo! ¡Envíe otro mensaje para iniciar el video nuevamente!"}
Eso no es lo que estoy diciendo.
O si funciona "como digo" {... es decir, como la programación normal ", haga todo lo que pueda hasta que los fotogramas estén a punto de terminar - ¡oh no, se está acabando! - ¡espere hasta el siguiente fotograma! }
No es así como funciona UIKit y no veo cómo podría hacerlo sin cambiar fundamentalmente su arquitectura. ¿Cómo se pretende ver el final del cuadro?
Como se explica en la sección "Descripción general de UIKit Layout & Rendering" de mi respuesta, UIKit trata de no trabajar por adelantado. -setNeedsLayout
y -setNeedsDisplay
se pueden llamar tantas veces por fotograma como desee. Solo invalidan el diseño y la vista, si ya se ha invalidado ese marco, entonces la segunda llamada no hace nada. Esto significa que si 10 cambios todos invalidan el diseño de una vista, UIKit solo necesita pagar el costo de volver a calcular el diseño una vez (a menos que haya usado -layoutIfNeeded
entre -setNeedsLayout
calls).
Lo mismo ocurre con -setNeedsDisplay
. Aunque como se discutió anteriormente, ninguno de estos se relaciona con lo que aparece en pantalla. layoutIfNeeded
actualiza el marco de las vistas y displayIfNeeded
actualiza la textura de las vistas, pero eso no está relacionado con lo que aparece en la pantalla. Imagina que cada UIView tiene una variable UIImage que representa su almacén de respaldo (en realidad está en CALayer, o inferior, y no es un UIImage. Pero esta es una ilustración). Al volver a dibujar esa vista, simplemente se actualiza el UIImage. Pero el UIImage sigue siendo solo datos, no un gráfico en pantalla hasta que algo se dibuja en la pantalla.
Entonces, ¿cómo se dibuja un UIView en la pantalla?
Anteriormente escribí el pseudo código UIKit en el runloop principal. Hasta ahora, en mis respuestas, he estado ignorando una parte importante de UIKit, no todo se ejecuta dentro de su proceso. Una sorprendente cantidad de cosas de UIKit relacionadas con la visualización de cosas realmente sucede en el proceso del servidor de procesamiento, no en el proceso de sus aplicaciones. El servidor de renderizado / servidor de ventana fue SpringBoard (la interfaz de usuario de la pantalla de inicio) hasta iOS 6 (desde entonces, BackBoard y FrontBoard han absorbido una gran cantidad de funciones de SpringBoard más relacionadas con el SO, por lo que se centran más en ser la principal interfaz de usuario del sistema operativo. pantalla / pantalla de bloqueo / centro de notificación / centro de control / conmutador de aplicaciones / etc ...).
El pseudo código para el runloop de render principal de UIKit es probablemente más cercano a esto. Y nuevamente, recuerde que la arquitectura de UIKit está diseñada para hacer el menor trabajo posible, por lo que solo hará esto una vez por fotograma (a diferencia de las llamadas de red o cualquier otra cosa que también pueda gestionar el runloop principal).
-(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()
}
Esto tiene sentido, una única congelación de aplicaciones iOS no debería poder congelar todo el dispositivo. De hecho, podemos demostrarlo en un iPad con 2 aplicaciones que se ejecutan en paralelo. Cuando causamos que uno se congele, el otro no se ve afectado.
Estas son 2 plantillas de aplicaciones vacías que creé y pegué el mismo código en ambas. Ambos deben la hora actual en una etiqueta en el centro de la pantalla. Cuando presiono congelar, se llama sleep(1)
y se congela la aplicación. Todo se detiene. Pero iOS en su conjunto está bien. La otra aplicación, el centro de control, el centro de notificaciones, etc. no se ven afectados por ello.
Si UIKit funciona "como usted dice" {... ", envíe un mensaje para detener los cuadros de video. ¡Haga todo nuestro trabajo! ¡Envíe otro mensaje para iniciar el video nuevamente!"}
En la aplicación no hay un comando UIKit stop video frames
porque su aplicación no tiene ningún control sobre la pantalla. La pantalla se actualizará a 60FPS usando cualquier marco que el servidor de Windows le dé. El servidor de ventanas compondrá un nuevo marco para la pantalla a 60 FPS utilizando las últimas posiciones, texturas y árboles de capas conocidos que su aplicación le dio para trabajar.
Cuando congela el subproceso principal en su aplicación, la línea CoreAnimation.commitLayersAnimationsToWindowServer()
, que se ejecuta en último lugar (después de su costoso código para add lots of views
), se bloquea y no se ejecuta. Como resultado, incluso si hay cambios, el servidor de Windows no se ha enviado todavía y, por lo tanto, continúa utilizando el último estado que se envió para su aplicación.
Las animaciones son otra parte de UIKit que se queda sin proceso en el servidor de Windows. Si, antes de la sleep(1)
en esa aplicación de ejemplo, iniciamos primero una animación UIView, la veremos comenzar, luego la etiqueta se congelará y dejará de actualizarse (porque se ha ejecutado sleep()
). Sin embargo, a pesar de que el hilo principal de las aplicaciones está congelado, la animación continuará independientemente.
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");
}
}
Este es el resultado:
De hecho podemos ir uno mejor.
Si cambiamos el método freezePressed()
a esto.
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
}
}
Ahora, sin la llamada de sleep(2)
la animación se ejecutará durante 0.2 segundos, luego se cancelará y la vista se moverá a otra parte de la pantalla con un color diferente. Sin embargo, la llamada inactiva bloquea el hilo principal durante 2 segundos, lo que significa que ninguno de estos cambios se envía al servidor de la ventana hasta la mayoría de la animación.
Y solo para confirmar aquí está el resultado con la línea sleep()
comentada.
Esperemos que esto explique lo que está pasando.Estos cambios son como los UIView que agrega en su pregunta. Se ponen en cola para incluirse en la próxima actualización, pero debido a que está bloqueando el hilo principal al enviar tantos a la vez, se detiene el envío del mensaje, que se incluirá en el siguiente marco. El siguiente marco no se bloquea, iOS producirá un nuevo marco que muestra todas las actualizaciones que ha recibido de SpringBoard y otras aplicaciones de iOS. Pero debido a que su aplicación aún está bloqueando su hilo principal, iOS no ha recibido ninguna actualización de su aplicación y, por lo tanto, no mostrará ningún cambio (a menos que haya cambios, como animaciones, ya en cola en el servidor de Windows).
Así que para resumir
- UIKit intenta hacer lo menos posible, por lo que los lotes cambian el diseño y la renderización en una sola vez.
- UIKit se ejecuta en el subproceso principal, el bloqueo del subproceso principal impide que UIKit haga nada hasta que la operación haya finalizado.
- UIKit en proceso no puede tocar la pantalla, envía capas y se actualiza al servidor de la ventana en cada fotograma
- Si bloquea el hilo principal, los cambios nunca se envían al servidor de Windows y, por lo tanto, no se muestran.
El servidor de la ventana tiene el control final de lo que aparece en la pantalla. iOS solo envía actualizaciones al servidor de CATransaction
cuando se CATransaction
la CATransaction
actual. Para que esto suceda cuando sea necesario, iOS registra un CFRunLoopObserver
para la actividad .beforeWaiting
en el bucle de ejecución del hilo principal. Después de manejar un evento (presumiblemente al llamar a su código), el bucle de ejecución llama al observador antes de esperar a que llegue el próximo evento. El observador confirma la transacción actual, si la hay. Confirmar la transacción incluye ejecutar el pase de diseño, el pase de visualización (en el que se llaman los métodos drawRect
) y enviar el diseño y los contenidos actualizados al servidor de Windows.
Calling layoutIfNeeded
realiza el diseño, si es necesario, pero no invoca el pase de visualización ni envía nada al servidor de layoutIfNeeded
. Si desea que iOS envíe actualizaciones al servidor de Windows, debe confirmar la transacción actual.
Una forma de hacerlo es llamar a CATransaction.flush()
. Un caso razonable para usar CATransaction.flush()
es cuando desea poner un CALayer
nuevo en la pantalla y desea que tenga una animación de inmediato. El nuevo CALayer
no se enviará al servidor de CALayer
se CALayer
la transacción, y no podrá agregarle animaciones hasta que aparezca en la pantalla. Entonces, agrega la capa a su jerarquía de capas, llame a CATransaction.flush()
y luego agregue la animación a la capa.
Puede usar CATransaction.flush
para obtener el efecto que desea. No recomiendo esto , pero aquí está el código:
@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)
}
Y aquí está el resultado:
El problema con la solución anterior es que bloquea el subproceso principal llamando a Thread.sleep
. Si su hilo principal no responde a los eventos, no solo el usuario se frustra (porque su aplicación no responde a sus toques), sino que, finalmente, iOS decidirá que la aplicación se cuelga y la mata.
La mejor manera es simplemente programar la adición de cada vista cuando desee que aparezca. Usted dice "no es ingeniería", pero está equivocado, y sus razones dadas no tienen sentido. Generalmente, iOS actualiza la pantalla cada 16⅔ milisegundos (a menos que su aplicación demore más que eso para manejar eventos). Siempre que la demora que desea sea al menos tan larga, solo puede programar que se ejecute un bloqueo después de la demora para agregar la siguiente vista. Si desea un retraso de menos de 16⅔ milisegundos, en general no puede tenerlo.
Así que aquí está la mejor forma recomendada de agregar las subvistas:
@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)
}
Y aquí está el resultado (idéntico):
3 métodos que podrían funcionar a continuación. El primero podría hacer que funcionara si una subvista está agregando el controlador y si no está directamente en el controlador de la vista. El segundo es una vista personalizada :) Parece que se preguntará cuándo estará listo LaySubviews en la vista. Este proceso continuo es lo que está congelando la visualización debido a las 1000 subvistas más secuencialmente. Dependiendo de su situación, puede agregar un controlador de vista de vista infantil y publicar una notificación cuando finalice viewDidLayoutSubviews (), pero no sé si esto se ajusta a su caso de uso. Probé con 1000 subs en la vista viewcontroller que se agregó y funcionó. En ese caso, un retraso de 0 hará exactamente lo que usted desea. Aquí hay un ejemplo de trabajo.
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
} }
Luego, en el Controlador de vista principal que está agregando subvistas, puede hacer esto.
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
})
}
}
}
O hay una forma más loca y probablemente mejor. Cree su propia vista que, en cierto sentido, conserva su propio tiempo y devuelve la llamada cuando es bueno no dejar marcos. Esto está sin pulir pero funcionaría.
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)
}
}
}
O finalmente, puede hacer la devolución de llamada o notificación en viewDidAppear pero también parece que cualquier código ejecutado en la devolución de llamada tendría que ajustarse para ejecutarse de manera oportuna desde la devolución de llamada viewDidAppear.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.0, execute: {
//code})
Se usa NSNotification
para lograr el efecto necesario.
Primero: registre un observador en la vista principal y cree un controlador de observador.
Luego, inicialice todos estos objetos A, B, C ... en un hilo separado (fondo, por ejemplo), por ejemplo, self performSelectorInBackground
Luego, publique la notificación de las subvistas y la última, performSelectorOnMainThread
para agregar la subvista en el orden deseado con los retrasos necesarios.
Para responder las preguntas de los comentarios, supongamos que tiene un UIViewController que se mostró en la pantalla. Este objeto: no es un punto de discusión y usted puede decidir dónde colocar el código, controlando la apariencia de la vista. El código es para el objeto UIViewController (por lo tanto, es uno mismo). Vista: algún objeto UIView, considerado como una vista principal. ViewN - una de las subvistas. Se puede escalar más tarde.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"ViewNotification"
object:nil];
Esto registró un observador - necesario para comunicarse entre hilos. ViewN * V1 = [[ViewN alloc] init]; Aquí - las subvistas se pueden asignar, todavía no se muestran.
- (void) handleNotification: (id) note {
ViewN * Vx = (ViewN*) [(NSNotification *) note.userInfo objectForKey: @"ViewArrived"];
[self.View performSelectorOnMainThread: @selector(addSubView) withObject: Vx waitUntilDone: FALSE];
}
Este controlador permite recibir mensajes y colocar UIView
objetos en la vista principal. Parece extraño, pero el punto es que debe ejecutar el método addSubview en el hilo principal para que tenga efecto. performSelectorOnMainThread
permite comenzar a agregar subvistas en el hilo principal sin bloquear la ejecución de la aplicación.
Ahora, creamos un método que colocará subvistas en la pantalla.
-(void) sendToScreen: (id) obj {
NSDictionary * mess = [NSDictionary dictionaryWithObjectsAndKeys: obj, @"ViewArrived",nil];
[[NSNotificationCenter defaultCenter] postNotificationName: @"ViewNotification" object: nil userInfo: mess];
}
Este método publicará una notificación desde cualquier hilo, enviando un objeto como elemento NSDictionary llamado ViewArrived.
Y, por último, las vistas que deben agregarse con un retraso de 3 segundos:
-(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];
}
No es la única solución. También es posible controlar subvistas de la vista principal contando la NSArray
subviews
propiedad.
En cualquier caso, puede ejecutar el initViews
método siempre que lo necesite e incluso en subprocesos en segundo plano y permite controlar la apariencia de subvista, el performSelector
mecanismo permite evitar el bloqueo de subprocesos de ejecución.