iOS中的复杂显示集合:VKontakte feed示例中的问题和解决方案

你好 我叫Sasha,我是制作VKontakte供稿的团队中的iOS开发人员。 现在,我将告诉您如何优化界面显示并解决与此相关的问题。
我想您可以想象什么是VK磁带。 在此屏幕上,您可以查看各种内容:文本,静态图片,GIF动画,嵌入式元素(视频和音乐)。 所有这些都应平稳显示,因此对解决方案的性能提出了很高的要求。


现在,让我们看看存在哪些使用映射的标准方法以及应该考虑哪些限制或优点。


如果您喜欢听多于阅读,请在此处查看报告的录像。



目录内容


  1. 布局描述和计算
    1.1。 自动布局
    1.2。 手动frame计算
  2. 文字大小计算
    2.1。 计算UILabel / UITextView / UITextField大小的标准方法
    2.2。 NSAttributedString / NSString方法
    2.3。 文字套件
    2.4。 核心文字
  3. VKontakte提要如何工作?
  4. 如何获得更好的性能
    4.1为什么出现性能问题
    4.2。 CATransaction.commit
    4.3。 渲染管线
    4.4。 最脆弱的表演场所
  5. 测量工具
    5.1。 金属系统痕迹
    5.2。 我们在应用程序运行时修复代码中的性能下降

  • 如何研究问题。 推荐建议
  • 结论
  • 信息来源

1.布局的描述和计算


首先,让我们回想一下如何使用常规工具创建可视界面结构( 布局 )。 为了节省空间,我们将不列出任何内容-我将仅列出解决方案并说明其功能。


1.1。 自动布局


在iOS中创建界面的最流行的方法也许是使用Apple的自动版式布局系统。 它基于 Cassowary算法,与约束概念密不可分


现在,请记住,使用自动布局实现的界面是基于限制的。


该方法的特点:


  • 约束系统转化为线性规划问题
  • Cassowary使用单纯形法 解决了由此产生的优化问题。 该方法具有指数渐近复杂性。 这是什么意思? 随着布局中约束的数量增加,在最坏的情况下,计算速度可能呈指数下降。
  • UIView的最终frame值是相应优化问题的解决方案。

使用自动版式的好处:


  • 在简单映射上,线性计算复杂性是可能的
  • 它是Apple的“本机”技术,它与所有标准元素都相处得很好。
  • 开箱即用可与UIView
  • 在Interface Builder中可用,它允许您在情节提要或XIB中描述布局。
  • 即使在过渡期间,它也保证了最新的解决方案。 这意味着每个UIViewframe值始终UIView (!)解决实际布局任务的方法。

该系统功能足以满足大多数显示器的需求。 但是它不适用于创建具有大量异构内容的磁带。 怎么了


重要的是要记住自动布局:


  • 仅在主线程中有效 。 假设Apple工程师选择了Mainstream作为自动版式解决方案的同步点以及所有UIView的帧值。 否则,您将不得不在单独的线程中计算自动布局,并不断将值与主线程同步。
  • 它基于复杂的算法,在最坏情况下的复杂度是指数级的,因此它可以在复杂的表示形式上缓慢运行
  • 在iOS 6.0中 可用 。 现在这几乎不是问题,但值得考虑。

结论:使用自动版式可以方便地创建不带集合或不带集合的显示,但元素之间没有复杂的关系。


1.2。 手动frame计算


该方法的本质:我们自己计算所有frame值。 例如,我们实现方法layoutSubviewssizeThatFits 。 也就是说,在layoutSubviews自己安排所有子元素,在sizeThatFits我们计算与子元素和内容的所需位置相对应的大小。


它有什么作用? 我们可以将复杂的计算传递到Background流,并且可以在Main流中执行相对简单的计算。


怎么了 您必须自己实施计算,很容易出错。 您还需要确保子代的位置与sizeThatFits返回的结果sizeThatFits


在以下情况下,自我评估是合理的:


  • 我们已经遇到或预期会遇到自动版式性能限制。
  • 该应用程序具有复杂的集合,并且很有可能使已开发的元素落入其单元格之一;
  • 我们要计算Background线程中元素的大小;
  • 我们会在屏幕上显示非标准元素,必须根据内容或环境不断重新计算其大小。

一个例子。 绘图工具提示会自动缩放以适合内容。 此任务中最有趣的部分是如何计算每个工具提示中文本的视觉大小。




