| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
uigl1418
8年前发布

iOS从 Auto Layout 的布局算法谈性能

   <p>这是使用 ASDK 性能调优系列的第二篇文章,前一篇文章中讲到了如何提升 iOS 应用的渲染性能,你可以点击 <a href="/misc/goto?guid=4959713429035780262" rel="nofollow,noindex">这里</a> 了解这部分的内容。</p>    <p>在上一篇文章中,我们提到了 iOS 界面的渲染过程以及如何对渲染过程进行优化。ASDK 的做法是将渲染绘制的工作抛到后台线程进行,并在每次 Runloop 结束时,将绘制结果交给 CALayer 进行展示。</p>    <p>而这篇文章就要从 iOS 中影响性能的另一大杀手,也就是万恶之源 Auto Layout(自动布局)来分析如何对 iOS 应用的性能进行优化以及 Auto Layout 到底为什么会影响性能?</p>    <p><img src="https://simg.open-open.com/show/ca08c29cc0ca7adaf43c1278ecaced47.jpg"></p>    <h2>把 Auto Layout 批判一番</h2>    <p>由于在 2012 年苹果发布了 4.0 寸的 iPhone5,在 iOS 平台上出现了不同尺寸的移动设备,使得原有的 frame 布局方式无法很好地适配不同尺寸的屏幕,所以,为了解决这一问题 Auto Layout 就诞生了。</p>    <p>Auto Layout 的诞生并没有如同苹果的其它框架一样收到开发者的好评,它自诞生的第一天起就饱受 iOS 开发者的批评,其蹩脚、冗长的语法使得它在刚刚面世就被无数开发者吐槽,写了几个屏幕的代码都不能完成一个简单的布局,哪怕是 VFL(Visual Format Language)也拯救不了它。</p>    <p>真正使 Auto Layout 大规模投入使用的应该还是 <a href="/misc/goto?guid=4958877303436721101" rel="nofollow,noindex">Masonry</a> ,它使用了链式的语法对 Auto Layout 进行了很好的封装,使得 Auto Layout 更加简单易用;时至今日,开发者也在日常使用中发现了 Masonry 的各种问题,于是出现了各种各样的布局框架,不过这都是后话了。</p>    <p><img src="https://simg.open-open.com/show/133ce07815f6bc9e5606ec65b164dd2e.jpg"></p>    <h2>Auto Layout 的原理和 Cassowary</h2>    <p>Auto Layout 的原理其实非常简单,在这里通过一个例子先简单的解释一下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7d04e49b589bc1883cb1ff46591e1e88.png"></p>    <p>iOS 中视图所需要的布局信息只有两个,分别是 origin/center 和 size ,在这里我们以 origin & size 为例,也就是 frame 时代下布局的需要的两个信息;这两个信息由四部分组成:</p>    <ul>     <li>x & y</li>     <li>width & height</li>    </ul>    <p>以左上角的 (0, 0) 为坐标的原点,找到坐标 (x, y) ,然后绘制一个大小为 (width, height) 的矩形,这样就完成了一个最简单的布局。而 Auto Layout 的布局方式与上面所说的 frame 有些不同, frame 的原理是与父视图之间的绝对距离,但是 Auto Layout 中大部分的约束都是 <strong>描述性的</strong> ,表示视图间相对距离,以上图为例:</p>    <pre>  A.left = Superview.left + 50    A.top  = Superview.top + 30    A.width  = 100    A.height = 100    B.left = (A.left + A.width)/(A.right) + 30    B.top  = A.top    B.width  = A.width    B.height = A.height</pre>    <p>虽然上面的约束很好的表示了各个视图之间的关系,但是 Auto Layout 实际上并没有改变原有的 Hard-Coded 形式的布局方式,只是将原有没有太多意义的 (x, y) 值,变成了描述性的代码。我们仍然需要知道布局信息所需要的四部分 x 、 y 、 width 以及 height 。换句话说,我们要求解上述的 <strong>八元一次</strong> 方程组,将每个视图所需要的信息解出来;Cocoa 会在运行时求解上述的方程组,最终使用 frame 来绘制视图。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/74350112f44c61e080cca9faf9d1b353.png"></p>    <h3>Cassowary 算法</h3>    <p>在上世纪 90 年代,一个名叫 <a href="/misc/goto?guid=4959713429158491034" rel="nofollow,noindex">Cassowary</a> 的布局算法解决了用户界面的布局问题,它通过将布局问题抽象成线性等式和不等式约束来进行求解。</p>    <p>Auto Layout 其实就是对 Cassowary 算法的一种实现,但是这里并不会对这里进行展开介绍,有兴趣的读者可以在文章最后的 Reference 中了解一下 Cassowary 算法相关的文章。</p>    <p>Auto Layout 的原理就是对 <strong>线性方程组或者不等式</strong> 的求解。</p>    <h2>Auto Layout 的性能</h2>    <p>在我们使用 Auto Layout 进行布局时,可以指定一系列的约束,比如视图的高度、宽度是多少等等。而每一个约束其实都是一个简单的线性等式或不等式,整个界面上的所有约束在一起就 <strong>明确地(没有冲突)</strong> 定义了整个系统的布局。</p>    <p>在涉及冲突发生时,Auto Layout 会尝试 break 一些优先级低的约束,能够尽量满足最多并且优先级最高的约束。</p>    <p>因为布局系统在最后仍然需要通过 frame 来进行,所以 Auto Layout 虽然为开发者在描述布局时带来了一些好处,不过它相当于在原有的布局系统中加了从约束计算 frame 的过程,而在这里,我们需要了解 Auto Layout 的性能到底是怎么样的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/01928fe441c0fad0e7ffd5faf96fd4e4.jpg"></p>    <p>因为使用 Cassowary 算法解决约束问题就是对线性等式或不等式求解,所以其时间复杂度就是 <strong>多项式时间</strong> 的,不难推测出,在处理极其复杂的 UI 界面时,会造成性能上的巨大损失。</p>    <p>在这里我们会对 Auto Layout 的性能进行测试,为了更明显的展示 Auto Layout 的性能,我们通过 frame 的性能建立一条基准线 以消除对象的创建和销毁、视图的渲染、视图层级的改变带来的影响 。</p>    <p>代码分别使用 Auto Layout 和 frame 对 N 个视图进行布局,测算其执行时间。</p>    <p>使用 AutoLayout 时,每个视图会随机选择两个视图对它的 top 和 left 进行约束,随机生成一个数字作为 offset ;同时,还会用几个优先级高的约束保证视图的布局不会超出整个 keyWindow 。</p>    <p>而下图就是对 100~1000 个视图布局所需要的时间的折线图。</p>    <p>这里的数据是在 OS X EL Captain,Macbook Air (13-inch Mid 2013)上的 iPhone 6s Plus 模拟器上采集的, Xcode 版本为 7.3.1。在其他设备上可能不会获得一致的信息,由于笔者的 iPhone 升级到了 iOS 10,所以没有办法真机测试,最后的结果可能会有一定的偏差。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ad16ca6a212708e453d6677578a3559b.jpg"></p>    <p>从图中可以看到,使用 Auto Layout 进行布局的时间会是只使用 frame 的 <strong>16 倍</strong> 左右,如果去掉设置 frame 的过程消耗的时间,Auto Layout 过程进行的计算量也是非常巨大的。</p>    <p>在上一篇文章中,我们曾经提到,想要让 iOS 应用的视图保持 60 FPS 的刷新频率,我们必须在 <strong>1/60 = 16.67 ms</strong> 之内完成包括布局、绘制以及渲染等操作。</p>    <p>也就是说如果当前界面上的视图大于 100 的话,使用 Auto Layout 是很难达到绝对流畅的要求的;而在使用 frame 时,同一个界面下哪怕有 500 个视图,也是可以在 16.67 ms 之内完成布局的。不过在一般情况下,在 iOS 的整个 UIWindow 中也不会一次性出现如此多的视图。</p>    <p>我们更关心的是,在日常开发中难免会使用 Auto Layout 进行布局,既然有 16.67 ms 这个限制,那么在界面上出现了多少个视图时,我才需要考虑其它的布局方式呢?在这里,我们将需要布局的视图数量减少一个量级,重新绘制一个图表:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1370cbd5f00f714ca97b628e1ac8fd90.jpg"></p>    <p>从图中可以看出,当对 <strong>30 个左右视图</strong> 使用 Auto Layout 进行布局时,所需要的时间就会在 16.67 ms 左右,当然这里不排除一些其它因素的影响;到目前为止,会得出一个大致的结论,使用 Auto Layout 对复杂的 UI 界面进行布局时(大于 30 个视图)就会对性能有严重的影响。</p>    <p>上述对 Auto Layout 的使用还是比较简单的,而在日常使用中,使用嵌套的视图层级有非常正常。</p>    <p>在笔者对嵌套视图层级中使用 Auto Layout 进行布局时,当视图的数量超过了 500 时,模拟器直接就 crash 了,所以这里没有超过 500 个视图的数据。</p>    <p>我们对嵌套视图数量在 100~500 之间布局时间进行测量,并与 Auto Layout 进行比较:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c35ece956e61e55dc36ae3a503571a9c.jpg"></p>    <p>在视图数量大于 200 之后,随着视图数量的增加,使用 Auto Layout 对嵌套视图进行布局的时间相比非嵌套的布局成倍增长。</p>    <p>虽然说 Auto Layout 为开发者在多尺寸布局上提供了遍历,而且支持跨越视图层级的约束,但是由于其实现原理导致其时间复杂度为 <strong>多项式时间</strong> ,其性能损耗是仅使用 frame 的十几倍,所以在处理庞大的 UI 界面时表现差强人意。</p>    <p>在三年以前,有一篇关于 Auto Layout 性能分析的文章,可以点击这里了解这篇文章的内容 <a href="/misc/goto?guid=4959713429242019854" rel="nofollow,noindex">Auto Layout Performance on iOS</a> 。</p>    <h2>ASDK 的布局引擎</h2>    <p>Auto Layout 不止在复杂 UI 界面布局的表现不佳,它还会强制视图在主线程上布局;所以在 ASDK 中提供了另一种可以在后台线程中运行的布局引擎,它的结构大致是这样的:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e00c40c6ffc3a4bad9295b6187030478.jpg"></p>    <p>ASLayoutSpec 与下面的所有的 Spec 类都是继承关系,在视图需要布局时,会调用 ASLayoutSpec 或者它的子类的 - measureWithSizeRange: 方法返回一个用于布局的对象。</p>    <p>ASLayoutable 是 ASDK 中一个协议,遵循该协议的类实现了一系列的布局方法。</p>    <p>当我们使用 ASDK 布局时,需要做下面四件事情中的一件:</p>    <ul>     <li>提供 layoutSpecBlock</li>     <li>覆写 - layoutSpecThatFits: 方法</li>     <li>覆写 - calculateSizeThatFits: 方法</li>     <li>覆写 - calculateLayoutThatFits: 方法</li>    </ul>    <p>只有做上面四件事情中的其中一件才能对 ASDK 中的视图或者说结点进行布局。</p>    <p>方法 - calculateSizeThatFits: 提供了手动布局的方式,通过在该方法内对 frame 进行计算,返回一个当前视图的 CGSize 。</p>    <p>而 - layoutSpecThatFits: 与 layoutSpecBlock 其实没什么不同,只是前者通过覆写方法返回 ASLayoutSpec ;后者通过 block 的形式提供一种不需要子类化就可以完成布局的方法,这两者可以看做是完全等价的。</p>    <p>- calculateLayoutThatFits: 方法有一些不同,它把上面的两种布局方式:手动布局和 Spec 布局封装成了一个接口,无论是 CGSize 还是 ASLayoutSpec 最后都会以 ASLayout 的形式返回给方法调用者。</p>    <h3>手动布局</h3>    <p>这里简单介绍一下手动布局使用的方法 -[ASDisplayNode calculatedSizeThatFits:] 方法,这个方法与 UIView 中的 -[UIView sizeThatFits:] 非常相似,其区别只是在 ASDK 中,所有的计算出的大小都会通过缓存来提升性能。</p>    <pre>  - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize {    return _preferredFrameSize;  }</pre>    <p>子类可以在这个方法中进行计算,通过覆写这个方法返回一个合适的大小,不过一般情况下都不会使用手动布局的方式。</p>    <h3>使用 ASLayoutSpec 布局</h3>    <p>在 ASDK 中,更加常用的是使用 ASLayoutSpec 布局,在上面提到的 ASLayout 是一个保存布局信息的媒介,而真正计算视图布局的代码都在 ASLayoutSpec 中;所有 ASDK 中的布局(手动 / Spec)都是由 -[ASLayoutable measureWithSizeRange:] 方法触发的,在这里我们以 ASDisplayNode 的调用栈为例:</p>    <pre>  -[ASDisplayNode measureWithSizeRange:]      -[ASDisplayNode shouldMeasureWithSizeRange:]      -[ASDisplayNode calculateLayoutThatFits:]          -[ASDisplayNode layoutSpecThatFits:]          -[ASLayoutSpec measureWithSizeRange:]          +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:]          -[ASLayout filteredNodeLayoutTree]</pre>    <p>ASDK 的文档中推荐在子类中覆写 - layoutSpecThatFits: 方法,返回一个用于布局的 ASLayoutSpec 对象,然后使用 ASLayoutSpec 中的 - measureWithSizeRange: 方法对它指定的视图进行布局,不过通过覆写一节中的其它方法也都是可以的。</p>    <p>如果我们使用 ASStackLayoutSpec 对视图进行布局的话,方法调用栈大概是这样的:</p>    <pre>  -[ASDisplayNode measureWithSizeRange:]      -[ASDisplayNode shouldMeasureWithSizeRange:]      -[ASDisplayNode calculateLayoutThatFits:]          -[ASDisplayNode layoutSpecThatFits:]          -[ASStackLayoutSpec measureWithSizeRange:]              ASStackUnpositionedLayout::compute              ASStackPositionedLayout::compute            ASStackBaselinePositionedLayout::compute        +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:]          -[ASLayout filteredNodeLayoutTree]</pre>    <p>这里只是执行了 ASStackLayoutSpec 对应的 - measureWithSizeRange: 方法,对其中的视图进行布局。在 - measureWithSizeRange: 中调用了一些 C++ 方法 ASStackUnpositionedLayout 、 ASStackPositionedLayout 以及 ASStackBaselinePositionedLayout 的 compute 方法,这些方法完成了对 ASStackLayoutSpec 中视图的布局。</p>    <p>相比于 Auto Layout,ASDK 实现了一种完全不同的布局方式;比较类似与前端开发中的 Flexbox 模型,而 ASDK 其实就实现 Flexbox 的一个子集。</p>    <p>在 ASDK 1.0 时代,很多开发者都表示希望 ASDK 中加入 ComponentKit 的布局引擎;而现在,ASDK 布局引擎的大部分代码都是从 <a href="/misc/goto?guid=4959713429326625265" rel="nofollow,noindex">ComponentKit</a> 中移植过来的(ComponentKit 是另一个 非死book 团队开发的用于布局的框架)。</p>    <p>ASLayout</p>    <p>ASLayout 表示当前的结点在布局树中的大小和位置;当然,它还有一些其它的奇怪的属性:</p>    <pre>  @interface ASLayout : NSObject    @property (nonatomic, weak, readonly) id<ASLayoutable> layoutableObject;  @property (nonatomic, readonly) CGSize size;  @property (nonatomic, readwrite) CGPoint position;  @property (nonatomic, readonly) NSArray<ASLayout *> *sublayouts;  @property (nonatomic, readonly) CGRect frame;    ...    @end</pre>    <p>代码中的 layoutableObject 表示当前的对象, sublayouts 表示当前视图的子布局 ASLayout 数组。</p>    <p>整个类的实现都没有什么值得多说的,除了大量的构造方法,唯一一个做了一些事情的就是 -[ASLayout filteredNodeLayoutTree] 方法了:</p>    <pre>  - (ASLayout *)filteredNodeLayoutTree {    NSMutableArray *flattenedSublayouts = [NSMutableArray array];    struct Context {      ASLayout *layout;      CGPoint absolutePosition;    };    std::queue<Context> queue;    queue.push({self, CGPointMake(0, 0)});    while (!queue.empty()) {      Context context = queue.front();      queue.pop();        if (self != context.layout && context.layout.type == ASLayoutableTypeDisplayNode) {        ASLayout *layout = [ASLayout layoutWithLayout:context.layout position:context.absolutePosition];        layout.flattened = YES;        [flattenedSublayouts addObject:layout];      }        for (ASLayout *sublayout in context.layout.sublayouts) {        if (sublayout.isFlattened == NO) queue.push({sublayout, context.absolutePosition + sublayout.position});    }      return [ASLayout layoutWithLayoutableObject:_layoutableObject                           constrainedSizeRange:_constrainedSizeRange                                           size:_size                                     sublayouts:flattenedSublayouts];  }</pre>    <p>而这个方法也只是将 sublayouts 中的内容展平,然后实例化一个新的 ASLayout 对象。</p>    <p>ASLayoutSpec</p>    <p>ASLayoutSpec 的作用更像是一个抽象类,在真正使用 ASDK 的布局引擎时,都不会直接使用这个类,而是会用类似 ASStackLayoutSpec 、 ASRelativeLayoutSpec 、 ASOverlayLayoutSpec 以及 ASRatioLayoutSpec 等子类。</p>    <p>笔者不打算一行一行代码深入讲解其内容,简单介绍一下最重要的 ASStackLayoutSpec 。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a63baa3678fc4333763d7c6173f31132.jpg"></p>    <p>ASStackLayoutSpec 从 Flexbox 中获得了非常多的灵感,比如说 justifyContent 、 alignItems 等属性,它和苹果的 UIStackView 比较类似,不过底层并没有使用 Auto Layout 进行计算。如果没有接触过 ASStackLayoutSpec 的开发者,可以通过这个小游戏 <a href="/misc/goto?guid=4959713429414858344" rel="nofollow,noindex">Foggy-ASDK-Layout</a> 快速学习 ASStackLayoutSpec 的使用。</p>    <h3>关于缓存以及异步并发</h3>    <p>因为计算视图的 CGRect 进行布局是一种非常昂贵的操作,所以 ASDK 在这里面加入了缓存机制,在每次执行 - measureWithSizeRange: 方法时,都会通过 -shouldMeasureWithSizeRange: 判断是否需要重新计算布局:</p>    <pre>  - (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize {    return [self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _calculatedLayout.constrainedSizeRange);  }    - (BOOL)_hasDirtyLayout {    return _calculatedLayout == nil || _calculatedLayout.isDirty;  }</pre>    <p>在一般情况下,只有当前结点被标记为 dirty 或者这一次布局传入的 constrainedSize 不同时,才需要进行重新计算。在不需要重新计算布局的情况下,只需要直接返回 _calculatedLayout 布局对象就可以了。</p>    <p>因为 ASDK 实现的布局引擎其实只是对 frame 的计算,所以无论是在主线程还是后台的异步并发进程中都是可以执行的,也就是说,你可以在任意线程中调用 - measureWithSizeRange: 方法,ASDK 中的一些 ViewController 比如: ASDataViewController 就会在后台并发进程中执行该方法:</p>    <pre>  - (NSArray<ASCellNode *> *)_layoutNodesFromContexts:(NSArray<ASIndexedNodeContext *> *)contexts {    ...      dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_apply(nodeCount, queue, ^(size_t i) {      ASIndexedNodeContext *context = contexts[i];      ASCellNode *node = [context allocateNode];      if (node == nil) node = [[ASCellNode alloc] init];        CGRect frame = CGRectZero;      frame.size = [node measureWithSizeRange:context.constrainedSize].size;      node.frame = frame;        [ASDataController _didLayoutNode];    });      ...      return nodes;  }</pre>    <p>上述代码做了比较大的修改,将原有一些方法调用放到了当前方法中,并省略了大量的代码。</p>    <h3>关于性能的对比</h3>    <p>由于 ASDK 的布局引擎的问题,其性能比较难以测试,在这里只对 ASDK 使用 ASStackLayoutSpec 的 <strong>布局计算时间</strong> 进行了测试,不包括视图的渲染以及其它时间:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1a66a4d1ed5c35472826b3ea725af682.jpg"></p>    <p>测试结果表明 ASStackLayoutSpec 花费的布局时间与结点的数量成正比,哪怕计算 100 个视图的布局也只需要 <strong>8.89 ms</strong> ,虽然这里没有包括视图的渲染时间,不过与 Auto Layout 相比性能还是有比较大的提升。</p>    <h2>总结</h2>    <p>其实 ASDK 的布局引擎大部分都是对 ComponentKit 的封装,不过由于摆脱了 Auto Layout 这一套低效但是通用的布局方式,ASDK 的布局计算不仅在后台并发线程中进行、而且通过引入 Flexbox 的概念提升了布局的性能,但是 ASDK 的使用相对比较复杂,如果只想对布局性能进行优化,更推荐单独使用 ComponentKit 框架进行。</p>    <h2>References</h2>    <ul>     <li><a href="/misc/goto?guid=4959713429502232881" rel="nofollow,noindex">Cassowary, Cocoa Auto Layout, and enaml constraints</a></li>     <li><a href="/misc/goto?guid=4959713429586128299" rel="nofollow,noindex">Solving constraint systems</a></li>     <li><a href="/misc/goto?guid=4959713429242019854" rel="nofollow,noindex">Auto Layout Performance on iOS</a></li>     <li><a href="/misc/goto?guid=4959713429679410202" rel="nofollow,noindex">The Cassowary Linear Arithmetic Constraint Solving Algorithm: Interface and Implementation</a></li>     <li><a href="/misc/goto?guid=4959713429759562290" rel="nofollow,noindex">The Cassowary Linear Arithmetic Constraint Solving Algorithm</a></li>     <li><a href="/misc/goto?guid=4959713429849867633" rel="nofollow,noindex">Solving Linear Arithmetic Constraints for User Interface Applications</a></li>     <li><a href="/misc/goto?guid=4959713429928187903" rel="nofollow,noindex">AsyncDisplayKit 介绍(二)布局系统</a></li>    </ul>    <p> </p>    <p>来自:http://draveness.me/layout-performance/</p>    <p> </p>    
 本文由用户 uigl1418 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1472701267252.html
算法 iOS开发 移动开发