
Existem várias dicas e truques que permitem que os desenvolvedores do iOS saibam como fazer otimizações de desempenho para que as animações nos aplicativos funcionem sem problemas. Depois de ler o artigo, você entenderá o que significa 16,67 milissegundos para desenvolvedor iOS e quais ferramentas são melhores para usar para rastrear o código.
O artigo é baseado na palestra proferida por
Luke Parham , atualmente engenheiro de iOS na Apple e autor de tutoriais para desenvolvimento de iOS em RayWenderlich.com, na International Mobile Developers Conference
MBLT DEV 2017 .
“Ei pessoal. Se você pode, digamos, reduzir 10 segundos do tempo de inicialização, multiplique por 5 milhões de usuários e isso significa 50 milhões de segundos todos os dias. Ao longo de um ano, provavelmente são dezenas de vidas. Portanto, se você inicializar dez segundos mais rápido, salvará uma dúzia de vidas. Realmente vale a pena, não acha?
Steve Jobs sobre o desempenho (tempo de inicialização do Apple II).Desempenho no iOS ou como sair principal
O segmento principal é responsável por aceitar a entrada do usuário e exibir os resultados na tela. Aceitar torneiras, panelas, todos os gestos e depois renderizar. A maioria dos celulares modernos é renderizada a 60 quadros por segundo. Isso significa que todo mundo quer fazer todo o trabalho em 16,67 milissegundos. Portanto, sair da discussão principal é algo realmente importante.
Se algo demorar mais que 16,67 milissegundos, você eliminará automaticamente os quadros e os usuários o verão quando houver animações em andamento. Alguns dispositivos têm ainda menos tempo para renderizar, por exemplo, o novo iPad tem 120 Hertz, portanto, existem apenas 8 milissegundos por quadro para fazer o trabalho.
Quadros caídos
Regra nº 1: use um CADisplayLink para rastrear quadros perdidos
O CADisplayLink é um timer especial que dispara no Vsync. O Vsync é quando o aplicativo é renderizado na tela e acontece a cada 16 milissegundos. Para fins de teste, no AppDelegate, você pode configurar o CADisplayLink adicionado ao loop principal de execução e, em seguida, apenas ter outra função na qual você faz um pouco de matemática. Em seguida, você acompanha há quanto tempo o aplicativo está em execução e há quanto tempo desde a última vez em que essa função foi acionada. E veja se demorou mais de 16 milissegundos.

Isso só é acionado quando realmente é renderizado. Se você estava fazendo um monte de trabalho e diminuiu a velocidade do encadeamento principal, isso será executado 100 milissegundos mais tarde, o que significa que você fez muito trabalho e perdeu quadros nesse período.
Por exemplo, este é o aplicativo Catstagram. Ele gagueja quando a imagem está sendo carregada. E então você pode ver que o quadro caiu em um determinado momento e teve um tempo decorrido de 200 milissegundos. Isso significa que este aplicativo está fazendo algo que está demorando muito.

Os usuários não gostam dessa experiência, especialmente se o aplicativo suportar dispositivos mais antigos, como iPhone 5, iPods antigos etc.
Profiler de tempo
O Time Profiler é provavelmente a ferramenta mais útil para rastrear as coisas. As outras ferramentas são úteis, mas, no final, no Fyusion, usamos o Time Profiler em 90% das vezes. Os suspeitos comuns do aplicativo são scrollview, texto e imagens.
As imagens são realmente grandes. Temos decodificação JPEG - “UIImageView” é igual a alguma UIImage. UIimages decodifica todos os JPEGs para o aplicativo. Eles fazem isso lentamente, para que você não possa acompanhar o desempenho diretamente. Isso não acontece exatamente quando você define a imagem, mas é possível vê-la nos traços do Criador de perfis de tempo.
A medição de texto é outra grande coisa. Ele aparece, por exemplo, se você tem muitos realmente complexos, como japonês ou chinês. Isso pode levar muito tempo para fazer a medição de linhas.
O layout da hierarquia também diminui a renderização do aplicativo. Isto é especialmente verdade com o Layout automático. É conveniente, mas também é agressivamente lento em comparação com o layout manual. Portanto, é um desses trade-offs. Se o aplicativo diminuir a velocidade, talvez seja hora de sair dele e tentar outra técnica de layout.
Rastreio de Exemplo

