Astuces de performance iOS pour rendre votre application plus performante

Publié: 2022-03-10
Résumé rapide ↬ De bonnes performances sont essentielles pour offrir une bonne expérience utilisateur, et les utilisateurs d'iOS ont souvent des attentes élevées vis-à-vis de leurs applications. Une application lente et qui ne répond pas peut amener les utilisateurs à abandonner l'utilisation de votre application ou, pire, à laisser une mauvaise note.

Bien que le matériel iOS moderne soit suffisamment puissant pour gérer de nombreuses tâches intensives et complexes, l'appareil peut toujours ne pas répondre si vous ne faites pas attention aux performances de votre application. Dans cet article, nous examinerons cinq astuces d'optimisation qui rendront votre application plus réactive.

1. Cellule réutilisable Dequeue

Vous avez probablement déjà utilisé tableView.dequeueReusableCell(withIdentifier:for:) dans tableView(_:cellForRowAt:) auparavant. Vous êtes-vous déjà demandé pourquoi vous deviez suivre cette API maladroite, au lieu de simplement transmettre un tableau de cellules ? Passons en revue le raisonnement de ceci.

Supposons que vous disposiez d'une vue tableau avec mille lignes. Sans utiliser de cellules réutilisables, il faudrait créer une nouvelle cellule pour chaque ligne, comme ceci :

 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 }

Comme vous l'avez peut-être pensé, cela ajoutera mille cellules à la mémoire de l'appareil lorsque vous faites défiler vers le bas. Imaginez ce qui se passerait si chaque cellule contenait une UIImageView et beaucoup de texte : les charger toutes en même temps pourrait entraîner un manque de mémoire dans l'application ! En dehors de cela, chaque cellule nécessiterait l'allocation d'une nouvelle mémoire lors du défilement. Si vous faites défiler rapidement une vue de table, beaucoup de petits morceaux de mémoire seront alloués à la volée, et ce processus rendra l'interface utilisateur janky !

