Collections d'affichage complexes dans iOS: problèmes et solutions sur l'exemple du flux VKontakte

Salut Je m'appelle Sasha, je suis développeur iOS dans l'équipe qui crée le flux VKontakte. Je vais maintenant vous expliquer comment nous optimisons l'affichage de l'interface et contournons les problèmes associés.
Je pense que vous pouvez imaginer ce qu'est la bande VK. Il s'agit d'un écran où vous pouvez visualiser une variété de contenus: textes, images statiques, gifs animés, éléments intégrés (vidéo et musique). Tout cela doit être affiché en douceur, d'où les exigences élevées sur les performances des solutions.


Voyons maintenant quelles approches standard pour travailler avec les mappages existent et quelles limitations ou avantages doivent être pris en compte.


Si vous aimez écouter plus que lire, l'enregistrement vidéo du reportage est ici .



Table des matières


  1. Description et calcul de la disposition
    1.1. Disposition automatique
    1.2. Calcul de frame manuel
  2. Calcul de la taille du texte
    2.1. Méthodes standard pour calculer la taille de UILabel / UITextView / UITextField
    2.2. NSAttributedString / NSString
    2.3. Textkit
    2.4. Coretext
  3. Comment fonctionne le flux VKontakte?
  4. Comment obtenir de meilleures performances
    4.1 Pourquoi des problèmes de performances
    4.2. CATransaction.commit
    4.3. Pipeline de rendu
    4.4. Les lieux de performance les plus vulnérables
  5. Outils de mesure
    5.1. Trace de système métallique
    5.2. Nous corrigeons les baisses de performances dans le code pendant l'exécution de l'application

  • Comment rechercher des problèmes. Recommandations
  • Conclusion
  • Sources d'information

1. Description et calcul de la disposition


Tout d'abord, rappelons comment créer une structure d'interface visuelle ( mise en page ) à l'aide d'outils classiques. Par souci d'économie d'espace, nous nous passerons des listes - je vais simplement lister les solutions et expliquer leurs fonctionnalités.


1.1. Disposition automatique


La façon la plus populaire de créer une interface dans iOS est peut-être d'utiliser le système de mise en page Auto Layout d' Apple. Il est basé sur l'algorithme de Cassowary , inextricablement lié au concept de contraintes.


Pour l'instant, n'oubliez pas que l'interface implémentée à l'aide de la mise en page automatique est basée sur des restrictions.


Caractéristiques de l'approche:


  • Le système de contraintes est transformé en un problème de programmation linéaire .
  • Cassowary résout le problème d'optimisation résultant en utilisant la méthode simplex . Cette méthode a une complexité asymptotique exponentielle. Qu'est-ce que cela signifie? À mesure que le nombre de contraintes dans la disposition augmente, dans le pire des cas, les calculs peuvent ralentir de façon exponentielle.
  • Les valeurs de frame résultantes pour UIView sont la solution au problème d'optimisation correspondant.

Avantages de l'utilisation de la mise en page automatique:


  • Sur des mappages simples, une complexité de calcul linéaire est possible .
  • Il s'entend bien avec tous les éléments standard, car il s'agit de la technologie "native" d'Apple.
  • UIView à l' UIView fonctionne avec UIView .
  • Disponible dans Interface Builder, qui vous permet de décrire la mise en page dans un Storyboard ou XIB.
  • Il garantit une solution à jour même pendant la transition. Cela signifie que la valeur de frame de chaque UIView toujours (!) Une solution à la tâche de mise en page réelle.

Les capacités du système sont suffisantes pour la plupart des écrans. Mais il ne convient pas pour créer des bandes avec une énorme quantité de contenu hétérogène. Pourquoi?


Il est important de se rappeler que la disposition automatique:


  • Fonctionne uniquement dans le thread principal . Supposons que les ingénieurs Apple aient choisi le Mainstream comme point de synchronisation de la solution de mise en page automatique et les valeurs de trame de tous les UIView . Sans cela, vous devrez calculer la disposition automatique dans un thread séparé et synchroniser constamment les valeurs avec le thread principal.
  • Il peut fonctionner lentement sur des représentations complexes , car il est basé sur un algorithme de force brute dont la complexité dans le pire des cas est exponentielle.
  • Disponible avec iOS 6.0 . Maintenant, ce n'est guère un problème, mais cela vaut la peine d'être considéré.

Conclusion: en utilisant la disposition automatique, il est pratique de créer des affichages sans ou avec des collections, mais sans relations complexes entre les éléments.


1.2. Calcul de frame manuel


L'essence de l'approche: nous calculons nous-mêmes toutes les valeurs de frame . Par exemple, nous implémentons les méthodes layoutSubviews , sizeThatFits . Autrement dit, dans layoutSubviews mêmes tous les éléments enfants, dans sizeThatFits nous calculons la taille correspondant à l'emplacement souhaité des éléments enfants et du contenu.


Qu'est-ce que ça donne? Nous pouvons transférer des calculs complexes vers le flux d'arrière-plan et des calculs relativement simples peuvent être effectués dans le flux principal.


Quel est le problème? Vous devez implémenter les calculs vous-même, il est facile de se tromper. Vous devez également vous assurer que la position des enfants et les résultats renvoyés dans sizeThatFits .


L'auto-évaluation est justifiée si:


  • Nous avons rencontré ou nous prévoyons rencontrer des limitations de performances de mise en page automatique.
  • l'application a une collection complexe et il y a de fortes chances que l'élément développé tombe dans l'une de ses cellules;
  • nous voulons calculer la taille de l'élément dans le fil d'arrière-plan;
  • nous affichons à l'écran des éléments non standard, dont la taille doit être constamment recomptée en fonction du contenu ou de l'environnement.

Un exemple. Dessinez des info-bulles qui évoluent automatiquement pour s'adapter au contenu. La partie la plus intéressante de cette tâche est de savoir comment calculer la taille visuelle du texte dans chaque info-bulle.




2. Calcul de la taille du texte


Ce problème peut être résolu de quatre manières au moins, chacune reposant sur son propre ensemble de méthodes. Et chacun a ses propres caractéristiques et limites.


2.1. Méthodes standard pour calculer la taille de UILabel / UITextView / UITextField


Les sizeThatFits (utilisées par défaut dans sizeToFit ) et intrinsicContentSize (utilisées dans Auto Layout) renvoient la taille préférée du contenu de la vue. Par exemple, avec leur aide, nous pouvons savoir combien d'espace le texte écrit dans UILabel .


L'inconvénient est que les deux méthodes ne fonctionnent que dans le thread principal - elles ne peuvent pas être appelées en arrière-plan.


Quand les méthodes standard sont-elles utiles?


  • Si nous utilisons déjà sizeToFit ou Auto Layout.
  • Lorsqu'il y a des éléments standard dans l'affichage, et nous voulons obtenir leur taille dans le code.
  • Pour tous les écrans sans collections complexes.

2.2. Méthodes NSAttributedString / NSString


Notez les sizeWithAttributes et sizeWithAttributes . Je ne conseille pas de les utiliser pour lire la taille du contenu de UILabel / UITextView / UITextField . Je n'ai trouvé nulle part dans la documentation des informations que les méthodes NSString et les méthodes de mise en page des éléments UIView sont basées sur le même code (mêmes classes). Ces deux groupes de classes appartiennent à des cadres différents: Foundation et UIKit, respectivement. Peut-être avez-vous déjà dû adapter le résultat UILabel taille UILabel ? Ou avez-vous rencontré le fait que les NSString ne prennent pas en compte la taille des emoji ? Ce sont les problèmes que vous pouvez rencontrer.


Je vais également vous dire quelles classes sont responsables du dessin du texte dans UILabel / UITextView / UITextField , mais pour le moment, UITextField aux méthodes.


L'utilisation de boundingRect et sizeWithAttributes en vaut la peine si nous:


  • Nous drawInRect éléments d'interface non standard à l'aide de drawInRect , drawAtPoint ou d'autres méthodes de la NSAttributedString NSString / NSAttributedString .
  • Nous voulons considérer la taille des éléments dans le flux d'arrière-plan. Encore une fois, ce n'est que lorsque vous utilisez les méthodes de rendu appropriées.
  • Dessinez sur un contexte arbitraire, par exemple, affichez une ligne au-dessus de l'image.

2.3. Textkit


Cet outil comprend les classes standard NLayoutManager , NSTextStorage et NSTextContainer . La disposition UILabel / UITextView / UITextField également basée sur eux.


TextKit est très pratique lorsque vous devez décrire en détail l'emplacement du texte et indiquer les formes autour desquelles il circulera :



À l'aide de TextKit, vous pouvez calculer la taille des éléments d'interface dans la file d'attente d'arrière-plan, ainsi que le frame lignes / caractères . De plus, le cadre vous permet de dessiner des glyphes et de changer complètement l'apparence du texte dans la mise en page existante. Tout cela fonctionne dans iOS 7.0 et supérieur.


TextKit est utile lorsque vous devez:


  • afficher du texte avec une mise en page complexe;
  • dessiner du texte sur des images;
  • calculer les tailles de sous-chaînes individuelles;
  • compter le nombre de lignes;
  • utiliser les résultats des calculs dans un UITextView .

J'insiste encore. Si vous devez calculer la taille de l' UITextView , nous configurons d'abord les instances des NSLayoutManager , NSTextStorage et NSTextContainer , puis passons ces instances à l' UITextView correspondante , où elles seront responsables de la mise en page. Ce n'est qu'ainsi que nous garantissons la pleine coïncidence de toutes les valeurs.


N'utilisez pas TextKit avec UILabel et UITextField ! Pour eux (contrairement à UITextView ), vous ne pouvez pas configurer NSLayoutManager , NSTextStorage et NSTextContainer .


2.4. Coretext


Il s'agit de l'outil de texte de niveau le plus bas dans iOS. Il donne un contrôle maximal sur le rendu des polices, des caractères, des lignes et des retraits. Et lui, comme TextKit, vous permet de calculer les paramètres typographiques du texte, tels que la ligne de base et la taille du cadre des lignes individuelles.


Comme vous le savez, plus il y a de liberté, plus la responsabilité est élevée. Et pour obtenir de bons résultats en utilisant CoreText, vous devez pouvoir utiliser ses méthodes.


CoreText assure la sécurité des threads pour les opérations sur la plupart des objets. Cela signifie que nous pouvons appeler ses méthodes à partir de différents threads. À titre de comparaison, lorsque vous utilisez TextKit, vous devez vous-même penser à la séquence des appels de méthode.


CoreText doit être utilisé si:


  • Une API de bas niveau extrêmement simple est nécessaire pour accéder directement aux paramètres de texte. Je dois dire tout de suite que pour la grande majorité des tâches, les capacités de TextKit sont suffisantes.
  • Il y a beaucoup de travail à faire avec les lignes individuelles ( CTLine ) et les caractères / éléments.
  • Le support est important dans iOS 6.0.

Pour le flux VKontakte, nous avons utilisé CoreText. Pourquoi? Juste au moment où nous avons implémenté les fonctions de base de travailler avec du texte, TextKit n'était pas encore là.




3. Comment fonctionne le flux VKontakte?


En bref sur la façon dont nous recevons les données du serveur, la présentation du formulaire et les affichages.



Tout d'abord, considérez les tâches effectuées dans la file d'attente d'arrière-plan. Nous recevons des données du serveur, les traitons et décrivons de manière déclarative l'affichage suivant. À ce stade, nous n'avons pas encore d'instances UIView , nous ne définissons que les règles et la structure de la future interface avec notre outil déclaratif, quelque peu similaire à SwiftUI . Pour calculer la mise en page, nous calculons la totalité du frame en tenant compte des restrictions actuelles, par exemple, la largeur de l'écran. Nous effectuons une mise à jour de la dataSource actuelle ( dataSourceUpdate ). Ici, dans la file d'attente d'arrière-plan, nous préparons les images: effectuez une décompression (voir la section performances pour plus de détails), dessinez des ombres, des arrondis et d'autres effets.


Allez maintenant dans la file d'attente principale. dataSourceUpdate reçu à l' UITableView , réutilisons et traitons les événements d'interface, remplissons les cellules.


Pour décrire notre système de mise en page, un article séparé serait nécessaire, mais ici je vais énumérer ses principales caractéristiques:


  • Une API déclarative est un ensemble de règles sur lesquelles une interface est construite.
  • Les composants de base forment un arbre ( nodes ).
  • Calculs simples dans les composants de base. Par exemple, dans les listes, nous calculons uniquement le décalage d' origin , en tenant compte de la largeur / hauteur de tous les enfants.
  • Les éléments de base ne créent pas de «conteneurs» inutiles de l' UIView dans la hiérarchie. Par exemple, le composant de liste ne forme pas une UIView supplémentaire et n'y ajoute pas d'enfants. Au lieu de cela, nous calculons le décalage d' origin des enfants par rapport à l'élément parent (pour la liste).
  • Gestion de texte de bas niveau avec CoreText.