Na árvore de chamadas de exemplo, você pode ver quanto trabalho suas CPUs estão fazendo. Você pode mudar as visualizações, ver por threads, ver por CPUs. Normalmente, a coisa mais interessante é separar por threads e depois ver o que está em destaque.
Muitas vezes, quando você começa a olhar para isso, parece super esmagador. Você às vezes tem um sentimento: “O que é todo esse lixo? Não sei o que isso significa "FRunLoopDoSource0".
Mas é uma das coisas em que você pode entender e entender como as coisas funcionam e começa a fazer sentido. Assim, você pode seguir o rastreamento da pilha e examinar todas as coisas do sistema que não escreveu. Mas na parte inferior, você pode ver seu código real.
A árvore de chamadas
Por exemplo, temos um aplicativo realmente simples que tem a função principal e, em seguida, chama alguns métodos dentro do principal. A que horas o criador de perfil faz é tirar uma captura instantânea de qualquer que seja o rastreamento de pilha agora por padrão a cada milissegundo. Então, ele espera um milissegundo e tira uma foto instantânea, onde você chamou "main", que chamou "foo", que chamou "bar". Há o primeiro rastreamento de pilha sobre a captura de tela. Então isso é coletado. Temos as seguintes contagens: 1, 1, 1.

Cada uma dessas funções foi chamada uma vez. Depois de um milissegundo, capturamos outra pilha. E desta vez, é exatamente a mesma coisa, aumentamos todas as contagens por 2.

Então, no terceiro milissegundo, temos uma pilha de chamadas ligeiramente diferente. Principal está chamando "bar" diretamente. Principal e bar estão acima de um. Mas então temos uma divisão. Às vezes, as chamadas principais "foo", às vezes, as chamadas principais "bar" diretamente. Isso acontece uma vez. Um método foi chamado dentro de outro.
Mais adiante, um método foi chamado dentro de outro que chama o terceiro método. Vemos que "buz" foi chamado duas vezes. Mas é um método tão pequeno que acontece entre um milissegundo.
Usando o criador de perfil de tempo, é importante lembrar que ele não fornece os horários exatos. Não diz exatamente quanto tempo um método leva. Ele informa com que frequência ele aparece nos instantâneos, o que pode apenas aproximar o tempo de execução de cada método. Porque se algo é curto o suficiente, nunca aparecerá.

Se você alternar para o modo do console na árvore de chamadas, poderá ver todos os eventos de queda de quadros e compará-los. Temos um monte de quadros sendo descartados e temos um monte de trabalho acontecendo. Você pode ampliar o perfil de tempo e ver o que estava sendo executado apenas nesta seção.

Na verdade, no Mac, em geral, você pode clicar nos triângulos de divulgação e ele será aberto magicamente e mostrará o que há de mais importante. Vai cair para o que está fazendo mais trabalho. E 90% do tempo será CFRunLoopRun e, em seguida, os retornos de chamada.

Todo o aplicativo é baseado em um loop de execução. Você tem esse loop que dura para sempre e, a cada iteração do loop, os retornos de chamada são chamados. Quando você chega a esse ponto, pode se aprofundar em cada uma delas e, basicamente, analisar quais são seus três ou quatro principais gargalos.
Se analisarmos um deles, podemos ver essas coisas em que é realmente fácil olhar para ela e ser como: "Uau, eu não sei o que isso está fazendo". Como renderizações, provedor de imagem, IO.

Há uma opção em que você pode ocultar as bibliotecas do sistema. É realmente tentador esconder, mas, na realidade, esse é realmente o maior gargalo do aplicativo.
Existem pesos que mostram qual porcentagem do trabalho esta função ou método específico está realizando. E se detalharmos o exemplo, temos 34% e isso acontece por causa da Apple jpeg_decode_image_all. Após algumas pesquisas, fica claro que isso significa que a decodificação JPEG está acontecendo no thread principal e causa a maior parte do quadro cair.