Pour résoudre ce problème, Apple nous a fourni la dequeueReusableCell(withIdentifier:for:) . La réutilisation des cellules fonctionne en plaçant la cellule qui n'est plus visible à l'écran dans une file d'attente, et lorsqu'une nouvelle cellule est sur le point d'être visible à l'écran (par exemple, la cellule suivante ci-dessous lorsque l'utilisateur fait défiler vers le bas), la vue du tableau sera récupérer une cellule de cette file d'attente et la modifier dans la cellForRowAt indexPath:

Mécanisme de file d'attente de réutilisation des cellules
Comment fonctionnent les files d'attente de réutilisation de cellules dans iOS ( Grand aperçu )

En utilisant une file d'attente pour stocker les cellules, la vue tableau n'a pas besoin de créer un millier de cellules. Au lieu de cela, il a juste besoin de suffisamment de cellules pour couvrir la zone de la vue du tableau.

En utilisant dequeueReusableCell , nous pouvons réduire la mémoire utilisée par l'application et la rendre moins sujette à manquer de mémoire !

Plus après saut! Continuez à lire ci-dessous ↓

2. Utiliser un écran de lancement qui ressemble à l'écran initial

Comme mentionné dans les Human Interface Guidelines (HIG) d'Apple, les écrans de lancement peuvent être utilisés pour améliorer la perception de la réactivité d'une application :

"Il est uniquement destiné à améliorer la perception de votre application comme étant rapide à lancer et immédiatement prête à l'emploi. Chaque application doit fournir un écran de lancement.

C'est une erreur courante d'utiliser un écran de lancement comme écran de démarrage pour afficher la marque ou pour ajouter une animation de chargement. Concevez l'écran de lancement pour qu'il soit identique au premier écran de votre application, comme mentionné par Apple :

« Concevez un écran de lancement presque identique au premier écran de votre application. Si vous incluez des éléments qui semblent différents à la fin du lancement de l'application, les utilisateurs peuvent ressentir un flash désagréable entre l'écran de lancement et le premier écran de l'application.

"L'écran de lancement n'est pas une opportunité de marque. Ne concevez pas une expérience d'entrée qui ressemble à un écran de démarrage ou à une fenêtre "À propos". N'incluez pas de logos ou d'autres éléments de marque, sauf s'ils font partie statique du premier écran de votre application. »

L'utilisation d'un écran de lancement à des fins de chargement ou de personnalisation peut ralentir le moment de la première utilisation et donner l'impression à l'utilisateur que l'application est lente.

Lorsque vous démarrez un nouveau projet iOS, un LaunchScreen.storyboard vierge sera créé. Cet écran sera affiché à l'utilisateur pendant que l'application charge les contrôleurs de vue et la mise en page.

Pour rendre votre application plus rapide, vous pouvez concevoir l'écran de lancement pour qu'il soit similaire au premier écran (contrôleur de vue) qui sera présenté à l'utilisateur.

Par exemple, l'écran de lancement de l'application Safari est similaire à sa première vue :

L'écran de lancement et la première vue se ressemblent
Une comparaison de l'écran de lancement et de la première vue de l'application Safari ( Grand aperçu )

Le storyboard de l'écran de lancement est comme n'importe quel autre fichier de storyboard, sauf que vous ne pouvez utiliser que les classes UIKit standard, telles que UIViewController, UITabBarController et UINavigationController. Si vous essayez d'utiliser d'autres sous-classes personnalisées (telles que UserViewController), Xcode vous avertira que l'utilisation de noms de classe personnalisés est interdite.

Xcode affiche une erreur lorsqu'une classe personnalisée est utilisée
Le storyboard de l'écran de lancement ne peut pas contenir de classe standard non UIKit. ( Grand aperçu )

Une autre chose à noter est que UIActivityIndicatorView ne s'anime pas lorsqu'il est placé sur l'écran de lancement, car iOS génère une image statique à partir du storyboard de l'écran de lancement et l'affiche à l'utilisateur. (Ceci est mentionné brièvement dans la présentation de la WWDC 2014 "Platforms State of the Union", vers 01:21:56 .)

Le HIG d'Apple nous conseille également de ne pas inclure de texte sur notre écran de lancement, car l'écran de lancement est statique et vous ne pouvez pas localiser le texte pour répondre à différentes langues.

Lecture recommandée : Application mobile avec fonction de reconnaissance faciale : comment la rendre réelle

3. Restauration d'état pour les contrôleurs de vue

La préservation et la restauration de l'état permettent à l'utilisateur de revenir exactement au même état de l'interface utilisateur juste avant de quitter l'application. Parfois, en raison d'une mémoire insuffisante, le système d'exploitation peut avoir besoin de supprimer votre application de la mémoire alors que l'application est en arrière-plan, et l'application peut perdre la trace de son dernier état d'interface utilisateur si elle n'est pas conservée, ce qui peut entraîner la perte de travail des utilisateurs. en cours!

Dans l'écran multitâche, nous pouvons voir une liste d'applications qui ont été mises en arrière-plan. Nous pouvons supposer que ces applications fonctionnent toujours en arrière-plan ; en réalité, certaines de ces applications peuvent être tuées et redémarrées par le système en raison des demandes de mémoire. Les instantanés d'application que nous voyons dans la vue multitâche sont en fait des captures d'écran prises par le système dès que nous avons quitté l'application (c'est-à-dire pour accéder à l'écran d'accueil ou multitâche).

iOS crée l'illusion d'applications exécutées en arrière-plan en prenant une capture d'écran de la vue la plus récente
Captures d'écran des applications prises par iOS lorsque l'utilisateur quitte l'application ( Grand aperçu )

iOS utilise ces captures d'écran pour donner l'illusion que l'application est toujours en cours d'exécution ou affiche toujours cette vue particulière, alors que l'application a peut-être déjà été arrêtée ou redémarrée en arrière-plan tout en affichant la même capture d'écran.

Avez-vous déjà constaté, lors de la reprise d'une application à partir de l'écran multitâche, que l'application affiche une interface utilisateur différente de l'instantané affiché dans la vue multitâche ? En effet, l'application n'a pas implémenté le mécanisme de restauration d'état et les données affichées ont été perdues lorsque l'application a été arrêtée en arrière-plan. Cela peut entraîner une mauvaise expérience car l'utilisateur s'attend à ce que votre application soit dans le même état qu'au moment où il l'a laissée.

Extrait de l'article d'Apple :

"Ils s'attendent à ce que votre application soit dans le même état que lorsqu'ils l'ont laissée. La préservation et la restauration de l'état garantissent que votre application revient à son état précédent lors de son nouveau lancement. »