Mais même avec cette approche, la visualisation des bandes peut ne pas être fluide en raison de problèmes de performances. Pourquoi?


Chaque cellule a une hiérarchie complexe de nodes . Et bien que les éléments de base ne créent pas de conteneurs inutiles, de nombreux UIView toujours affichés dans le ruban. Et lorsque vous remplissez la hiérarchie avec des «nœuds» (liaison de vue) dans la file d'attente principale, il y a du travail supplémentaire dont il est difficile de s'éloigner.


Nous avons essayé de transférer autant de tâches que possible dans la file d'attente d'arrière-plan et continuons maintenant à le faire. De plus, il existe des opérations gourmandes en CPU et en GPU qui doivent être prises en compte et contournées.




4. Comment obtenir de meilleures performances


La réponse la plus simple consiste à décharger le thread principal, le CPU et le GPU. Pour ce faire, vous devez comprendre en profondeur le travail des applications iOS. Et surtout, identifier les sources de problèmes.


4.1 Pourquoi des problèmes de performances


Animation de base, RunLoop et Scroll
Rappelons-nous comment l'interface est construite dans iOS. Au niveau supérieur, il y a UIKit , qui est chargé d'interagir avec l'utilisateur: traitement des gestes, sortie de l'application du sommeil et autres choses similaires. Pour le rendu de l'interface, un outil de niveau inférieur est responsable - Core Animation (comme dans macOS). Il s'agit d'un framework avec son propre système de description d'interface . Considérez les concepts de base de la construction d'une interface.


Pour Core Animation, toute l'interface est CALayer couches CALayer . Ils forment un arbre de rendu, géré via des transactions CATransaction .


Une transaction est un groupe de modifications, plus précisément, des informations sur la nécessité de mettre à jour quelque chose dans l'interface affichée. Toute modification du frame ou d'autres paramètres de couche tombe dans la transaction en cours. Si ce n'est pas déjà fait, le système lui-même crée une transaction implicite .


Plusieurs transactions forment une pile. Les nouvelles mises à jour tombent dans la transaction la plus élevée de la pile.


Nous savons maintenant que pour mettre à jour l'écran, nous devons former des transactions avec de nouveaux paramètres pour l'arborescence des couches.



Quand et comment créer des transactions? Dans notre application, les threads ont une entité appelée RunLoop . En termes simples, il s'agit d'une boucle infinie, à chaque itération dont la file d'attente d'événements actuelle est traitée.


Dans le thread principal, RunLoop nécessaire pour traiter les événements provenant de diverses sources, telles qu'une interface (gestes), des temporisateurs ou, par exemple, des gestionnaires pour recevoir des données de NSStream et NSPort .



Quel est le RunLoop Core Animation et RunLoop ? Nous avons constaté ci-dessus que lors de la modification des propriétés d'une couche dans l'arbre de rendu, le système crée des transactions implicites si nécessaire (par conséquent, nous n'avons pas besoin d'appeler CATransaction.begin pour redessiner quelque chose). De plus, à chaque itération RunLoop système ferme automatiquement les transactions ouvertes et applique les modifications apportées ( CATransaction.commit ).


Faites attention! Le nombre d'itérations RunLoop ne dépend pas du taux de rafraîchissement de l'écran. Le cycle n'est pas du tout synchronisé avec l'écran et fonctionne comme «sans fin while() ».


