drawRect ou não drawRect (quando usar o drawRect / Core Graphics vs subviews / images e por quê?)

142

Para esclarecer o objetivo desta pergunta: Eu sei como criar visualizações complicadas com ambas as subvisões e usando o drawRect. Estou tentando entender completamente o quando e por que usar um sobre o outro.

Também entendo que não faz sentido otimizar isso muito antes do tempo e fazer algo da maneira mais difícil antes de fazer qualquer criação de perfil. Considere que estou confortável com os dois métodos e agora realmente quero um entendimento mais profundo.

Muita confusão vem de aprender como tornar o desempenho de rolagem de exibição de tabela muito suave e rápido. É claro que a fonte original desse método é do autor por trás do twitter para iPhone (anteriormente tweetie). Basicamente, diz que, para tornar a rolagem da mesa mais suave, o segredo é NÃO usar subviews, mas fazer todo o desenho em uma visualização personalizada. Essencialmente, parece que o uso de muitas subvisões diminui a renderização porque elas têm muita sobrecarga e são constantemente recompostas nas visualizações pai.

Para ser justo, isso foi escrito quando o 3GS era bastante novo, e os iDevices ficaram muito mais rápidos desde então. Ainda assim, esse método é sugerido regularmente nas interwebs e em outros lugares para tabelas de alto desempenho. Na verdade, é um método sugerido no Table Sample Code da Apple , foi sugerido em vários vídeos da WWDC ( desenho prático para desenvolvedores de iOS ) e em muitos livros de programação para iOS .

Existem ainda ferramentas incríveis para projetar gráficos e gerar o código de gráficos principais para eles.

Então, no começo, sou levado a acreditar que "existe uma razão pela qual o Core Graphics existe. É RÁPIDO!"

Mas assim que penso que recebo a idéia "Favorecer gráficos principais quando possível", começo a perceber que o drawRect geralmente é responsável pela falta de resposta em um aplicativo, é extremamente caro em termos de memória e realmente sobrecarrega a CPU. Basicamente, eu deveria " Evitar substituir o drawRect " ( Desempenho do aplicativo iOS da WWDC 2012 : gráficos e animações )

Então eu acho que, como tudo, é complicado. Talvez você possa ajudar a mim e a outras pessoas a entender os quando e por que usar o drawRect?

Vejo algumas situações óbvias para usar o Core Graphics:

  1. Você tem dados dinâmicos (exemplo do gráfico de ações da Apple)
  2. Você tem um elemento de interface do usuário flexível que não pode ser executado com uma imagem redimensionável simples
  3. Você está criando um gráfico dinâmico que, uma vez renderizado, é usado em vários locais

Vejo situações para evitar gráficos principais:

  1. As propriedades da sua visualização precisam ser animadas separadamente
  2. Você tem uma hierarquia de exibição relativamente pequena, portanto, qualquer esforço extra percebido usando o CG não vale o ganho
  3. Você deseja atualizar partes da vista sem redesenhar a coisa toda
  4. O layout de suas subvisões precisa ser atualizado quando o tamanho da visualização principal for alterado

Portanto, conceda seu conhecimento. Em que situações você alcança o drawRect / Core Graphics (que também pode ser realizado com subvisões)? Quais fatores levam você a essa decisão? Como / por que o desenho em uma exibição personalizada é recomendado para rolagem suave de células da tabela, mas a Apple recomenda o drawRect por razões de desempenho em geral? E as imagens de fundo simples (quando você as cria com CG vs usando uma imagem png redimensionável)?

Talvez não seja necessário um entendimento profundo desse assunto para criar aplicativos que valham a pena, mas não gosto de escolher entre técnicas sem poder explicar o porquê. Meu cérebro fica bravo comigo.

Pergunta Update

Obrigado pela informação a todos. Algumas perguntas esclarecedoras aqui:

  1. Se você está desenhando algo com gráficos principais, mas pode realizar o mesmo com UIImageViews e um png pré-renderizado, sempre deve seguir esse caminho?
  2. Uma pergunta semelhante: especialmente com ferramentas duras como esta , quando você deve considerar desenhar elementos de interface em gráficos principais? (Provavelmente quando a exibição do seu elemento é variável. Por exemplo, um botão com 20 variações de cores diferentes. Algum outro caso?)
  3. Dado o meu entendimento na minha resposta abaixo, os ganhos de desempenho de uma célula de tabela poderiam ser obtidos capturando efetivamente um bitmap de instantâneo da sua célula após a renderização do UIView complexo e exibindo-o enquanto rolava e oculta a visualização complexa? Obviamente, algumas peças teriam que ser trabalhadas. Apenas um pensamento interessante que tive.
