dibujarRect o no dibujarRect (¿cuándo se debe usar drawRect / Core Graphics vs subviews / images y por qué?

142

Para aclarar el propósito de esta pregunta: Sé CÓMO crear vistas complicadas con ambas subvistas y usando drawRect. Estoy tratando de entender completamente cuándo y por qué usar uno sobre el otro.

También entiendo que no tiene sentido optimizar tanto antes de tiempo, y hacer algo de la manera más difícil antes de hacer un perfil. Considere que me siento cómodo con ambos métodos, y ahora realmente quiero una comprensión más profunda.

Gran parte de mi confusión proviene de aprender a hacer que el rendimiento de desplazamiento de la vista de tabla sea realmente suave y rápido. Por supuesto, la fuente original de este método es del autor detrás de twitter para iPhone (anteriormente tweetie). Básicamente dice que para hacer que el desplazamiento de la tabla sea suave, el secreto es NO usar subvistas, sino hacer todo el dibujo en una vista personalizada. Básicamente, parece que el uso de muchas subvistas ralentiza el procesamiento porque tienen mucha sobrecarga y se vuelven a componer constantemente sobre sus vistas principales.

Para ser justos, esto se escribió cuando el 3GS era bastante nuevo, e iDevices se ha vuelto mucho más rápido desde entonces. Aún así, este método se sugiere regularmente en las páginas web y en otros lugares para tablas de alto rendimiento. De hecho, es un método sugerido en el Código de muestra de tabla de Apple , se ha sugerido en varios videos de WWDC ( Dibujo práctico para desarrolladores de iOS ) y en muchos libros de programación de iOS .

Incluso hay herramientas increíbles para diseñar gráficos y generar código de Core Graphics para ellos.

Así que al principio me hacen creer "hay una razón por la que existe Core Graphics. ¡Es RÁPIDO!"

Pero tan pronto como creo que tengo la idea de "Favorecer Core Graphics cuando sea posible", empiezo a ver que drawRect a menudo es responsable de la poca capacidad de respuesta en una aplicación, es extremadamente costosa en cuanto a memoria y realmente grava la CPU. Básicamente, que debería " Evitar anular drawRect " (WWDC 2012 iOS App Performance: Graphics and Animations )

Así que supongo que, como todo, es complicado. ¿Tal vez puedan ayudarme a mí mismo y a otros a comprender cuándo y por qué usar drawRect?

Veo un par de situaciones obvias para usar Core Graphics:

  1. Tienes datos dinámicos (ejemplo del gráfico de acciones de Apple)
  2. Tiene un elemento de interfaz de usuario flexible que no se puede ejecutar con una simple imagen de tamaño variable
  3. Está creando un gráfico dinámico, que una vez renderizado se usa en varios lugares

Veo situaciones para evitar Core Graphics:

  1. Las propiedades de su vista deben ser animadas por separado
  2. Tiene una jerarquía de visualización relativamente pequeña, por lo que no vale la pena ganar ningún esfuerzo adicional percibido con CG
  3. Desea actualizar partes de la vista sin volver a dibujar todo
  4. El diseño de sus subvistas necesita actualizarse cuando cambia el tamaño de la vista principal

Así que otorga tu conocimiento. ¿En qué situaciones recurres a drawRect / Core Graphics (que también podría lograrse con subvistas)? ¿Qué factores te llevan a esa decisión? ¿Cómo / por qué se recomienda dibujar en una vista personalizada para el desplazamiento suave de las celdas de la tabla? ¿Qué pasa con las imágenes de fondo simples (cuando las creas con CG vs usando una imagen PNG redimensionable)?

Es posible que no se necesite una comprensión profunda de este tema para crear aplicaciones que valgan la pena, pero no me encanta elegir entre técnicas sin poder explicar por qué. Mi cerebro se enoja conmigo.

Actualización de preguntas

Gracias a todos por la información. Algunas preguntas aclaratorias aquí:

  1. Si está dibujando algo con gráficos básicos, pero puede lograr lo mismo con UIImageViews y un png pre-renderizado, ¿siempre debe seguir esa ruta?
  2. Una pregunta similar: Especialmente con herramientas rudas como esta , ¿cuándo debería considerar dibujar elementos de interfaz en gráficos básicos? (Probablemente cuando la visualización de su elemento es variable. Por ejemplo, un botón con 20 variaciones de color diferentes. ¿Algún otro caso?)
  3. Dado mi entendimiento en mi respuesta a continuación, ¿podrían lograrse las mismas ganancias de rendimiento para una celda de tabla capturando efectivamente un mapa de bits de instantánea de su celda después de su renderizado UIView complejo y mostrándolo mientras se desplaza y oculta su vista compleja? Obviamente algunas piezas tendrían que ser resueltas. Solo un pensamiento interesante que tuve.
Bob Spryn
fuente

Respuestas:

72

Se adhieren a UIKit y subvistas siempre que puedas. Puede ser más productivo y aprovechar todos los mecanismos de OO que deberían hacer las cosas más fáciles de mantener. Utilice Core Graphics cuando no pueda obtener el rendimiento que necesita de UIKit, o si sabe que tratar de hackear efectos de dibujo en UIKit sería más complicado.

El flujo de trabajo general debe ser construir las vistas de tabla con subvistas. Use los instrumentos para medir la velocidad de fotogramas en el hardware más antiguo que admitirá su aplicación. Si no puede obtener 60 fps, desplácese a CoreGraphics. Cuando ha hecho esto por un tiempo, tiene una idea de cuándo UIKit es probablemente una pérdida de tiempo.

Entonces, ¿por qué es rápido Core Graphics?

CoreGraphics no es realmente rápido. Si se usa todo el tiempo, probablemente va despacio. Es una API de dibujo rica, que requiere que su trabajo se realice en la CPU, a diferencia de una gran cantidad de trabajo UIKit que se descarga a la GPU. Si tuviera que animar una bola que se mueve por la pantalla, sería una idea terrible llamar a setNeedsDisplay en una vista 60 veces por segundo. Entonces, si tiene subcomponentes de su vista que necesitan ser animados individualmente, cada componente debe ser una capa separada.

El otro problema es que cuando no hace un dibujo personalizado con drawRect, UIKit puede optimizar las vistas de stock, por lo que drawRect no funciona, o puede tomar atajos con la composición. Cuando anula drawRect, UIKit tiene que tomar el camino lento porque no tiene idea de lo que está haciendo.

Estos dos problemas pueden ser superados por los beneficios en el caso de las celdas de vista de tabla. Después de llamar a drawRect cuando aparece una vista por primera vez en la pantalla, el contenido se almacena en caché y el desplazamiento es una traducción simple realizada por la GPU. Debido a que se trata de una vista única, en lugar de una jerarquía compleja, las optimizaciones drawRect de UIKit se vuelven menos importantes. Entonces, el cuello de botella se convierte en cuánto puede optimizar su dibujo de Core Graphics.

Siempre que puedas, usa UIKit. Haz la implementación más simple que funcione. Perfil. Cuando hay un incentivo, optimice.

Ben Sandofsky
fuente
53

La diferencia es que UIView y CALayer esencialmente manejan imágenes fijas. Estas imágenes se cargan en la tarjeta gráfica (si conoce OpenGL, piense en una imagen como una textura y un UIView / CALayer como un polígono que muestra dicha textura). Una vez que una imagen está en la GPU, se puede dibujar muy rápidamente, e incluso varias veces, y (con una ligera penalización de rendimiento) incluso con niveles variables de transparencia alfa sobre otras imágenes.

CoreGraphics / Quartz es una API para generar imágenes. Toma un búfer de píxeles (de nuevo, piense en la textura OpenGL) y cambia los píxeles individuales dentro de él. Todo esto sucede en la RAM y en la CPU, y solo una vez que se hace Quartz, la imagen se vuelve "enjuagada" a la GPU. Este viaje de ida y vuelta para obtener una imagen de la GPU, cambiarla y luego cargar toda la imagen (o al menos una porción relativamente grande) de vuelta a la GPU es bastante lenta. Además, el dibujo real que hace Quartz, aunque es muy rápido para lo que está haciendo, es mucho más lento que lo que hace la GPU.

Eso es obvio, teniendo en cuenta que la GPU se mueve principalmente alrededor de píxeles sin cambios en grandes fragmentos. Quartz hace un acceso aleatorio de píxeles y comparte la CPU con redes, audio, etc. Además, si tiene varios elementos que dibuja utilizando Quartz al mismo tiempo, debe volver a dibujarlos todos cuando cambie, luego cargue el parte completa, mientras que si cambia una imagen y luego deja que UIViews o CALayers la peguen en sus otras imágenes, puede evitar cargar cantidades mucho más pequeñas de datos en la GPU.

Cuando no implementa -drawRect :, la mayoría de las vistas se pueden optimizar. No contienen píxeles, por lo que no pueden dibujar nada. Otras vistas, como UIImageView, solo dibujan un UIImage (que, nuevamente, es esencialmente una referencia a una textura, que probablemente ya se haya cargado en la GPU). Entonces, si dibuja el mismo UIImage 5 veces con un UIImageView, solo se carga en la GPU una vez y luego se dibuja en la pantalla en 5 ubicaciones diferentes, lo que nos ahorra tiempo y CPU.

Cuando implementa -drawRect :, esto hace que se cree una nueva imagen. Luego dibujas en eso en la CPU usando Quartz. Si dibuja un UIImage en su drawRect, es probable que descargue la imagen de la GPU, la copie en la imagen a la que está dibujando y, una vez que haya terminado, cargue esta segunda copia de la imagen nuevamente en la tarjeta gráfica. Entonces estás usando el doble de memoria de GPU en el dispositivo.

Por lo tanto, la forma más rápida de dibujar es mantener el contenido estático separado del contenido cambiante (en subclases UIViews / UIView / CALayers separadas). Cargue contenido estático como un UIImage y dibuje usando un UIImageView y coloque el contenido generado dinámicamente en tiempo de ejecución en un drawRect. Si tiene contenido que se dibuja repetidamente, pero por sí mismo no cambia (es decir, 3 iconos que se muestran en la misma ranura para indicar algún estado), use UIImageView también.

Una advertencia: existe la posibilidad de tener demasiadas UIViews. Las áreas especialmente transparentes tienen un mayor costo en la GPU para dibujar, ya que deben mezclarse con otros píxeles detrás de ellas cuando se muestran. Es por eso que puede marcar una UIView como "opaca", para indicarle a la GPU que puede borrar todo lo que está detrás de esa imagen.

Si tiene contenido que se genera dinámicamente en tiempo de ejecución pero permanece igual durante la vida útil de la aplicación (por ejemplo, una etiqueta que contiene el nombre de usuario), puede tener sentido dibujar todo el contenido una vez usando Quartz, con el texto, el borde del botón, etc., como parte del fondo. Pero eso suele ser una optimización que no es necesaria a menos que la aplicación Instrumentos le indique lo contrario.

uli testigo
fuente
Gracias por la respuesta detallada. Con suerte, más personas se desplazarán hacia abajo y votarán también.
Bob Spryn
Ojalá pudiera votar esto dos veces. ¡Gracias por la gran escritura!
Costa del mar del Tíbet
Corrección ligera: En la mayoría de los casos no hay abajo de carga de la GPU, pero la carga sigue siendo básicamente la operación más lenta puede pedir a un GPU que hacer, porque tiene que transferir grandes cantidades de memoria RAM más lenta de píxeles de sistema en más rápido VRAM. OTOH una vez que una imagen está allí, moverla solo implica enviar un nuevo conjunto de coordenadas (unos pocos bytes) a la GPU para que sepa dónde dibujar.
uliwitness
@uliwitness estaba revisando tu respuesta y mencionaste que marcar un objeto como "opaco borra todo lo que está detrás de esa imagen". ¿Puedes por favor arrojar algo de luz sobre esa parte?
Rikh
@Rikh Marcar una capa como opaca significa que a) en la mayoría de los casos, la GPU no se molestará en dibujar otras capas debajo de esa capa, bueno, al menos las partes de ellas que se encuentran debajo de la capa opaca. Incluso si se han dibujado, en realidad no realizará una mezcla alfa, sino que simplemente copiará el contenido de la capa superior en la pantalla (por lo general, los píxeles completamente transparentes en una capa opaca terminarán en negro o al menos una versión opaca de su color)
uliwitness
26