2.计算文字大小


可以通过至少四种方式解决此问题,每种方式都依赖于其自己的方法集。 而且每种都有自己的特点和局限性。


2.1。 计算UILabel / UITextView / UITextField大小的标准方法


sizeThatFits (默认在sizeToFit )和intrinsicContentSize (在Auto Layout中使用)方法返回视图内容的首选大小。 例如,在他们的帮助下,我们可以找出用UILabel编写的文本要占用多少空间。


缺点是这两种方法都只能在Main线程中工作-不能从后台调用它们。


标准方法什么时候有用?


  • 如果我们已经使用sizeToFit或Auto Layout。
  • 当显示中有标准元素时,我们希望在代码中获取它们的大小。
  • 对于任何没有复杂集合的显示。

2.2。 NSAttributedString / NSString方法


注意boundingRectsizeWithAttributes 。 我不建议使用它们来读取UILabel / UITextView / UITextField内容的大小。 我没有在文档信息中的任何地方找到NSString方法和UIView元素的布局方法基于同一代码(相同类)。 这两组类分别属于不同的框架:Foundation和UIKit。 也许您已经必须使boundingRect结果适合UILabel大小? 还是您遇到 NSString不考虑表情符号大小的事实? 这些是您可以获得的问题。


我还将告诉您哪些类负责绘制UILabel / UITextView / UITextField文本,但现在UITextField返回方法。


如果我们能够使用boundingRect和sizeWithAttributes是值得的:


  • 我们使用drawInRectdrawAtPointNSString / NSAttributedString其他方法绘制非标准接口元素。
  • 我们要考虑Background流中元素的大小。 同样,这仅在使用适当的渲染方法时。
  • 在任意上下文上绘制,例如,在图像顶部显示一条线。

2.3。 文字套件


该工具由标准类NLayoutManagerNSTextStorageNSTextContainer 。 布局UILabel / UITextView / UITextField基于它们。


当您需要详细描述文本的位置并指出它将围绕哪些形状流动时,TextKit十分方便。



使用TextKit,您可以计算后台队列中界面元素的大小以及行/字符 frame 。 此外,该框架还允许您绘制字形并完全改变现有布局中文本的外观。 所有这些在iOS 7.0及更高版本中均有效。


当您需要:


  • 显示具有复杂布局的文本;
  • 在图像上绘制文字;
  • 计算单个子字符串的大小;
  • 计算行数;
  • 使用UITextView的计算结果。

我再次强调。 如果需要提前计算UITextView的大小,我们首先配置NSLayoutManagerNSTextStorageNSTextContainer 实例 ,然后将这些实例传递给相应的 UITextView ,由它们负责布局。 只有这样,我们才能保证所有值的完全一致。


不要将TextKit与UILabelUITextField ! 对于它们(与UITextView不同),您无法配置NSLayoutManagerNSTextStorageNSTextContainer


2.4。 核心文字


这是iOS中最低级别的文本工具。 它可以最大程度地控制字体,字符,线条,缩进的呈现。 而且,他和TextKit一样,允许您计算文本的印刷参数,例如基线和每行的框架大小。


如您所知,自由越多,责任就越高。 为了使用CoreText获得良好的结果,您需要能够使用其方法。


CoreText为大多数对象上的操作提供线程安全性。 这意味着我们可以从不同的线程调用其方法。 为了进行比较,使用TextKit时,您自己必须考虑方法调用的顺序。


在以下情况下应使用CoreText:


  • 直接访问文本参数需要非常简单的低级API。 我必须马上说,对于绝大多数任务,TextKit的功能就足够了。
  • 单独的行( CTLine )和字符/元素有很多工作要做。
  • 支持在iOS 6.0中很重要。

对于VKontakte提要,我们使用了CoreText。 怎么了 就在我们实现处理文本的基本功能时,TextKit还不存在。




3. VKontakte提要如何工作?


简要介绍我们如何从服务器接收数据,表单布局和显示。



首先,考虑在后台队列中执行的任务。 我们从服务器接收数据,对其进行处理并以声明方式描述随后的显示。 在这个阶段,我们还没有UIView实例,我们仅使用声明工具来设置future接口的规则和结构,这与SwiftUI有点类似。 为了计算布局,我们在考虑当前限制(例如屏幕宽度)的情况下计算整个frame 。 我们更新当前的dataSourcedataSourceUpdate )。 在这里,我们在背景队列中准备图像:执行解压缩(有关更多详细信息,请参见性能部分),绘制阴影,倒圆角和其他效果。