Bob Spryn
fonte

Respostas:

72

Atenha-se ao UIKit e às subvisões sempre que puder. Você pode ser mais produtivo e tirar proveito de todos os mecanismos de OO que devem facilitar a manutenção das coisas. Use os Core Graphics quando não conseguir obter o desempenho necessário do UIKit, ou você sabe que tentar hackear os efeitos de desenho no UIKit seria mais complicado.

O fluxo de trabalho geral deve ser o de criar as visualizações de tabela com subvisões. Use instrumentos para medir a taxa de quadros no hardware mais antigo suportado pelo aplicativo. Se você não conseguir 60fps, vá para CoreGraphics. Quando você faz isso há um tempo, percebe quando o UIKit provavelmente é uma perda de tempo.

Então, por que o Core Graphics é rápido?

CoreGraphics não é realmente rápido. Se estiver sendo usado o tempo todo, você provavelmente está indo devagar. É uma API de desenho rica, que exige que seu trabalho seja feito na CPU, em oposição a muito trabalho do UIKit que é transferido para a GPU. Se você tivesse que animar uma bola se movendo pela tela, seria uma péssima idéia chamar setNeedsDisplay em uma exibição 60 vezes por segundo. Portanto, se você tem subcomponentes de sua visualização que precisam ser animados individualmente, cada componente deve ser uma camada separada.

O outro problema é que, quando você não faz um desenho personalizado com o drawRect, o UIKit pode otimizar as vistas de material, para que o drawRect não funcione ou pode usar atalhos com a composição. Quando você substitui o drawRect, o UIKit precisa seguir o caminho lento porque não faz ideia do que está fazendo.

Esses dois problemas podem ser superados por benefícios no caso de células de exibição de tabela. Depois que o drawRect é chamado quando uma exibição aparece pela primeira vez na tela, o conteúdo é armazenado em cache e a rolagem é uma tradução simples realizada pela GPU. Como você está lidando com uma única visualização, em vez de uma hierarquia complexa, as otimizações drawRect do UIKit se tornam menos importantes. Portanto, o gargalo se torna o quanto você pode otimizar seu desenho dos gráficos principais.

Sempre que puder, use o UIKit. Faça a implementação mais simples que funcionar. Perfil. Quando houver um incentivo, otimize.

Ben Sandofsky
fonte
53

A diferença é que o UIView e o CALayer lidam essencialmente com imagens fixas. Essas imagens são carregadas na placa gráfica (se você conhece o OpenGL, pense em uma imagem como uma textura e em um UIView / CALayer como um polígono que mostre essa textura). Quando uma imagem está na GPU, ela pode ser desenhada muito rapidamente, e até várias vezes, e (com uma pequena penalidade de desempenho), mesmo com níveis variados de transparência alfa sobre outras imagens.

CoreGraphics / Quartz é uma API para gerar imagens. É necessário um buffer de pixel (novamente, pense na textura OpenGL) e altera os pixels individuais dentro dele. Isso tudo acontece na RAM e na CPU, e somente depois que o Quartz é concluído, a imagem é "liberada" de volta para a GPU. Essa ida e volta de obter uma imagem da GPU, alterá-la e depois carregar a imagem inteira (ou pelo menos uma parte comparativamente grande) de volta na GPU é bastante lenta. Além disso, o desenho real que o Quartz faz, embora seja muito rápido para o que você está fazendo, é muito mais lento do que o que a GPU faz.

Isso é óbvio, considerando que a GPU se move principalmente em pixels inalterados em grandes partes. O Quartz acessa aleatoriamente os pixels e compartilha a CPU com a rede, o áudio etc. Além disso, se você tiver vários elementos desenhados usando o Quartz ao mesmo tempo, será necessário redesenhar todos eles quando mudar, depois faça o upload do parte inteira, enquanto se você alterar uma imagem e permitir que UIViews ou CALayers colem em suas outras imagens, você poderá fazer o upload de quantidades muito menores de dados para a GPU.

Quando você não implementa -drawRect:, a maioria das visualizações pode ser simplesmente otimizada. Eles não contêm pixels, portanto não podem desenhar nada. Outras visualizações, como UIImageView, apenas desenham uma UIImage (que, novamente, é essencialmente uma referência a uma textura, que provavelmente já foi carregada na GPU). Portanto, se você desenhar a mesma UIImage 5 vezes usando um UIImageView, ele será carregado apenas na GPU uma vez e depois atraído para a tela em 5 locais diferentes, economizando tempo e CPU.