Voy a intentar mantener un resumen de lo que estoy extrapolando de las respuestas de otros aquí, y hacer preguntas aclaratorias en una actualización de la pregunta original. Pero animo a otros a que sigan respondiendo y voten por aquellos que han proporcionado buena información.

Enfoque general

Está bastante claro que el enfoque general, como Ben Sandofsky mencionó en su respuesta , debería ser "Siempre que pueda, use UIKit. Realice la implementación más simple que funcione. Perfil. Cuando haya un incentivo, optimice".

El porque

  1. Hay dos posibles cuellos de botella en un iDevice, la CPU y la GPU
  2. La CPU es responsable del dibujo / representación inicial de una vista
  3. GPU es responsable de la mayoría de la animación (Core Animation), efectos de capa, composición, etc.
  4. UIView tiene muchas optimizaciones, almacenamiento en caché, etc., integradas para manejar jerarquías de vista complejas
  5. Al anular drawRect, se pierde muchos de los beneficios que ofrece UIView, y generalmente es más lento que dejar que UIView maneje el renderizado.

Dibujar el contenido de las celdas en una UIView plana puede mejorar enormemente su FPS en las tablas de desplazamiento.

Como dije anteriormente, la CPU y la GPU son dos posibles cuellos de botella. Como generalmente manejan cosas diferentes, debe prestar atención a qué cuello de botella se está enfrentando. En el caso de las tablas de desplazamiento, no es que Core Graphics esté dibujando más rápido, y es por eso que puede mejorar enormemente su FPS.

De hecho, Core Graphics puede ser más lento que una jerarquía UIView anidada para el renderizado inicial. Sin embargo, parece que la razón típica para el desplazamiento entrecortado es que está obstruyendo la GPU, por lo que debe abordar eso.

Por qué anular drawRect (usando gráficos centrales) puede ayudar al desplazamiento de la tabla:

Por lo que entiendo, la GPU no es responsable de la representación inicial de las vistas, sino que recibe texturas o mapas de bits, a veces con algunas propiedades de capa, después de que se hayan renderizado. Luego es responsable de componer los mapas de bits, renderizar todos los efectos de capa y la mayoría de la animación (Core Animation).

En el caso de las celdas de vista de tabla, la GPU se puede embotellar con jerarquías de vista complejas, porque en lugar de animar un mapa de bits, está animando la vista principal y haciendo cálculos de diseño de subvista, renderizando efectos de capa y componiendo todas las subvistas. Entonces, en lugar de animar un mapa de bits, es responsable de la relación de un montón de mapas de bits y cómo interactúan, para la misma área de píxeles.

En resumen, la razón por la que dibujar su celda en una vista con gráficos principales puede acelerar el desplazamiento de su tabla NO es porque esté dibujando más rápido, sino porque está reduciendo la carga en la GPU, que es el cuello de botella que le da problemas en ese escenario particular .

