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
- Description et calcul de la disposition
1.1. Disposition automatique
1.2. Calcul de frame
manuel - 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 - Comment fonctionne le flux VKontakte?
- 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 - 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.layoutSubviews
ou d'une hiérarchie complexe desUIView
couchesaffiché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 .
, ( ). , : , .
, 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
— . , , .
, — . , .
, :
- Apple .
- Auto Layout .
- The Cassowary Linear Arithmetic Constraint Solving Algorithm .
- iOS Core Animation: Advanced Techniques. Nick Lockwood.
- WWDC 2014 Session 419. Advanced Graphics and Animations for iOS Apps.
