Truques de desempenho do iOS para tornar seu aplicativo mais eficiente
Publicados: 2022-03-10Embora o hardware iOS moderno seja poderoso o suficiente para lidar com muitas tarefas intensivas e complexas, o dispositivo ainda pode não responder se você não tomar cuidado com o desempenho do seu aplicativo. Neste artigo, analisaremos cinco truques de otimização que farão com que seu aplicativo pareça mais responsivo.
1. Desenfileirar Célula Reutilizável
Você provavelmente já usou tableView.dequeueReusableCell(withIdentifier:for:)
dentro de tableView(_:cellForRowAt:)
antes. Já se perguntou por que você tem que seguir essa API estranha, em vez de apenas passar uma matriz de células? Vamos passar pelo raciocínio disso.
Digamos que você tenha uma visualização de tabela com mil linhas. Sem usar células reutilizáveis, teríamos que criar uma nova célula para cada linha, assim:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // Create a new cell whenever cellForRowAt is called. let cell = UITableViewCell() cell.textLabel?.text = "Cell \(indexPath.row)" return cell }
Como você deve ter pensado, isso adicionará mil células à memória do dispositivo enquanto você rola até o final. Imagine o que aconteceria se cada célula contivesse um UIImageView
e muito texto: carregá-los todos de uma vez pode fazer com que o aplicativo fique sem memória! Além disso, cada célula exigiria que uma nova memória fosse alocada durante a rolagem. Se você rolar uma visualização de tabela rapidamente, muitos pequenos pedaços de memória serão alocados em tempo real, e esse processo tornará a interface do usuário instável!
Para resolver isso, a Apple nos forneceu o dequeueReusableCell(withIdentifier:for:)
. A reutilização de células funciona colocando a célula que não está mais visível na tela em uma fila e, quando uma nova célula estiver prestes a ficar visível na tela (digamos, a célula subsequente abaixo enquanto o usuário rola para baixo), a visualização da tabela recupere uma célula dessa fila e modifique-a no método cellForRowAt indexPath:
Ao usar uma fila para armazenar células, a visualização de tabela não precisa criar mil células. Em vez disso, ele precisa de células suficientes para cobrir a área da visualização da tabela.
Usando dequeueReusableCell
, podemos reduzir a memória usada pelo aplicativo e torná-lo menos propenso a ficar sem memória!
2. Usando uma tela de inicialização que se parece com a tela inicial
Conforme mencionado nas Diretrizes de interface humana (HIG) da Apple, as telas de inicialização podem ser usadas para melhorar a percepção da capacidade de resposta de um aplicativo:
“Ele destina-se exclusivamente a melhorar a percepção de seu aplicativo como rápido para lançar e imediatamente pronto para uso. Cada aplicativo deve fornecer uma tela de inicialização.”
É um erro comum usar uma tela inicial como tela inicial para mostrar a marca ou adicionar uma animação de carregamento. Projete a tela de inicialização para ser idêntica à primeira tela do seu aplicativo, conforme mencionado pela Apple:
“Projete uma tela de inicialização quase idêntica à primeira tela do seu aplicativo. Se você incluir elementos que pareçam diferentes quando o aplicativo terminar de ser iniciado, as pessoas poderão experimentar um flash desagradável entre a tela de inicialização e a primeira tela do aplicativo.
“A tela de lançamento não é uma oportunidade de branding. Não crie uma experiência de entrada que se pareça com uma tela inicial ou uma janela "Sobre". Não inclua logotipos ou outros elementos de marca, a menos que sejam uma parte estática da primeira tela do seu aplicativo.”
O uso de uma tela de inicialização para fins de carregamento ou branding pode diminuir o tempo do primeiro uso e fazer com que o usuário sinta que o aplicativo está lento.
Quando você inicia um novo projeto iOS, um LaunchScreen.storyboard
em branco será criado. Esta tela será mostrada ao usuário enquanto o aplicativo carrega os controladores de visualização e o layout.
Para tornar seu aplicativo mais rápido, você pode projetar a tela inicial para ser semelhante à primeira tela (controlador de visualização) que será mostrada ao usuário.
Por exemplo, a tela de inicialização do aplicativo Safari é semelhante à sua primeira visualização :
O storyboard da tela de inicialização é como qualquer outro arquivo de storyboard, exceto que você só pode usar as classes UIKit padrão, como UIViewController, UITabBarController e UINavigationController. Se você tentar usar qualquer outra subclasse personalizada (como UserViewController), o Xcode o notificará de que o uso de nomes de classe personalizados é proibido.
Outra coisa a ser observada é que UIActivityIndicatorView
não é animado quando colocado na tela de inicialização, porque o iOS gerará uma imagem estática do storyboard da tela de inicialização e a exibirá para o usuário. (Isso é mencionado brevemente na apresentação da WWDC 2014 “Platforms State of the Union”, por volta de 01:21:56
.)
O HIG da Apple também nos aconselha a não incluir texto em nossa tela de inicialização, porque a tela de inicialização é estática e você não pode localizar texto para atender a diferentes idiomas.
Leitura recomendada : Aplicativo móvel com recurso de reconhecimento facial: como torná-lo real
3. Restauração de estado para controladores de exibição
A preservação e a restauração do estado permitem que o usuário retorne exatamente ao mesmo estado da interface do usuário imediatamente antes de sair do aplicativo. Às vezes, devido à memória insuficiente, o sistema operacional pode precisar remover seu aplicativo da memória enquanto o aplicativo está em segundo plano, e o aplicativo pode perder o controle de seu último estado de interface do usuário se não for preservado, possivelmente fazendo com que os usuários percam o trabalho em andamento!
Na tela multitarefa, podemos ver uma lista de aplicativos que foram colocados em segundo plano. Podemos supor que esses aplicativos ainda estão sendo executados em segundo plano; na realidade, alguns desses aplicativos podem ser mortos e reiniciados pelo sistema devido às demandas de memória. Os instantâneos do aplicativo que vemos na visualização multitarefa são, na verdade, capturas de tela tiradas pelo sistema quando saímos do aplicativo (ou seja, para ir para a tela inicial ou multitarefa).
O iOS usa essas capturas de tela para dar a ilusão de que o aplicativo ainda está em execução ou ainda está exibindo essa visualização específica, enquanto o aplicativo já pode ter sido encerrado ou reiniciado em segundo plano enquanto ainda exibe a mesma captura de tela.
Você já experimentou, ao retomar um aplicativo da tela multitarefa, que o aplicativo mostra uma interface de usuário diferente do instantâneo mostrado na visualização multitarefa? Isso ocorre porque o aplicativo não implementou o mecanismo de restauração de estado e os dados exibidos foram perdidos quando o aplicativo foi encerrado em segundo plano. Isso pode levar a uma experiência ruim porque o usuário espera que seu aplicativo esteja no mesmo estado de quando o deixou.
Do artigo da Apple:
“Eles esperam que seu aplicativo esteja no mesmo estado de quando o deixaram. A preservação e restauração do estado garantem que seu aplicativo retorne ao estado anterior quando for lançado novamente.”
O UIKit faz muito trabalho para simplificar a preservação e a restauração do estado para nós: ele lida com o salvamento e o carregamento do estado de um aplicativo automaticamente nos momentos apropriados. Tudo o que precisamos fazer é adicionar alguma configuração para informar ao aplicativo para oferecer suporte à preservação e restauração do estado e informar ao aplicativo quais dados precisam ser preservados.
Para habilitar o salvamento e a restauração de estado, podemos implementar esses dois métodos em AppDelegate.swift
:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
Isso dirá ao aplicativo para salvar e restaurar o estado do aplicativo automaticamente.
Em seguida, informaremos ao aplicativo quais controladores de visualização precisam ser preservados. Fazemos isso especificando o “ID de restauração” no storyboard:
Você também pode marcar “Usar ID do storyboard” para usar o ID do storyboard como o ID de restauração.
Para definir o ID de restauração no código, podemos usar a propriedade restorationIdentifier
do controlador de exibição.
// ViewController.swift self.restorationIdentifier = "MainVC"
Durante a preservação de estado, qualquer controlador de exibição ou exibição que tenha recebido um identificador de restauração terá seu estado salvo em disco.
Os identificadores de restauração podem ser agrupados para formar um caminho de restauração. Os identificadores são agrupados usando a hierarquia de exibição, do controlador de exibição raiz ao controlador de exibição ativo atual. Suponha que um MyViewController esteja embutido em um controlador de navegação, que está embutido em outro controlador de barra de guias. Supondo que eles estejam usando seus próprios nomes de classe como identificadores de restauração, o caminho de restauração ficará assim:
TabBarController/NavigationController/MyViewController
Quando o usuário sai do aplicativo com o MyViewController sendo o controlador de exibição ativo, esse caminho será salvo pelo aplicativo; então o aplicativo irá lembrar a hierarquia de visualização anterior mostrada ( Tab Bar Controller → Navigation Controller → My View Controller ).
Depois de atribuir o identificador de restauração, precisaremos implementar os métodos encodeRestorableState(com codificador:) e decodeRestorableState(com codificador:) para cada um dos controladores de exibição preservados. Esses dois métodos nos permitem especificar quais dados precisam ser salvos ou carregados e como codificá-los ou decodificá-los.
Vamos ver o controlador de exibição:
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { // will be called during state preservation override func encodeRestorableState(with coder: NSCoder) { // encode the data you want to save during state preservation coder.encode(self.username, forKey: "username") super.encodeRestorableState(with: coder) } // will be called during state restoration override func decodeRestorableState(with coder: NSCoder) { // decode the data saved and load it during state restoration if let restoredUsername = coder.decodeObject(forKey: "username") as? String { self.username = restoredUsername } super.decodeRestorableState(with: coder) } }
Lembre-se de chamar a implementação da superclasse na parte inferior do seu próprio método. Isso garante que a classe pai tenha a chance de salvar e restaurar o estado.
Assim que os objetos terminarem de decodificar, applicationFinishedRestoringState()
será chamado para informar ao controlador de exibição que o estado foi restaurado. Podemos atualizar a interface do usuário para o controlador de exibição neste método.
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { ... override func applicationFinishedRestoringState() { // update the UI here self.usernameLabel.text = self.username } }
Aí está! Esses são os métodos essenciais para implementar a preservação e restauração do estado do seu aplicativo. Lembre-se de que o sistema operacional removerá o estado salvo quando o aplicativo estiver sendo fechado à força pelo usuário, para evitar ficar preso em um estado quebrado caso algo dê errado na preservação e restauração do estado.
Além disso, não armazene nenhum dado de modelo (ou seja, dados que deveriam ter sido salvos em UserDefaults ou Core Data) no estado, mesmo que pareça conveniente fazê-lo. Os dados de estado serão removidos quando a força do usuário sair do seu aplicativo e você certamente não deseja perder os dados do modelo dessa maneira.
Para testar se a preservação e restauração do estado estão funcionando bem, siga as etapas abaixo:
- Crie e inicie um aplicativo usando o Xcode.
- Navegue até a tela com estado de preservação e restauração que deseja testar.
- Retorne à tela inicial (deslizando para cima ou clicando duas vezes no botão home, ou pressionando Shift ⇧ + Cmd ⌘ + H no simulador) para enviar o aplicativo para segundo plano.
- Pare o aplicativo no Xcode pressionando o botão.
- Inicie o aplicativo novamente e verifique se o estado foi restaurado com sucesso.
Como esta seção cobre apenas os conceitos básicos de preservação e restauração do estado, recomendo os seguintes artigos da Apple Inc. para um conhecimento mais aprofundado da restauração do estado:
- Preservando e Restaurando o Estado
- Processo de preservação da interface do usuário
- Processo de restauração da interface do usuário
4. Reduza ao máximo o uso de visualizações não opacas
Uma visão opaca é uma visão que não tem transparência, o que significa que qualquer elemento de interface do usuário colocado atrás dela não é visível. Podemos definir uma visualização como opaca no Interface Builder:
Ou podemos fazer isso programaticamente com a propriedade isOpaque
de UIView:
view.isOpaque = true
Definir uma vista como opaca fará com que o sistema de desenho otimize algum desempenho de desenho durante a renderização da tela.
Se uma visualização tiver transparência (ou seja, alfa está abaixo de 1.0), então o iOS terá que fazer um trabalho extra para calcular o que deve ser exibido, misturando diferentes camadas de visualizações na hierarquia de visualizações. Por outro lado, se uma vista estiver definida como opaca, o sistema de desenho apenas colocará essa vista na frente e evitará o trabalho extra de mesclar as várias camadas de vista atrás dela.
Você pode verificar quais camadas estão sendo mescladas (não opacas) no iOS Simulator marcando Debug → Color Blended Layers .
Depois de marcar a opção Color Blended Layers , você pode ver que algumas vistas são vermelhas e outras são verdes. Vermelho indica que a visualização não é opaca e que sua exibição de saída é resultado de camadas mescladas atrás dela. Verde indica que a visualização está opaca e nenhuma mesclagem foi feita.
Os rótulos mostrados acima (“Visualizar amigos”, etc.) são destacados em vermelho porque quando um rótulo é arrastado para o storyboard, sua cor de fundo é definida como transparente por padrão. Quando o sistema de desenho estiver compondo a tela perto da área da etiqueta, ele solicitará a camada atrás da etiqueta e fará alguns cálculos.
Uma maneira de otimizar o desempenho do aplicativo é reduzir ao máximo o número de visualizações destacadas em vermelho.
Alterando label.backgroundColor = UIColor.clear
para label.backgroundColor = UIColor.white
, podemos reduzir a mesclagem de camadas entre o rótulo e a camada de visualização por trás dele.
Você deve ter notado que, mesmo que tenha definido um UIImageView como opaco e atribuído uma cor de fundo a ele, o simulador ainda mostrará vermelho na visualização da imagem. Isso provavelmente ocorre porque a imagem que você usou para a visualização da imagem possui um canal alfa.
Para remover o canal alfa de uma imagem, você pode usar o aplicativo Preview para duplicar a imagem ( Shift ⇧ + Cmd ⌘ + S ) e desmarque a caixa de seleção “Alpha” ao salvar.
5. Passar funções de processamento pesado para threads em segundo plano (GCD)
Como o UIKit funciona apenas no encadeamento principal, a execução de processamento pesado no encadeamento principal diminuirá a velocidade da interface do usuário. O thread principal é usado pelo UIKit não apenas para manipular e responder à entrada do usuário, mas também para desenhar a tela.
A chave para tornar um aplicativo responsivo é mover o maior número possível de tarefas de processamento pesado para threads em segundo plano. Evite fazer cálculos complexos, redes e operações de E/S pesadas (por exemplo, leitura e gravação em disco) no encadeamento principal.
Você pode ter usado uma vez um aplicativo que de repente parou de responder à sua entrada de toque e parece que o aplicativo travou. Isso provavelmente é causado pelo aplicativo executando tarefas de computação pesadas no thread principal.
O encadeamento principal geralmente alterna entre tarefas do UIKit (como manipular a entrada do usuário) e algumas tarefas leves em pequenos intervalos. Se uma tarefa pesada estiver sendo executada no encadeamento principal, o UIKit precisará esperar até que a tarefa pesada seja concluída antes de poder manipular a entrada de toque.
Por padrão, o código dentro dos métodos de ciclo de vida do controlador de exibição (como viewDidLoad) e funções IBOutlet são executados no thread principal. Para mover tarefas de processamento pesado para um thread em segundo plano, podemos usar as filas do Grand Central Dispatch fornecidas pela Apple.
Aqui está o modelo para alternar filas:
// Switch to background thread to perform heavy task. DispatchQueue.global(qos: .default).async { // Perform heavy task here. // Switch back to main thread to perform UI-related task. DispatchQueue.main.async { // Update UI. } }
O qos
significa “qualidade de serviço”. Diferentes valores de qualidade de serviço indicam diferentes prioridades para as tarefas especificadas. O sistema operacional alocará mais tempo de CPU e taxa de transferência de E/S de energia da CPU para tarefas alocadas em filas com valores de QoS mais altos, o que significa que uma tarefa terminará mais rapidamente em uma fila com valores de QoS mais altos. Um valor de QoS mais alto também consumirá mais energia devido ao uso de mais recursos.
Aqui está a lista de valores de QoS da prioridade mais alta para a mais baixa:
A Apple forneceu uma tabela prática com exemplos de quais valores de QoS usar para diferentes tarefas.
Uma coisa a ter em mente é que todo código UIKit deve sempre ser executado no thread principal. A modificação de objetos UIKit (como UILabel
e UIImageView
) no thread em segundo plano pode ter uma consequência não intencional, como a UI não ser realmente atualizada, ocorrer uma falha e assim por diante.
Do artigo da Apple:
“Atualizar a interface do usuário em um thread que não seja o thread principal é um erro comum que pode resultar em atualizações perdidas da interface do usuário, defeitos visuais, corrupções de dados e falhas.”
Eu recomendo assistir ao vídeo WWDC 2012 da Apple sobre a simultaneidade da interface do usuário para entender melhor como criar um aplicativo responsivo.
Notas
A desvantagem da otimização de desempenho é que você precisa escrever mais código ou definir configurações adicionais sobre a funcionalidade do aplicativo. Isso pode fazer com que seu aplicativo seja entregue mais tarde do que o esperado, e você terá mais código para manter no futuro, e mais código significa potencialmente mais bugs.
Antes de gastar tempo otimizando seu aplicativo, pergunte-se se o aplicativo já está bom ou se tem alguma parte que não responde que realmente precisa ser otimizada. Gastar muito tempo otimizando um aplicativo já suave para cortar 0,01 segundo pode não valer a pena, pois o tempo poderia ser melhor gasto desenvolvendo melhores recursos ou outras prioridades.
Recursos adicionais
- “A Suite of Delicious iOS Eye Candy”, Tim Oliver, Tokyo iOS Meetup 2018 (Vídeo)
- “Construindo interfaces de usuário simultâneas no iOS”, Andy Matuschak, WWDC 2012 (Vídeo)
- “Preservando a interface do usuário do seu aplicativo em todos os lançamentos”, Apple
- “Guia de programação de simultaneidade: filas de despacho”, Arquivo de documentação, Apple
- “Verificador de thread principal”, Apple