Bob Spryn
fuente
1
Cuidado sin embargo. Las GPU vuelven a componer el árbol de capas completo para cada fotograma. Por lo tanto, mover una capa es efectivamente cero costo. Si crea una capa grande, solo mover esa capa es más rápido que mover varias. Mover algo dentro de esta capa de repente involucra a la CPU y tiene un costo. Dado que el contenido de las celdas generalmente no cambia mucho (solo en respuesta a acciones de usuario relativamente raras), usar menos vistas acelera las cosas. Pero hacer una vista grande con todas las celdas sería una mala idea ™.
uliwitness
6

Soy un desarrollador de juegos, y estaba haciendo las mismas preguntas cuando mi amigo me dijo que mi jerarquía de vistas basada en UIImageView iba a ralentizar mi juego y hacerlo terrible. Luego procedí a investigar todo lo que pude encontrar sobre si usar UIViews, CoreGraphics, OpenGL o algo de terceros como Cocos2D. La respuesta constante que recibí de amigos, maestros e ingenieros de Apple en WWDC fue que al final no habrá mucha diferencia porque en algún nivel todos están haciendo lo mismo . Las opciones de nivel superior como UIViews se basan en las opciones de nivel inferior como CoreGraphics y OpenGL, solo están envueltas en código para que sea más fácil de usar.

No use CoreGraphics si solo va a terminar reescribiendo la UIView. Sin embargo, puede ganar algo de velocidad al usar CoreGraphics, siempre que haga todo su dibujo en una vista, pero ¿realmente vale la pena ? La respuesta que he encontrado es generalmente no. Cuando comencé mi juego, estaba trabajando con el iPhone 3G. A medida que mi juego creció en complejidad, comencé a ver cierto retraso, pero con los dispositivos más nuevos era completamente imperceptible. Ahora tengo mucha acción y el único retraso parece ser una caída de 1-3 fps cuando se juega en el nivel más complejo en un iPhone 4.

Aún así, decidí usar los instrumentos para encontrar las funciones que ocupaban más tiempo. Descubrí que los problemas no estaban relacionados con mi uso de UIViews . En cambio, repetidamente estaba llamando a CGRectMake para ciertos cálculos de detección de colisión y cargando archivos de imagen y audio por separado para ciertas clases que usan las mismas imágenes, en lugar de hacer que extraigan de una clase de almacenamiento central.

Entonces, al final, es posible que pueda lograr una ligera ganancia al usar CoreGraphics, pero por lo general no valdrá la pena o puede que no tenga ningún efecto. La única vez que uso CoreGraphics es cuando dibujo formas geométricas en lugar de texto e imágenes.

WolfLink
fuente
8
Creo que puede estar confundiendo Core Animation con Core Graphics. Core Graphics es realmente lento, un dibujo basado en software y no se usa para dibujar UIViews regulares a menos que anule el método drawRect. Core Animation es acelerado por hardware, rápido y responsable de la mayoría de lo que ves en la pantalla.
Nick Lockwood