Quando você implementa -drawRect :, isso cria uma nova imagem. Você então desenha isso na CPU usando o Quartz. Se você desenhar uma UIImage no seu drawRect, é provável que ela faça o download da imagem da GPU, copie-a na imagem para a qual você está desenhando e, assim que terminar, faça o upload dessa segunda cópia da imagem de volta para a placa gráfica. Então você está usando o dobro da memória da GPU no dispositivo.

Portanto, a maneira mais rápida de desenhar é manter o conteúdo estático separado da alteração do conteúdo (em subclasses UIViews / UIView / CALayers). Carregue conteúdo estático como UIImage e desenhe-o usando um UIImageView e coloque o conteúdo gerado dinamicamente em tempo de execução em um drawRect. Se você tiver um conteúdo que é desenhado repetidamente, mas por si só não muda (ou seja, 3 ícones mostrados no mesmo slot para indicar algum status), use o UIImageView também.

Uma ressalva: existe uma quantidade excessiva de UIViews. Áreas particularmente transparentes têm um custo maior na GPU para desenhar, porque precisam ser misturadas com outros pixels atrás delas quando exibidas. É por isso que você pode marcar uma UIView como "opaca", para indicar à GPU que ela pode simplesmente destruir tudo por trás dessa imagem.

Se você tiver conteúdo gerado dinamicamente em tempo de execução, mas permanecer o mesmo durante toda a vida útil do aplicativo (por exemplo, um rótulo contendo o nome do usuário), pode fazer sentido desenhar a coisa toda uma vez usando Quartz, com o texto, o borda do botão etc., como parte do plano de fundo. Mas geralmente é uma otimização desnecessária, a menos que o aplicativo Instruments indique de maneira diferente.

uliwitness
fonte
Obrigado pela resposta detalhada. Espero que mais pessoas rolem para baixo e votem nisso também.
Bob Spryn
Gostaria de poder votar isso duas vezes. Obrigado pela excelente redação!
Costa do Mar do Tibete
Correção ligeira: Na maioria dos casos, não há para baixo de carga da GPU, mas o carregamento ainda é basicamente a operação mais lenta você pode pedir uma GPU para fazer, porque tem que transferir grandes buffers de pixels de mais lento RAM sistema em mais rápido VRAM. Quando a imagem está lá, movê-la envolve apenas o envio de um novo conjunto de coordenadas (alguns bytes) para a GPU, para que você saiba onde desenhar.
uliwitness
@uliwitness eu estava passando por sua resposta e você mencionou marcar um objeto como "opaco oblitera tudo por trás dessa imagem". Você pode, por favor, lançar alguma luz sobre essa parte?
Rikh 31/08/19
@Rikh Marcar uma camada como opaca significa que a) na maioria dos casos, a GPU não se preocupará em desenhar outras camadas abaixo dessa camada, bem, pelo menos as partes delas que ficam embaixo da camada opaca. Mesmo se tiverem sido desenhados, ele não executará a mistura alfa, mas apenas copiará o conteúdo da camada superior na tela (geralmente pixels totalmente transparentes em uma camada opaca terminam em preto ou pelo menos uma versão opaca de sua cor)
uliwitness
26

Vou tentar manter um resumo do que estou extrapolando das respostas de outras pessoas aqui e fazer perguntas esclarecedoras em uma atualização para a pergunta original. Mas encorajo outras pessoas a manterem as respostas e votarem nos que forneceram boas informações.

Abordagem geral

É bastante claro que a abordagem geral, como Ben Sandofsky mencionou em sua resposta , deve ser "Sempre que puder, use o UIKit. Faça a implementação mais simples que funcionar. Perfil. Quando houver um incentivo, otimize".

O porquê

  1. Existem dois principais gargalos possíveis em um iDevice, a CPU e a GPU
  2. A CPU é responsável pelo desenho / renderização inicial de uma vista
  3. A GPU é responsável pela maioria das animações (Core Animation), efeitos de camada, composição etc.
  4. O UIView possui muitas otimizações, armazenamento em cache etc., integradas para lidar com hierarquias complexas de exibição
  5. Ao substituir o drawRect, você perde muitos dos benefícios que o UIView oferece e geralmente é mais lento do que permitir que o UIView lide com a renderização.

Desenhar o conteúdo das células em um UIView simples pode melhorar muito o seu FPS nas tabelas de rolagem.