UIKit fait beaucoup pour nous simplifier la préservation et la restauration de l'état : il gère automatiquement la sauvegarde et le chargement de l'état d'une application aux moments appropriés. Tout ce que nous avons à faire est d'ajouter une configuration pour indiquer à l'application de prendre en charge la préservation et la restauration de l'état et pour indiquer à l'application quelles données doivent être conservées.

Pour activer la sauvegarde et la restauration de l'état, nous pouvons implémenter ces deux méthodes dans AppDelegate.swift :

 func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
 func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }

Cela indiquera à l'application d'enregistrer et de restaurer automatiquement l'état de l'application.

Ensuite, nous indiquerons à l'application quels contrôleurs de vue doivent être conservés. Nous faisons cela en spécifiant le "Restoration ID" dans le storyboard :

Définition de l'ID de restauration dans le storyboard
Définition de l'ID de restauration dans le storyboard ( Grand aperçu )

Vous pouvez également cocher "Utiliser l'ID du storyboard" pour utiliser l'ID du storyboard comme ID de restauration.

Pour définir l'ID de restauration dans le code, nous pouvons utiliser la propriété restorationIdentifier du contrôleur de vue.

 // ViewController.swift self.restorationIdentifier = "MainVC"

Pendant la conservation de l'état, tout contrôleur de vue ou vue auquel un identifiant de restauration a été attribué verra son état enregistré sur le disque.

Les identifiants de restauration peuvent être regroupés pour former un chemin de restauration. Les identifiants sont regroupés à l'aide de la hiérarchie des vues, du contrôleur de vue racine au contrôleur de vue actif actuel. Supposons qu'un MyViewController est intégré dans un contrôleur de navigation, qui est intégré dans un autre contrôleur de barre d'onglets. En supposant qu'ils utilisent leurs propres noms de classe comme identifiants de restauration, le chemin de restauration ressemblera à ceci :

 TabBarController/NavigationController/MyViewController

Lorsque l'utilisateur quitte l'application avec MyViewController comme contrôleur de vue actif, ce chemin sera enregistré par l'application ; alors l'application se souviendra de la hiérarchie de vue précédente affichée (Contrôleur de la barre d'ongletsContrôleur de navigationMon contrôleur de vue ).

Après avoir attribué l'identifiant de restauration, nous devrons implémenter les méthodes encodeRestorableState(with coder:) et decodeRestorableState(with coder:) pour chacun des contrôleurs de vue préservés. Ces deux méthodes nous permettent de spécifier quelles données doivent être sauvegardées ou chargées et comment les encoder ou les décoder.

Voyons le contrôleur de vue :

 // 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) } }

N'oubliez pas d'appeler l'implémentation de la superclasse au bas de votre propre méthode. Cela garantit que la classe parent a une chance de sauvegarder et de restaurer l'état.

Une fois que les objets ont fini de décoder, applicationFinishedRestoringState() sera appelée pour indiquer au contrôleur de vue que l'état a été restauré. Nous pouvons mettre à jour l'interface utilisateur du contrôleur de vue dans cette méthode.

 // 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 } }

Voilà! Ce sont les méthodes essentielles pour mettre en œuvre la préservation et la restauration de l'état de votre application. Gardez à l'esprit que le système d'exploitation supprimera l'état enregistré lorsque l'application est fermée de force par l'utilisateur, afin d'éviter de rester bloqué dans un état cassé en cas de problème dans la préservation et la restauration de l'état.

De plus, ne stockez aucune donnée de modèle (c'est-à-dire des données qui auraient dû être enregistrées dans UserDefaults ou Core Data) dans l'état, même si cela peut sembler pratique. Les données d'état seront supprimées lorsque l'utilisateur quittera votre application, et vous ne voulez certainement pas perdre les données du modèle de cette façon.

Pour tester si la préservation et la restauration de l'état fonctionnent bien, suivez les étapes ci-dessous :

  1. Créez et lancez une application à l'aide de Xcode.
  2. Accédez à l'écran avec la préservation et la restauration de l'état que vous souhaitez tester.
  3. Revenez à l'écran d'accueil (en balayant vers le haut ou en double-cliquant sur le bouton d'accueil, ou en appuyant sur Maj ⇧ + Cmd ⌘ + H dans le simulateur) pour envoyer l'application en arrière-plan.
  4. Arrêtez l'application dans Xcode en appuyant sur le bouton.
  5. Relancez l'application et vérifiez si l'état a été restauré avec succès.

Étant donné que cette section ne couvre que les bases de la préservation et de la restauration de l'état, je recommande les articles suivants d'Apple Inc. pour une connaissance plus approfondie de la restauration de l'état :

  1. Préserver et restaurer l'état
  2. Processus de préservation de l'interface utilisateur
  3. Processus de restauration de l'interface utilisateur

4. Réduisez autant que possible l'utilisation de vues non opaques

Une vue opaque est une vue qui n'a aucune transparence, ce qui signifie que tout élément d'interface utilisateur placé derrière n'est pas visible du tout. Nous pouvons définir une vue pour qu'elle soit opaque dans l'Interface Builder :

Cela informera le système de dessin de ne pas dessiner ce qui se trouve derrière cette vue
Définir UIView sur opaque dans le storyboard ( Grand aperçu )

Ou nous pouvons le faire par programmation avec la propriété isOpaque de UIView :

 view.isOpaque = true

Définir une vue sur opaque permet au système de dessin d'optimiser certaines performances de dessin lors du rendu de l'écran.

Si une vue est transparente (c'est-à-dire que l'alpha est inférieur à 1.0), iOS devra faire un travail supplémentaire pour calculer ce qui doit être affiché en mélangeant différentes couches de vues dans la hiérarchie des vues. D'autre part, si une vue est définie sur opaque, le système de dessin placera simplement cette vue au premier plan et évitera le travail supplémentaire de fusion des multiples couches de vue derrière elle.

Vous pouvez vérifier quels calques sont mélangés (non opaques) dans le simulateur iOS en cochant DebugColor Blended Layers .

Le vert est un mélange sans couleur, le rouge est un calque mélangé
Afficher les calques de couleurs mélangées dans le simulateur

Après avoir coché l'option Color Blended Layers , vous pouvez voir que certaines vues sont rouges et d'autres vertes. Le rouge indique que la vue n'est pas opaque et que son affichage de sortie est le résultat de couches fusionnées derrière elle. Le vert indique que la vue est opaque et qu'aucun fondu n'a été effectué.

Avec un fond de couleur opaque, le calque n'a pas besoin de se fondre avec un autre calque
Attribuez une couleur d'arrière-plan non transparente à UILabel chaque fois que possible pour réduire les calques de couleurs mélangées. ( Grand aperçu )

Les étiquettes affichées ci-dessus ("Afficher les amis", etc.) sont surlignées en rouge car lorsqu'une étiquette est déplacée vers le storyboard, sa couleur d'arrière-plan est définie sur transparent par défaut. Lorsque le système de dessin compose l'affichage près de la zone d'étiquette, il demande le calque derrière l'étiquette et effectue des calculs.

Une façon d'optimiser les performances de l'application consiste à réduire autant que possible le nombre de vues surlignées en rouge.

En changeant label.backgroundColor = UIColor.clear en label.backgroundColor = UIColor.white , nous pouvons réduire la fusion des couches entre l'étiquette et la couche de vue derrière elle.

L'utilisation d'une couleur d'arrière-plan transparente entraînera une fusion des calques
De nombreuses étiquettes sont surlignées en rouge car leur couleur d'arrière-plan est transparente, obligeant iOS à calculer la couleur d'arrière-plan en mélangeant la vue derrière. ( Grand aperçu )

Vous avez peut-être remarqué que, même si vous avez défini un UIImageView sur opaque et lui avez attribué une couleur d'arrière-plan, le simulateur affichera toujours du rouge dans la vue de l'image. C'est probablement parce que l'image que vous avez utilisée pour la vue d'image a un canal alpha.

Pour supprimer le canal alpha d'une image, vous pouvez utiliser l'application Aperçu pour faire une copie de l'image ( Maj ⇧ + Cmd ⌘ + S ) et décochez la case "Alpha" lors de l'enregistrement.

Décochez la case "Alpha" lors de l'enregistrement d'une image pour supprimer le canal alpha.
Décochez la case "Alpha" lors de l'enregistrement d'une image pour supprimer le canal alpha. ( Grand aperçu )

5. Passer les fonctions de traitement lourdes aux threads d'arrière-plan (GCD)

Étant donné que UIKit ne fonctionne que sur le thread principal, effectuer un traitement lourd sur le thread principal ralentira l'interface utilisateur. Le thread principal est utilisé par UIKit non seulement pour gérer et répondre aux entrées de l'utilisateur, mais également pour dessiner l'écran.

La clé pour rendre une application réactive est de déplacer autant de tâches de traitement lourdes que possible vers des threads d'arrière-plan. Évitez les calculs complexes, la mise en réseau et les opérations d'E/S lourdes (par exemple, lire et écrire sur le disque) sur le thread principal.

Vous avez peut-être déjà utilisé une application qui est soudainement devenue insensible à votre saisie tactile, et vous avez l'impression que l'application s'est bloquée. Cela est probablement dû au fait que l'application exécute des tâches de calcul lourdes sur le thread principal.

Le thread principal alterne généralement entre les tâches UIKit (telles que la gestion des entrées utilisateur) et certaines tâches légères à intervalles rapprochés. Si une tâche lourde est en cours d'exécution sur le thread principal, alors UIKit devra attendre que la tâche lourde soit terminée avant de pouvoir gérer la saisie tactile.

Évitez d'exécuter des tâches gourmandes en performances ou chronophages sur le thread principal
Voici comment le thread principal gère les tâches de l'interface utilisateur et pourquoi il provoque le blocage de l'interface utilisateur lorsque des tâches lourdes sont effectuées. ( Grand aperçu )

Par défaut, le code contenu dans les méthodes de cycle de vie du contrôleur de vue (telles que viewDidLoad) et les fonctions IBOutlet sont exécutés sur le thread principal. Pour déplacer les tâches de traitement lourdes vers un thread d'arrière-plan, nous pouvons utiliser les files d'attente Grand Central Dispatch fournies par Apple.

Voici le modèle pour changer de file d'attente :

 // 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. } }

Le qos signifie « qualité de service ». Différentes valeurs de qualité de service indiquent différentes priorités pour les tâches spécifiées. Le système d'exploitation allouera plus de temps CPU et de débit d'E/S de puissance CPU pour les tâches allouées dans les files d'attente avec des valeurs QoS plus élevées, ce qui signifie qu'une tâche se terminera plus rapidement dans une file d'attente avec des valeurs QoS plus élevées. Une valeur QoS plus élevée consommera également plus d'énergie car elle utilise plus de ressources.

Voici la liste des valeurs de QoS de la plus haute à la plus basse priorité :

Valeurs de qualité de service de la file d'attente triées par performances et efficacité énergétique
Valeurs de qualité de service de la file d'attente triées par performances et efficacité énergétique ( Grand aperçu )

Apple a fourni un tableau pratique avec des exemples de valeurs QoS à utiliser pour différentes tâches.

Une chose à garder à l'esprit est que tout le code UIKit doit toujours être exécuté sur le thread principal. La modification d'objets UIKit (tels que UILabel et UIImageView ) sur le thread d'arrière-plan peut avoir une conséquence inattendue, comme l'interface utilisateur qui ne se met pas à jour, un plantage, etc.

Extrait de l'article d'Apple :

"La mise à jour de l'interface utilisateur sur un thread autre que le thread principal est une erreur courante qui peut entraîner des mises à jour manquées de l'interface utilisateur, des défauts visuels, des corruptions de données et des plantages."

Je recommande de regarder la vidéo WWDC 2012 d'Apple sur la simultanéité de l'interface utilisateur pour mieux comprendre comment créer une application réactive.

Remarques

Le compromis de l'optimisation des performances est que vous devez écrire plus de code ou configurer des paramètres supplémentaires en plus des fonctionnalités de l'application. Cela pourrait rendre votre application livrée plus tard que prévu, et vous aurez plus de code à maintenir à l'avenir, et plus de code signifie potentiellement plus de bogues.

Avant de consacrer du temps à l'optimisation de votre application, demandez-vous si l'application est déjà fluide ou si elle contient une partie qui ne répond pas et qui doit vraiment être optimisée. Passer beaucoup de temps à optimiser une application déjà fluide pour gagner 0,01 seconde n'en vaut peut-être pas la peine, car le temps pourrait être mieux utilisé pour développer de meilleures fonctionnalités ou d'autres priorités.

Autres ressources

  • "Une suite de délicieux bonbons pour les yeux iOS", Tim Oliver, Tokyo iOS Meetup 2018 (vidéo)
  • "Création d'interfaces utilisateur simultanées sur iOS", Andy Matuschak, WWDC 2012 (vidéo)
  • "Préserver l'interface utilisateur de votre application à travers les lancements", Apple
  • « Guide de programmation de la concurrence : files d'attente de distribution », archives de la documentation, Apple
  • "Vérificateur de fil principal", Apple