Voyons maintenant ce qui se passe dans les itérations RunLoop sur le thread principal pendant le défilement:


  ... if (dispatchBlocks.count > 0) { //   MainQueue doBlocks() } ... if (hasPanEvent) { handlePan() // UIScrollView change content offset -> change bounds } ... if (hasCATransaction) { CATransaction.commit() } ... 

Tout d'abord, les blocs ajoutés à la file d'attente principale via dispatch_async / dispatch_sync sont exécutés. Et tant qu'ils ne sont pas terminés, le programme ne procède pas aux tâches suivantes.


Ensuite, UIKit commence à traiter le mouvement panoramique de l'utilisateur. Dans le cadre du traitement de ce geste, UIScrollView.contentOffset change et, par conséquent, UIScrollView.bounds . La modification des bounds UIScrollView (respectivement, et de ses descendants UITableView , UICollectionView ) met à jour la partie visible du contenu ( viewport ).


À la fin de l'itération RunLoop , si nous avons des transactions ouvertes, la commit ou le RunLoop se produit automatiquement.


Pour vérifier comment cela fonctionne, placez les points d'arrêt aux endroits appropriés.
Voici à quoi ressemblera le traitement des gestes:



Et voici CATransaction.commit après handlePan :



Pendant la décélération du défilement, UIScrollView crée un minuteur CADisplayLink pour synchroniser le nombre de modifications de contentOffset par seconde avec le taux de rafraîchissement de l'écran.



Nous CATransaction.commit que CATransaction.commit ne se produit pas à la fin de l'itération RunLoop , mais directement dans le traitement du temporisateur CADisplayLink . Mais cela n'a pas d'importance:



4.2. CATransaction.commit


En fait, toutes les opérations dans CATransaction.commit sont effectuées sur des couches CALayer . layoutSublayers ont leurs propres méthodes pour mettre à jour la mise en page ( layoutSublayers ) et l'image ( drawLayer ). L'implémentation par défaut de ces méthodes entraîne des appels de méthode délégués . En ajoutant une nouvelle instance de UIView à la hiérarchie UIView , nous ajoutons implicitement la couche correspondante à la hiérarchie de couches Core Animation. Dans ce cas, l' UIView par défaut un délégué de sa couche. Comme vous pouvez le voir dans la pile des appels, UIView dans le cadre de l'implémentation des méthodes déléguées CALayer , exécute ses méthodes, qui seront discutées:



Comme nous travaillons habituellement avec la hiérarchie UIView , la description continuera avec des exemples d' UIView .


Pendant CATransaction.commit , la mise en page de tous les UIView marqués avec setNeedsLayout . Notez qu'une fois de plus, nous layoutSubviews pas layoutSubviews ou layoutIfNeeded raison de leur exécution différée garantie dans le système à l'intérieur de CATransaction.commit . Même si dans une transaction (entre les appels à CATransaction.begin et CATransaction.commit ) vous modifiez plusieurs fois la frame et appelez setNeedsLayout , chaque modification ne s'appliquera pas instantanément. Les modifications finales ne prendront effet qu'après avoir appelé CATransaction.commit . Méthodes CALayer pertinentes: setNeedsLayout , layoutIfNeeded et layoutSublayers .


Un groupe similaire pour le dessin est formé par les méthodes setNeedsDisplay et drawRect . Pour CALayer il s'agit de setNeedsDisplay , drawLayer et drawLayer . CATransaction.commit appelle les méthodes de rendu sur tous les éléments marqués avec setNeedsDisplay . Cette étape est parfois appelée dessin hors écran.


Un exemple . Pour la spécificité et la commodité, prenez le UITableView :


  ... // Layout UITableView.layoutSubviews() //  ,   .. ... // Offscreen drawing UITableView.drawRect() //    ... 

UIKit réutilise les UICollectionView UITableView / UICollectionView dans layoutSubviews : appelle la willDisplayCell déléguée willDisplayCell et ainsi de suite. Pendant CATransaction.commit , un dessin hors écran se produit: les méthodes drawInContext de tous les calques ou drawRect tous UIView , marquées comme setNeedsDisplay , sont setNeedsDisplay . Je remarque que lorsque nous drawRect quelque chose dans drawRect , cela se produit sur le thread principal, et nous devons de toute urgence changer l'affichage des calques pour un nouveau cadre. Il est clair qu'une telle solution peut être très inefficace.


Que se passe-t-il ensuite dans CATransaction.commit ? L'arbre de rendu est envoyé au serveur de rendu.


4.3. Pipeline de rendu


Rappelez tout le processus de formation d'un cadre d'interface dans iOS (pipeline de rendu [WWDC 2014 Session 419. Advanced Graphics and Animations for iOS Apps]):



Non seulement le processus de notre application est responsable de la formation du cadre - Core Animation fonctionne également dans un processus système séparé appelé Render Server.


Comment le cadre est formé. Nous (ou le système pour nous) créons une nouvelle transaction ( CATransaction ) dans l'application avec une description des modifications de l'interface, la «validons» et la transférons vers le serveur de rendu. Tout, côté application, le travail est fait. Ensuite, le serveur de rendu décode la transaction (arbre de rendu), appelle les commandes nécessaires sur la puce vidéo, dessine un nouveau cadre et l'affiche à l'écran.


Fait intéressant, lors de la création du cadre, un certain «multithreading» est utilisé. Si le taux de rafraîchissement de l'écran est de 60 images par seconde, une nouvelle image est formée au total non pas en 1/60, mais en 1/30 de seconde. En effet, pendant que l'application prépare un nouveau cadre, le serveur de rendu traite toujours le précédent:



En gros, le temps total de formation de la trame avant l'affichage à l'écran est de 1/60 seconde dans notre processus pour la formation de la transaction et 1/60 seconde dans le processus Render Server pendant le traitement de la transaction.


Je voudrais faire la remarque suivante. Nous pouvons paralléliser le dessin des couches nous - mêmes et rendre le contenu de la CGImage UIImage / CGImage dans le flux d'arrière-plan. Après cela, dans le thread principal, vous devez affecter l'image créée à la propriété CALayer.contents . En termes de performances, c'est une très bonne approche. Ce sont les développeurs qui l'utilisent Texture . Mais comme nous ne pouvons changer CALayer.contents que dans le processus de génération d'une transaction dans le processus de notre application, nous n'avons que 1/60 seconde à 60 images au lieu de 1/30 seconde pour créer et remplacer une nouvelle image (en tenant compte des optimisations et de la parallélisation du pipeline de rendu avec le serveur de rendu )


De plus, le serveur de rendu peut toujours gérer le mélange (voir ci-dessous) et la mise en cache des couches à court terme [iOS Core Animation: Advanced Techniques. Nick Lockwood].Et si nous n'avons pas le temps de dessiner une nouvelle image pour la propriété en 1/60 seconde CALayer.contents, le calque correspondant restera vide ou avec l'ancien contenu mis en cache. Mais c'est une approche bien parallélisée.


Conclusion: vous devez toujours choisir la solution la plus adaptée à une tâche particulière.


4.4. Les lieux de performance les plus vulnérables


Fil principal



Problème 1. Incapacité à appliquer rapidement les modifications ( CATransaction.commit) en raison d'une longue exécutionUIView.layoutSubviewsou d'une hiérarchie complexe desUIViewcouchesaffichées(plus précisément, des couchesCALayer). Pour résoudre ce problème, vous devez simplifier autant que possible la hiérarchie et les calculs dans le cadre delayoutSubviews/cellForRow/willDisplayCell.


2. drawInContext / drawRect . - Main- ( CATransaction.commit ) — . , .


3. . . CATransaction.commit , , .


4. . UIImage / CGImage .


5. . Main-thread , scroll. - , UI.


6. Main-. , RunLoop Main- , , Main-. .


GPU



Blending . GPU ( Render Server GPU, ). , , Background-.


. , UIBlurEffect , UIVibrancyEffect , , (Render Pass). , , .


Offscreen rendering (Render Server)



Render Server . , , :



CALayer , , Offscreen rendering. , UIVisualEffect ( , Render Server CPU, GPU).


, .




5.


, , Time Profiler. Metal System Trace — Time Profiler .


5.1. Metal System Trace


, ( ). , : , .


, Metal System Trace , . , Render Server. , Main-, — , .



- , :



Metal System Trace . 64- , iPhone 5s. , . , - , , UI.


5.2.


. , - - . , CADisplayLink .


CADisplayLink timestamp — ( Render Server). CADisplayLink.timestamp timestamp . , (, 1/60 ) :


  //  CADisplayLink. link = [CADisplayLink displayLinkWithTarget:target selector:selector] [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode] //    CADisplayLink : diff = prevTimestamp - link.timestamp if (diff > 1/fps) { //  freeze } prevTimestamp = link.timestamp 

CADisplayLink UITrackingRunLoopMode , .


Rendering Pipeline:


UI-, . «» freezeFrameTimeRate :


 scrollTime //    Scroll freezeFrameTime //    ,  "",       freezeFrameTimeRate = freezeFrameTime / scrollTime 

, - UIView . , «»:



, , « UIView » . Pourquoi? , . , , , : CADisplayLink , Render Server link.timetamp , Render Server , . 60 UI-, Render Server. Render Server , .


, , , Render Server . Metal , Render Server. , , iOS, Render Server .


.


, , . , .


: — ! — .




Conclusion


— . , , .





, — . , .


, :


  1. Apple .
  2. Auto Layout .
  3. The Cassowary Linear Arithmetic Constraint Solving Algorithm .
  4. iOS Core Animation: Advanced Techniques. Nick Lockwood.
  5. WWDC 2014 Session 419. Advanced Graphics and Animations for iOS Apps.

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


All Articles