Como eu disse acima, CPU e GPU são dois possíveis gargalos. Como eles geralmente lidam com coisas diferentes, você deve prestar atenção a qual gargalo você está enfrentando. No caso de rolagem de tabelas, não é que o Core Graphics esteja atraindo mais rapidamente, e é por isso que ele pode melhorar bastante o seu FPS.

De fato, os gráficos principais podem muito bem ser mais lentos que uma hierarquia UIView aninhada para a renderização inicial. No entanto, parece que o motivo típico para rolagem irregular é que você está afunilando a GPU, portanto, é necessário resolver isso.

Por que substituir o drawRect (usando gráficos principais) pode ajudar na rolagem da tabela:

Pelo que entendi, a GPU não é responsável pela renderização inicial das exibições, mas recebe texturas ou bitmaps, às vezes com algumas propriedades de camada, depois de renderizadas. Ele é responsável por compor os bitmaps, renderizar todos esses efeitos de camada e a maioria das animações (Core Animation).

No caso de células de exibição de tabela, a GPU pode ter gargalos com hierarquias de exibição complexas, porque, em vez de animar um bitmap, está animando a exibição pai e fazendo cálculos de layout de subvisões, efeitos de camada de renderização e composição de todas as subvisões. Portanto, em vez de animar um bitmap, ele é responsável pelo relacionamento de vários bitmaps e como eles interagem na mesma área de pixels.

Portanto, em resumo, o motivo de desenhar sua célula em uma visualização com gráficos principais pode acelerar a rolagem da tabela NÃO é porque está desenhando mais rápido, mas porque está reduzindo a carga na GPU, que é o gargalo que causa problemas nesse cenário específico .

Bob Spryn
fonte
1
Cuidado, porém. As GPUs recompõem efetivamente toda a árvore da camada para cada quadro. Portanto, mover uma camada é efetivamente zero. Se você criar uma camada grande, apenas a movimentação dessa camada será mais rápida do que a movimentação de várias. Mover algo dentro dessa camada de repente envolve a CPU e tem um custo. Como o conteúdo da célula geralmente não muda muito (apenas em resposta a ações do usuário relativamente raras), o uso de menos visualizações acelera as coisas. Mas fazer uma visão ampla de todas as células seria uma Bad Idea ™.
uliwitness
6

Sou desenvolvedor de jogos e fazia as mesmas perguntas quando meu amigo me disse que minha hierarquia de visualizações baseada no UIImageView iria desacelerar meu jogo e torná-lo terrível. Então, comecei a pesquisar tudo o que eu poderia encontrar sobre o uso de UIViews, CoreGraphics, OpenGL ou algo de terceiros como o Cocos2D. A resposta consistente que recebi de amigos, professores e engenheiros da Apple na WWDC foi que não haverá muita diferença no final, porque em algum nível eles estão fazendo a mesma coisa . As opções de nível superior, como UIViews, contam com as opções de nível inferior, como CoreGraphics e OpenGL, apenas envoltas em código para facilitar o uso.

Não use o CoreGraphics se quiser apenas reescrever o UIView. No entanto, você pode ganhar velocidade usando o CoreGraphics, desde que faça todo o desenho em uma vista, mas será que realmente vale a pena ? A resposta que encontrei geralmente não. Quando comecei o jogo, estava trabalhando com o iPhone 3G. À medida que meu jogo crescia em complexidade, comecei a ver algum atraso, mas com os dispositivos mais novos era completamente imperceptível. Agora tenho muita ação acontecendo, e o único atraso parece ser uma queda de 1-3 fps ao jogar no nível mais complexo de um iPhone 4.

Ainda assim, decidi usar o Instruments para encontrar as funções que estavam ocupando mais tempo. Eu descobri que os problemas não estavam relacionados ao meu uso de UIViews . Em vez disso, chamava repetidamente o CGRectMake para determinados cálculos de detecção de colisão e carregava arquivos de imagem e áudio separadamente para determinadas classes que usam as mesmas imagens, em vez de tê-los de uma classe de armazenamento central.

Portanto, no final, você poderá obter um pequeno ganho usando o CoreGraphics, mas geralmente não valerá a pena ou poderá não ter nenhum efeito. A única vez em que uso o CoreGraphics é ao desenhar formas geométricas em vez de texto e imagens.

WolfLink
fonte
8
Eu acho que você pode estar confundindo Core Animation com Core Graphics. O Core Graphics é realmente lento, desenho baseado em software e não é usado para desenhar UIViews regulares, a menos que você substitua o método drawRect. O Core Animation é acelerado por hardware, rápido e responsável pela maior parte do que você vê na tela.
Nick Lockwood