现在转到主队列。 dataSourceUpdate接收到的dataSourceUpdate应用于UITableView ,重用并处理接口事件,填充单元格。


为了描述我们的布局系统,将需要另外一篇文章,但是在这里我将列出其主要功能:


  • 声明性API是在其上构建接口的一组规则。
  • 基本组件形成一棵树( nodes )。
  • 基本组成部分的简单计算。 例如,在列表中,我们仅考虑所有子项的宽度/高度来计算origin偏移。
  • 基本元素不会在层次结构中创建不必要的UIView “容器”。 例如,列表组件不会形成附加的UIView ,也不会为其添加子项。 相反,我们计算子项相对于父项(对于列表)元素的origin偏移。
  • 使用CoreText进行低级文本管理。

但是,即使采用这种方法,由于性能问题,磁带的观看可能仍然不够流畅。 怎么了


每个单元都有一个复杂的nodes层次结构。 而且,尽管基本元素不会创建不必要的容器,但功能区UIView仍显示许多UIView 。 并且在Main-queue中用“节点”(视图绑定)填充层次结构时,还有许多其他工作难以摆脱。


我们尝试将尽可能多的任务转移到“后台”队列中,现在继续这样做。 此外,必须考虑并规避CPU密集型和GPU密集型操作。




4.如何获得更好的性能


最简单的答案是卸载主线程,CPU和GPU。 为此,您需要深入了解iOS应用程序的工作。 最重要的是,找出问题的根源。


4.1为什么出现性能问题


核心动画, RunLoop和滚动
让我们记住该接口是如何在iOS中构建的。 在顶层,有UIKit ,它负责与用户进行交互:处理手势,使应用程序从睡眠中唤醒以及类似的事情。 为了渲染界面,需要使用较低级别的工具- 核心动画 (如macOS中一样)。 这是一个具有自己的接口描述系统的框架。 考虑构建接口的基本概念。


对于Core Animation,整个界面都是CALayer层。 它们形成了一个渲染树,通过CATransaction事务进行管理。


事务是一组更改,更准确地说,是有关在显示的界面中更新某些内容的信息。 frame或其他层参数的任何更改都属于当前事务。 如果还没有,系统本身会创建一个隐式事务


几个事务形成一个堆栈。 新的更新属于堆栈的最高事务。


现在我们知道要更新屏幕,我们需要使用层树的新参数来形成事务。



何时以及如何创建交易? 在我们的应用程序中,线程具有一个称为RunLoop实体 。 简单来说,这是一个无限循环,每次循环都会处理当前事件队列


在Main线程中,需要RunLoop来处理来自不同来源的事件,例如接口(手势),计时器或从NSStreamNSPort接收数据的处理程序。



核心动画和RunLoop ? 我们在上面发现,在更改“渲染树”中的图层属性时,系统会在必要时创建隐式事务(因此,我们无需调用CATransaction.begin 。开始重新绘制内容)。 此外,在每次RunLoop迭代时RunLoop系统都会自动关闭未完成的事务并应用所做的更改( CATransaction.commit )。


注意! RunLoop的迭代次数不取决于屏幕的刷新率。 该循环根本不与屏幕同步,其工作方式类似于“ Endless while() ”。


现在,让我们看看滚动过程中Main线程的RunLoop迭代中发生了什么:


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

首先,执行通过dispatch_async / dispatch_sync添加到Main队列中的块。 并且在完成之前,程序不会继续执行以下任务。


接下来,UIKit开始处理用户的手势 。 作为处理此手势的一部分, UIScrollView.contentOffset更改,因此, UIScrollView.bounds也会更改。 更改UIScrollView (分别及其后代UITableViewUICollectionView )的bounds更新内容的可见部分( viewport )。


RunLoop迭代结束时,如果我们有未完成的事务,则会自动commitflush


要检查它是如何工作的,请在适当的位置放置断点。
手势处理如下所示:



这是handlePan之后的CATransaction.commit



在滚动减速期间, UIScrollView创建一个CADisplayLink计时器,以将每秒对contentOffset的更改数量与屏幕的刷新率同步。



我们CATransaction.commitCATransaction.commit不在RunLoop迭代结束时发生,而是直接在CADisplayLink计时器的处理中CADisplayLink 。 但这无关紧要:



4.2。 CATransaction.commit


实际上, CATransaction.commit中的所有操作都在CALayer层上执行。 layoutSublayers具有自己的更新布局( layoutSublayers )和图像( drawLayer )的方法。 这些方法的默认实现导致委托方法调用。 通过将新的UIView实例添加到UIView层次结构中,我们隐式地将相应的图层添加到Core Animation图层层次结构中。 在这种情况下,默认情况下, UIView其图层的委托。 从调用堆栈中可以看到, UIView作为CALayer委托方法的实现的一部分,将执行其方法,将对此进行讨论:



由于我们通常使用UIView层次结构,因此将以UIView示例继续进行描述。


CATransaction.commit期间, setNeedsLayout所有带有setNeedsLayout标记的UIView的布局。 请注意,我们自己也不再次调用layoutSubviewslayoutIfNeeded因为它们保证了CATransaction.commit内部系统中的延迟执行。 即使在一次事务中(在调用CATransaction.beginCATransaction.commit ),您多次更改frame并调用setNeedsLayout ,但每次更改都不会立即CATransaction.commit 。 最终更改仅在调用CATransaction.commit之后CATransaction.commit 。 相关的CALayer方法: setNeedsLayoutlayoutIfNeededlayoutSublayers


通过setNeedsDisplaydrawRect方法可以形成类似的绘图束。 对于CALayer这是setNeedsDisplaydisplayIfNeededdrawLayerCATransaction.commit调用所有标记有setNeedsDisplay元素的呈现方法。 此步骤有时称为屏幕外绘制。


一个例子 。 为方便起见,请使用UITableView


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

UIKit重用layoutSubviews UITableView / UICollectionView :调用willDisplayCell委托willDisplayCell ,依此类推。 在CATransaction.commit期间,发生屏幕外绘制:标记为setNeedsDisplay的所有层的drawInContext方法或所有UIViewdrawRectsetNeedsDisplay 。 我注意到,当我们在drawRect绘制某些东西时,这发生在主线程上,我们迫切需要为新框架更改图层的显示。 显然,这样的解决方案可能效率很低。


CATransaction.commit接下来会发生什么? 渲染树将发送到渲染服务器。


4.3。 渲染管线


回顾在iOS中形成界面框架的整个过程(渲染管道[WWDC 2014 Session419。iOS应用的高级图形和动画]):



不仅我们应用程序的过程负责帧的形成-Core Animation还可以在名为Render Server的单独系统过程中工作。


框架的形成方式。 我们(或我们的系统)在应用程序中创建一个带有接口更改描述的新事务( CATransaction ),“提交”该事务CATransaction其传输到Render Server。 在应用程序方面,所有工作都已完成。 接下来,渲染服务器解码事务(渲染树),在视频芯片上调用必要的命令,绘制新帧并将其显示在屏幕上。


有趣的是,在创建框架时,使用了某种“多线程”。 如果屏幕刷新率为每秒60帧,则新形成的帧总数不是以1/60,而是以1/30秒。 这是因为在应用程序准备新帧时,Render Server仍在处理前一个帧:



粗略地说,在我们进行交易形成的过程中,在屏幕上显示之前帧形成的总时间为1/60秒,而在交易处理过程中,在Render Server进程中为1/60秒。


我想发表以下评论。 我们可以自己并行化图层的绘制,并在Background流中渲染 UIImage / CGImage 内容。 之后,在主线程中,您需要将创建的图像分配给CALayer.contents属性。 就性能而言,这是一种非常好的方法。 是使用它的开发人员Texture 。 但是由于我们只能在应用程序生成事务的过程中更改CALayer.contents ,因此在60帧时创建和替换新图像的时间只有1/60秒,而不是1/30秒(考虑到优化和渲染管道与Render Server的并行化) )


此外,Render Server仍可以处理混合(请参阅下文)和短期图层缓存[iOS核心动画:高级技术。 Nick Lockwood]. 1/60 CALayer.contents , . .


: , .


4.4.


Main-thread



1. ( CATransaction.commit ) - UIView.layoutSubviews UIView (, CALayer ). , layoutSubviews / 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 » . 怎么了 , . , , , : CADisplayLink , Render Server link.timetamp , Render Server , . 60 UI-, Render Server. Render Server , .


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


.


, , . , .


: — ! — .




结论


— . , , .





, — . , .


, :


  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/zh-CN481626/


All Articles