Regra nº 2
Geralmente, é melhor decodificar JPEGs em segundo plano. A maioria das bibliotecas de terceiros (AsyncDisplayKit, SDWebImage, ...) faz isso imediatamente. Se você não quiser usar estruturas, poderá fazê-lo. O que você faz é passar uma imagem, neste caso, é uma extensão da UIImage e, em seguida, você configura um contexto e desenha a imagem manualmente em um contexto em um CGBitmap.

Quando você faz isso, pode chamar o método Image () decodificado a partir de um thread em segundo plano. Isso sempre retornará a imagem decodificada. Não há como verificar se, em particular, o UIImage já está decodificado, e você sempre precisa passá-los por aqui. Mas se você armazenar em cache as coisas corretamente, isso não fará nenhum trabalho extra.
Fazer isso é tecnicamente menos eficiente. O uso do UIimageView é super otimizado, super eficiente. Ele fará a decodificação de hardware, por isso é uma troca. Suas imagens serão decodificadas mais lentamente dessa maneira. Mas o bom é que você pode despachar para uma fila de segundo plano, decodificar sua imagem com o método que acabamos de ver e, em seguida, voltar para o thread principal e definir seu conteúdo.

Embora esse trabalho tenha demorado mais, talvez não tenha acontecido no encadeamento principal, portanto não estava bloqueando a interação do usuário, pois não impedia a rolagem. Então isso é uma vitória.
Avisos de memória
Qualquer sinal de que você recebe um aviso de memória que deseja eliminar tudo, exclua toda a memória não utilizada que puder. Mas se você tem coisas acontecendo em threads de segundo plano, a alocação desses grandes JPEGs decodificados ocupa muita memória nova em threads de segundo plano.
Isso aconteceu no aplicativo Fyuse. Se eu pular para um thread em segundo plano, decodificar todos os meus JPEGs, em alguns casos em telefones antigos, o sistema o matará instantaneamente. E isso é porque está enviando um aviso de memória dizendo: “Ei! Livre-se da sua memória ”, mas as filas de segundo plano não escutam. O que acontece se você estiver alocando todas essas imagens e depois ela falha sempre. A maneira de contornar isso é fazer ping do thread principal do thread de segundo plano.

Em geral, o segmento principal é uma fila. As coisas ficam na fila e acontecem no segmento principal. Ao ir para o segundo plano no Objective-C, você pode usar performSelectorOnMainThread: withObject: waitUntilDone:. Isso o colocará no final da linha das filas principais. Se a fila principal estiver ocupada processando avisos de memória, essa chamada de função irá para o final da linha e aguardará que todos os avisos de memória sejam processados antes de fazer toda essa alocação pesada de memória
No Swift, é mais simples. Você pode executar um bloco vazio principal de expedição de forma síncrona no principal.
Aqui está um exemplo em que limpamos as coisas e estamos decodificando imagens nas filas de segundo plano. E a rolagem visual é muito mais bonita. Ainda estamos tendo quedas de quadros, mas esse é um iPod 5g, por isso é uma das piores coisas que você pode testar e que ainda suporta como iOS 10 e 11.

Quando você tiver essas quedas de quadros, poderá continuar procurando. Ainda há trabalho acontecendo e causando essas quedas de quadros. Você pode fazer mais coisas para torná-lo mais rápido.
Resumindo, nem sempre é fácil, mas se você tem pequenas coisas que levam muito tempo, pode fazê-las em segundo plano.
Verifique se ele não está relacionado ao UIKit. Muitas classes do UIKit não são seguras para threads e você não pode alocar esse UIView em segundo plano.
Use Core Graphics se precisar fazer coisas de imagem em segundo plano. Não oculte as bibliotecas do sistema. E não se esqueça dos avisos de memória.
Esta é a primeira parte de um artigo baseado na apresentação de Luke Parham. Se você quiser saber mais sobre como a interface do usuário funciona no iOS, por que usar um caminho mais bezier e quando voltar ao gerenciamento manual de memória, leia a segunda parte de um artigo
aqui .
Vídeo
Assista à palestra completa aqui: