Performances dans iOS ou comment décharger le thread principal. Partie 1



Il existe différentes astuces et astuces qui aident à optimiser le travail des applications iOS, lorsqu'une tâche doit être terminée en 16,67 millisecondes. Nous vous expliquons comment décharger le thread principal et quels outils sont les mieux adaptés pour suivre la pile d'appels qu'il contient.


«Les gars, imaginons que vous pouvez réduire le temps de démarrage de 10 secondes. En multipliant cela par 5 millions d'utilisateurs, nous aurons 50 millions de secondes par jour. En un an, cela équivaudra à une dizaine de vies humaines. Par conséquent, si vous accélérez le téléchargement initial de 10 secondes, vous économiserez plusieurs dizaines de vies. Ça vaut vraiment le coup, n'est-ce pas?

Steve Jobs sur les performances (temps de démarrage de l'ordinateur Apple II).


L'article est basé sur un rapport du développeur Fyusion iOS Luc Parham, qui a pris la parole lors de la conférence MBLT DEV International Mobile Developers Conference l'année dernière.


MBLT DEV 2018 se tiendra à Moscou le 28 septembre. Les billets sont les moins chers en ce moment. Par tradition, tandis que le Comité du programme sélectionne les rapports, vous pouvez acheter des billets pour le lève-tôt pour le konf. Saisissez cette opportunité maintenant. À partir du 29 juin, les billets seront plus chers.


Perte de trame


Le thread principal exécute le code qui est responsable des événements de type tactile et qui travaille avec l'interface utilisateur. Il rend l'écran. La plupart des smartphones modernes affichent 60 images par seconde. Cela signifie que les tâches doivent être exécutées en 16,67 millisecondes (1000 millisecondes / 60 images). Par conséquent, l'accélération dans le thread principal est importante.


Si une opération prend plus de 16,67 millisecondes, la perte de trame se produit automatiquement et les utilisateurs de l'application le remarqueront lors de la lecture des animations. Sur certains appareils, le rendu est encore plus rapide, par exemple, sur l'iPad Pro 2017, le taux de rafraîchissement de l'écran est de 120 Hz, il n'y a donc que 8 millisecondes pour terminer les opérations dans une seule image.


Règle n ° 1


CADisplayLink est un temporisateur spécial qui démarre pendant la synchronisation verticale (Vsync). La synchronisation verticale garantit que pas plus de 16,67 millisecondes sont alloués pour le rendu d'une image. En guise de vérification dans AppDelegate, vous pouvez enregistrer CADisplayLink dans la boucle d'exécution principale, puis vous aurez une fonction supplémentaire qui effectuera les calculs. Vous pouvez suivre la durée de l'application et savoir combien de temps s'est écoulé depuis le dernier lancement de cette fonction.


.


Le démarrage se produit lorsqu'un besoin de rendu apparaît. Si de nombreuses opérations différentes ont été effectuées qui ont surchargé le thread principal, cette fonction démarre avec un délai de 100 millisecondes. Cela signifie que trop de travail a été fait et qu'à ce moment il y a eu une perte de personnel.


Voici l'application Catstagram. Lors du téléchargement d'images, l'application commence à ralentir. Nous voyons que la fréquence d'images a diminué à un certain point, et le temps de chargement a duré environ 200 millisecondes. Quelque chose semble prendre trop de temps.


.


Les utilisateurs ne seront pas ravis de cela, surtout si l'application s'exécute sur des appareils plus anciens, tels que l'iPhone 5 ou des modèles d'iPod plus anciens, etc.


Profileur de temps


Time Profiler est un outil utile pour suivre ces problèmes. D'autres outils sont également utiles, mais finalement chez Fyusion 90% du temps, nous utilisons Time Profiler. En règle générale, les problèmes dans une application sont liés à ScrollView, les zones contenant du texte et des images.


Les images sont importantes. Nous décodons le format JPEG en utilisant UIImage . Ils le font lentement et nous ne pouvons pas suivre directement leurs performances. Cela ne se produit pas immédiatement après avoir défini l'image dans UIImageView , mais vous pouvez voir ce moment grâce au suivi dans Time Profiler.


La mise en forme du texte est un autre point important. Cela est important lorsque l'application contient une grande quantité de texte "complexe", par exemple en japonais ou en chinois. Le calcul des tailles correctes pour les lignes contenant du texte peut prendre un certain temps.


Le balisage d'interface ralentit également le rendu dans l'application. Cela est particulièrement vrai pour l'outil Mise en page automatique. La mise en page automatique est pratique à utiliser, mais elle ralentit considérablement l'application par rapport au balisage manuel. Nous devons faire des concessions. Si AutoLayout ralentit l'application, il est peut-être temps de l'abandonner et d'essayer d'autres types de balisage.


Modèle de trace




Dans cet exemple d'arborescence d'appels, vous pouvez voir quel type de travail fait le CPU. Vous pouvez changer le type de trace, le regarder du point de vue des threads, des processeurs. Habituellement, la chose la plus intéressante est de diviser la trace en threads et de surveiller le thread principal.


L'analyse initiale des traces peut sembler compliquée. Il n'est pas toujours possible de comprendre immédiatement ce FRunLoopDoSource0 signifie FRunLoopDoSource0 .


En fouillant dans la trace, vous pouvez comprendre comment fonctionne le système, puis tout est logique. Vous pouvez suivre la trace de la pile et regarder tous les éléments du système que vous n'avez pas écrits. Mais tout en bas se trouve votre code source.


Arbre d'appel


Supposons que nous ayons une application très simple. Il contient la fonction principale qui appelle plusieurs autres fonctions. L'essence du travail de Time Profiler est qu'il prend des instantanés de l'état actuel de la trace de pile avec une fréquence d'une milliseconde (par défaut). Après une autre milliseconde, il prend un instantané de la trace. Il appelle la fonction principale, qui appelle la fonction " foo ", qui a appelé la fonction " bar ". La trace de pile initiale est illustrée dans la capture d'écran ci-dessous. Ces données sont collectées ensemble. En face de chaque fonction, un nombre est indiqué: 1, 1, 1.




Cela signifie que chacune de ces fonctions a été appelée une fois. Puis, après une milliseconde, nous obtenons un autre tir de la pile. Cette fois, cela ressemble exactement à la même chose, donc tous les nombres augmentent de 1, et nous obtenons 2, 2, 2.




Pendant la troisième milliseconde, notre pile d'appels est un peu différente. La fonction principale appelle directement la bar . Par conséquent, une unité supplémentaire est ajoutée à la fonction principale et à la fonction « bar », et leur valeur devient 3. Ensuite, la séparation se produit. Parfois, la fonction principale appelle directement « foo », parfois « bar » est appelée directement. C'est arrivé une fois. Une fonction a été appelée par une autre.


Ensuite, une fonction a appelé une autre, qui a appelé la troisième fonction. On voit que la fonction « baz » a été appelée deux fois. Mais cette fonction est si insignifiante qu'elle est appelée plus rapide qu'une milliseconde.


Lors de l'utilisation de Time Profiler, il est important de se rappeler qu'il n'affiche pas d'intervalles de temps spécifiques. Il n'affiche pas le temps d'exécution exact d'une fonction. Il rapporte seulement la fréquence à laquelle il apparaît dans les images, ce qui ne donne qu'une valeur approximative de la durée de chaque fonction. Étant donné que certains processus sont assez rapides, ils ne sont jamais affichés sur les images.




Lorsque vous passez des appels en mode console, vous pouvez voir et comparer tous les moments de diminution de la fréquence d'images. Dans l'exemple, la perte de trame s'est produite plusieurs fois et divers processus ont été effectués.




Cliquer sur alt-click sur macOS étendra la section et les sous-sections, pas seulement celle sélectionnée. Ils seront triés selon la quantité de travail effectuée. Dans 90% des cas, CFRunLoopRun vient en CFRunLoopRun , suivi des rappels.


Cette application est entièrement basée sur un seul cycle d'exécution de tâche Run Loop. Il y a un cycle répétitif sans fin et à chaque itération, des rappels sont lancés. Si vous regardez ces rappels, vous pouvez mettre en évidence les principaux goulots d'étranglement.


Après avoir examiné ces défis plus en détail, vous ne comprendrez probablement pas ce qu'ils font. Il peut s'agir de rendus, de fournisseur d'images, d'E / S.




Il existe une option qui vous permet de masquer les bibliothèques système. Ce sont en fait les zones problématiques de l'application.


Il existe des compteurs qui, en pourcentage, indiquent le travail effectué par une fonction ou une opération particulière. Si nous regardons cet exemple, nous verrons ici la valeur - 34%. Il s'agit du processus Apple jpeg_decode_image_all . Après étude, il devient clair que le décodage des images JPEG se produit dans le thread principal, et dans la plupart des cas, c'est la cause de la perte de trame.




Règle n ° 2


En général, le décodage des images JPEG doit être effectué en arrière-plan. La plupart des bibliothèques tierces (AsyncDisplayKit, SDWebImage, etc.) peuvent le faire par défaut. Si vous ne souhaitez pas utiliser de frameworks, vous pouvez effectuer le décodage manuellement. Pour ce faire, vous pouvez écrire une extension sur UIImage dans laquelle vous créez un contexte et dessinez manuellement une image.




Lorsque vous effectuez cette opération, vous pouvez appeler la fonction decodeImage pas à partir du thread principal. Il renverra toujours une image décodée. Il n'y a aucun moyen de vérifier si une image UIImage particulière a passé le décodage, vous devez donc toujours les passer par cette méthode. Mais si vous mettez correctement en cache les données, il n'y aura pas de processus inutiles dans le système.


D'un point de vue technique, c'est moins efficace. L'utilisation de la classe UIImageView semble optimisée et efficace. Mais il effectue également le décodage matériel, il a donc aussi ses inconvénients. Avec cette méthode, vos images seront décodées plus lentement. Mais il y a une bonne nouvelle - vous pouvez décoder l'image de la manière ci-dessus et non sur le thread principal, puis revenir au thread principal et configurer l'interface.




Malgré le fait que cette opération nécessite plus de temps, elle peut ne pas être effectuée sur le thread principal, ce qui signifie qu'elle n'interfère pas avec l'activité de l'utilisateur dans l'application, car elle ne ralentit pas le défilement de la bande. Solution rentable.


Alertes de mémoire insuffisante


Avec tout signal de mémoire faible, je souhaite supprimer toutes les données inutilisées qui sont possibles. Mais si divers processus sont effectués sur des flux tiers, le placement d'images JPEG décodées volumétriques sur eux occupera la majeure partie de l'espace libre.


Un tel problème s'est produit dans l'application Fyuse. Si j'avais décodé toutes mes images JPEG sur un flux tiers, dans certains cas, par exemple, sur des modèles de téléphones plus anciens, ce système interromprait instantanément l'application. Cela serait dû au fait que les flux de tâches tiers ne répondent pas à un avertissement concernant une mémoire insuffisante du système, tel que «Hé, supprimez les données inutiles!». La situation suivante se produit: vous placez d'abord toutes ces images sur des flux tiers, puis l'application se bloque constamment. Si des threads tiers envoient des signaux au thread principal sur ce qui se passe dans le système, un tel problème ne se produira pas.


Travailler sans échecs




Essentiellement, le thread principal est une file d'attente composée de processus. Lorsque vous travaillez avec des threads tiers, vous pouvez écrire la commande performSelectorOnMainThread:withObject:waitUntilDone: dans Objective-C. Grâce à elle, les tâches seront mises à la fin de la file d'attente sur le thread principal. Par conséquent, si le thread principal est occupé à traiter des notifications de mémoire insuffisante, l'appel de cette commande vous permettra d'attendre que toutes les notifications aient été traitées, et alors seulement de commencer le processus complexe de chargement et de placement des données. Dans Swift, cela semble un peu plus simple. DispatchQueue.main.sync libère de l'espace sur le thread principal.


Voici un autre exemple. Nous avons libéré de la mémoire et décodé des images sur des flux tiers. Le défilement visuel de la bande est devenu beaucoup mieux. Nous perdons toujours des images en raison du fait que nous testons l'iPod 5g. C'est l'un des pires modèles de test de ceux qui prennent encore en charge les versions 10 et 11 d'iOS.




Si vous rencontrez ce type de perte de trame, vous pouvez toujours visualiser la bande. Cependant, il reste des processus qui continuent de créer des pertes de personnel. Il existe d'autres façons de rendre l'application plus rapide.


Bien sûr, il n'est pas toujours facile d'optimiser l'application. Mais si vous avez des tâches qui prennent un temps relativement long à terminer, vous devez les mettre dans les fils d'arrière-plan. Assurez-vous que ces tâches ne sont pas liées à l'interface utilisateur, car de nombreuses classes UIKit ne sont pas thread-safe, c'est-à-dire que vous ne pouvez pas les créer dans le backend.


Utilisez Core Graphics si vous devez traiter des images sur un flux tiers. Ne masquez pas l'affichage des bibliothèques système. N'oubliez pas les avertissements de mémoire insuffisante.


Bienvenue sur MBLT DEV 2018


Venez le 28 septembre à la 5e conférence internationale des développeurs mobiles MBLT DEV 2018 à Moscou. Les premiers intervenants sont déjà sur le site, et le dernier lève-tôt est toujours en vente. Le prix des billets augmentera le 29 juin. Achetez vos billets maintenant au prix le plus bas.



Découvrez la mise en œuvre de l'interface utilisateur dans iOS, l'utilisation des courbes de Bézier et d'autres outils utiles dans la deuxième partie de l'article, que nous publierons le 28 juin.

Source: https://habr.com/ru/post/fr415155/


All Articles