[Android开发学iOS系列] Auto Layout
<h1 id="android开发学ios系列-auto-layout"> Auto Layout</h1><p>内容:</p>
<ul>
<li>介绍什么是Auto Layout.</li>
<li>基本使用方法
<ul>
<li>在代码中写约束的方法</li>
</ul>
</li>
<li>Auto Layout的原理</li>
<li>尺寸和优先级</li>
<li>Auto Layout的使用细则
<ul>
<li>重要的属性</li>
<li>StackView</li>
<li>Layout Guide</li>
</ul>
</li>
<li>Performance</li>
<li>Debugging</li>
</ul>
<h2 id="what-is-auto-layout">What is Auto Layout</h2>
<p>Auto Layout会根据constraints(约束)动态计算出view hierarchy中所有View的位置和大小.</p>
<p>对于Android开发者来说, Auto Layout很容易上手, 它非常像<code>ConstraintLayout</code>和<code>RelativeLayout</code>: 给View规定它上下左右和谁对齐, 决定UI的位置和大小.</p>
<p>Auto Layout的约束更宽泛一些, 不仅仅是两个View之间的关系, 还有宽高, 比率等设置, 并且可以有一些大于小于等的范围设置.</p>
<h3 id="auto-layout不是一个view">Auto Layout不是一个View</h3>
<p>开始学Auto Layout我还以为它是一个叫<code>AutoLayout</code>的View, 把其他子View包进去然后设置一些放置规则, 就类似于Android的<code>ConstraintLayout</code>或者<code>RelativeLayout</code>.</p>
<p>但是其实不是, AutoLayout不是一个具体的View, 它代表的是一种计算引擎. 因为在代码里你从来不需要写<code>AutoLayout</code>这个关键字, 写的从来都是<code>Constraints</code>.</p>
<p>开发者为View设置足够多的约束, 规定和这个View位置和大小相关的因素, 这个引擎就可以为我们计算出View的位置和大小.</p>
<h3 id="autolayout为了解决什么问题">AutoLayout为了解决什么问题</h3>
<p>不同屏幕适配; 可以合理应对变化的responsive UI.</p>
<p>改变布局有内外两种因素, 除了屏幕尺寸, 屏幕旋转, 窗口大小改变等外部因素.</p>
<p>内部因素还包含了内容的动态变化, 国际化的支持, 字体的调整等.</p>
<h3 id="和auto-layout平行的解决方案是什么">和Auto Layout平行的解决方案是什么</h3>
<p>摆放UI有三种主要的方法:</p>
<ul>
<li>在程序里给每个View设置frame.</li>
<li>设置frame, 结合使用autoresizing masks来应对外部变化. (autoresizing mask定义了一个view的frame在它的superview frame变化时应该如何变化.)</li>
<li>使用Auto Layout.</li>
</ul>
<p>可以看出第二种只是在基于frame的方式上做出了一点改进, 所能应对的也仅仅是外部变化, 有一定的局限性. 所以可以把前两种归类为一种.</p>
<p>这也正是Auto Layout出现之前的解决方案, 即基于frame的布局方式.</p>
<p>Auto Layout的思考点不再着眼于view frame, 而是view的relationship.</p>
<h2 id="如何使用auto-layout">如何使用Auto Layout</h2>
<p>写iOS的UI有多种方式, Auto Layout属于UIKit, 在写的时候, 可以用storyboard, 也可以直接在代码中写约束.</p>
<p>在storyboard里面有一些好处, 比如所见即所得, 而且ide会给出一些warnings, 比如控件在storyboard上的位置与约束不一致, 会提示, 并且可以选择方式修复.<br>
在storyboard里面写约束确实是不容易出错的一种方式, xcode的操作也很直观, 这里不做演示了.</p>
<p>之前我们也讨论过, 用storyboard写UI存在阅读性差, 代码版本管理和团队合作都有问题等.<br>
所以具体使用需要看实际情况.</p>
<p>关于约束, location和size的约束不能混着用, 这个也是从逻辑上就可以理解的.<br>
比如让某个view的top和parent的top对齐(或者再offset个常量)是可以的, 但是让top等于某个size就不能理解了.</p>
<h3 id="在代码中创建约束">在代码中创建约束</h3>
<p>如果不用Interface Builder, 而是选择在代码中创建约束, 那么仍然有多种选择:</p>
<ul>
<li>使用layout anchor.</li>
<li>使用<code>NSLayoutConstraint</code>类.</li>
<li>使用Visual Format Language.</li>
</ul>
<p>我们在改变约束的时候通常不会add/remove constraints, 而是active/deactivate.</p>
<h4 id="使用layout-anchor">使用Layout anchor</h4>
<p>这个方法可能是最直观的一种方法.</p>
<pre><code class="language-swift">// Get the superview's layout
let margins = view.layoutMarginsGuide
// Pin the leading edge of myView to the margin's leading edge
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
// Pin the trailing edge of myView to the margin's trailing edge
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
// Give myView a 1:2 aspect ratio
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0).isActive = true
</code></pre>
<p>这里我们把每一条约束设置了<code>isActive = true</code>.</p>
<p>也可以直接放在一个数组里一起activate, 会有性能优势:</p>
<pre><code class="language-swift">NSLayoutConstraint.activate([
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0)
])
</code></pre>
<h4 id="使用nslayoutconstraint">使用NSLayoutConstraint</h4>
<p>使用<code>NSLayoutConstraint</code>写起来比较啰嗦, 必须给每个参数都指定值:</p>
<pre><code class="language-swift">NSLayoutConstraint(item: myView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: myView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: myView, attribute: .height, relatedBy: .equal, toItem: myView, attribute:.width, multiplier: 2.0, constant:0.0).isActive = true
</code></pre>
<p>这个不但写起来麻烦, 可读性也很差.</p>
<h4 id="visual-format-language-vfl">Visual Format Language (VFL)</h4>
<pre><code class="language-swift">let views = ["myView" : myView]
let formatString = "|--|"
let constraints = NSLayoutConstraint.constraints(withVisualFormat: formatString, options: .alignAllTop, metrics: nil, views: views)
NSLayoutConstraint.activate(constraints)
</code></pre>
<p>用一些键盘符号来表达这个布局的. (like a way of drawing the layout you want with a series of keyboard symbols)</p>
<p>管道符号代表parent view的边边.</p>
<h2 id="auto-layout的工作原理">Auto Layout的工作原理</h2>
<p><img src="https://img2022.cnblogs.com/blog/325852/202211/325852-20221102232327277-1389364526.png" alt="Auto Layout Internal" loading="lazy"></p>
<p>图来自于: https://developer.apple.com/videos/play/wwdc2018/220</p>
<p>Render loop包含如上三个阶段:</p>
<ul>
<li>update constraints从叶子节点向上.</li>
<li>layout从parent节点向下执行.</li>
<li>display即最后的绘制阶段.</li>
</ul>
<p>这三个阶段对应的方法:<br>
<img src="https://img2022.cnblogs.com/blog/325852/202211/325852-20221102232351375-1409267267.png" alt="Auto Layout methods" loading="lazy"></p>
<h3 id="update-constraints">Update Constraints</h3>
<p>它的工作是:</p>
<ul>
<li>把每个公式(约束)加入计算引擎Engine里.</li>
<li>计算引擎负责解出变量: 最后的frame.</li>
<li>通知View: Superview: setNeedsLayout().</li>
</ul>
<p>engine这里扮演一个layout cache和tracker. 收到变化时它会重新计算.</p>
<h3 id="layout">Layout</h3>
<p>从engine得到信息后, Subview setBounds(), subview setCenter().</p>
<h2 id="尺寸和优先级">尺寸和优先级</h2>
<p>了解了Auto Layout的原理之后, 看尺寸和优先级的部分就很好理解.</p>
<h3 id="intrinsic-content-size">Intrinsic content size</h3>
<p>有一些View有固有内容尺寸, 对于AutoLayout来说, 会默认使用intrinsic content size, 这样开发者就不用非得提供尺寸信息.</p>
<p>默认使用: intrinsic content size. 固有内容尺寸.</p>
<ul>
<li>UIImageView: image size.</li>
<li>UILabel: text size.</li>
</ul>
<h3 id="优先级">优先级</h3>
<p>优先级的值可以从1到1000, 默认是1000.</p>
<ul>
<li>Required: 1000</li>
<li>Default High: 750</li>
<li>Default Low: 250</li>
</ul>
<p>有优先级是因为多个constraints之间可能会有冲突, 那么约束的要求可能不能完全100%满足, 计算引擎会在在不能满足的情况下, 尽量地减少偏差.</p>
<p>约束的优先级就用来表示哪条约束我们更加关心, 更想满足, 优先考虑.</p>
<h3 id="优先级相关的变量">优先级相关的变量</h3>
<ul>
<li>content hugging priority: 尺寸比固有内容更大的可能性. 默认250. 值越小表示View更愿意扩张来满足约束了; 值越大表示View希望尽可能地接近固有尺寸.</li>
<li>content compression resistance priority: 尺寸比固有内容尺寸更小的阻力程度. 默认750. 值越大表示这个View压缩内容的可能性越小.</li>
</ul>
<h2 id="auto-layout的使用细则">Auto Layout的使用细则</h2>
<h3 id="properties--functions">Properties & Functions</h3>
<p>有个重要的属性要提一下:</p>
<ul>
<li><code>translatesAutoresizingMaskIntoConstraints</code></li>
</ul>
<p>这个属性是为了兼容Auto Layout出现之前的基于frame布局的legacy layout系统, 帮助View在Auto Layout的世界里, 以legacy layout system的方式运作.</p>
<p>当这个属性为true, 并且设置了frame时, 引擎会自动生成constraints来满足这个frame.</p>
<p>这个View的属性默认为true. 当我们要用constraints时需要设置为false.</p>
<ul>
<li>当在storyboard中开始为View设置constraints时, 会自动设置为false.</li>
<li>当我们在代码中给view设置约束之前, 需要自己显式地把这个属性设置为false.</li>
</ul>
<p>如果还是用frame布局, 这个属性不用设置成false. 比如在循环里生成很多view的时候, 可能想有一些尺寸和位置用frame设置.</p>
<ul>
<li>sizeToFit(): 刚好包裹内容的大小.</li>
</ul>
<h3 id="stack-view">Stack View</h3>
<p>Stack View是在Auto Layout的基础上的, 帮助我们做一些水平或者垂直的布局, 不用写内部元素间的constraints. (类似于Android中的<code>LinearLayout</code>.)</p>
<p>往Stack View里加需要叠放的元素用的是<code>addArrangedSubview()</code>这个方法.</p>
<p>与此同时, <code>addSubview()</code>方法可以用来加一些别的View.</p>
<p>几个属性:</p>
<ul>
<li>axis: 主轴方向.</li>
<li>alignment: 对齐方式.</li>
<li>distribution: 沿着主轴的分布.</li>
</ul>
<p>Stack View是比较轻量的, 所以官方会建议尽量多使用Stack View, 只在有必要的时候写约束.<br>
确实方便很多.</p>
<h3 id="layout-guide">Layout Guide</h3>
<p>很多时候为了布局的需要我们可能要包裹View或者是添加一下辅助View, 每个View都有自己的layer, 所以为了改进性能, 我们可以使用Layout Guide.</p>
<p>View自带一个layoutMarginsGuide.</p>
<p>还挺方便的. (看了这个视频: https://www.youtube.com/watch?v=4qPcMGiSADA)</p>
<h2 id="performance--building-efficient-layouts">Performance & Building Efficient Layouts</h2>
<p>iOS12对AutoLayout的性能做了很多改进, 这个WWDC的talk有讲.</p>
<p>关于有效率的布局, 简而言之就是少做无用功.</p>
<h3 id="constraint-churn">Constraint Churn</h3>
<p><code>constraint churning</code>是个典型的性能问题.<br>
<code>churn</code>: 搅动.</p>
<p>constraint churn是指更新了constraints, 但实际上view并不需要移动.</p>
<p>这样是给engine发送了额外的信息, 达到一定数量之后, 就会影响性能.</p>
<p>需要注意的是:</p>
<ul>
<li>不要remove all constraints然后又add all. 可以把它们分组, 哪些是固定不变的, 那么addView的时候就加上, 然后activate; 对于需要动态变化的部分可以分两组(比如一个根据内容动态决定是否需要显示图片的例子, 可以有两个数组: imageConstraints和noImageConstraints), 单独activate/deactivate这两组约束.</li>
<li>使用isHidden可以提高效率. 比起add/remove Subview来说.</li>
</ul>
<p>也是WWDC2018/220里提到的, 如何避免Constraint Churn:</p>
<ul>
<li>Avoid removing all constraints</li>
<li>Add static constraints once</li>
<li>Only change the constraints that need changing</li>
<li>Hide views instead of removing them</li>
</ul>
<h3 id="size">Size</h3>
<p>可以选择性地override一些尺寸, 减少text measure计算的过程:</p>
<ul>
<li>Return size if known without text measurement</li>
<li>Use <code>UIView.noIntrinsicMetric</code> and constraints.</li>
</ul>
<h3 id="system-layout-size-fitting-size">System Layout Size Fitting Size</h3>
<p>intrinsic content size是view传给engine的.</p>
<p>而这个system layout size fitting size, 是从engine取出来的.</p>
<p>但是它有想不到的性能消耗. (every time you call the method, an engine is created and discarded.)</p>
<h2 id="debugging">Debugging</h2>
<p>Auto Layout中由约束引起的错误可能会有:</p>
<ul>
<li>约束自相矛盾(冲突), 不能满足, 无解. (比如一个宽度即等于100又等于200, ???)</li>
<li>约束不足导致有很多可能的解. (Engine会给出一个解, 但可能不是你想要的.)</li>
</ul>
<p>关于怎么debug可以看: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/TypesofErrors.html#//apple_ref/doc/uid/TP40010853-CH17-SW1</p>
<p>大体上是根据Log还有一些可能有帮助的view的属性和方法(供debug用).</p>
<p>这个视频(https://developer.apple.com/videos/play/wwdc2015/219/)的后半段有讲debug.</p>
<p>这里还有一个小工具网站: https://www.wtfautolayout.com/</p>
<h2 id="summary">Summary</h2>
<p>Auto Layout是线性代数的应用实例.</p>
<p>有时候搬砖搬久了是不是应该慢下来欣赏一下数学的美.</p>
<h2 id="references">References</h2>
<ul>
<li>Understanding Auto Layout - Official Doc</li>
<li>High Performance Auto Layout - WWDC2018</li>
<li>Mysteries of Auto Layout, Part 1 - WWDC 2015</li>
<li>Mysteries of Auto Layout, Part 2 - WWDC 2015</li>
<li>Auto Layout Basics at codepath</li>
<li>The Auto Layout cheat sheet</li>
<li>Behind the Scenes with Auto Layout - iOS Conf SG 2019</li>
<li>AutoLayout Log分析小工具</li>
</ul>
</div>
<div id="MySignature" role="contentinfo">
作者: 圣骑士Wind<br>
出处: 博客园: 圣骑士Wind<br>
Github: https://github.com/mengdd<br>
微信公众号: 圣骑士Wind<br>
<img src="https://images.cnblogs.com/cnblogs_com/mengdd/869539/o_200422055937qrcode_for_gh_0e2ed690dcda_258.jpg" alt="微信公众号: 圣骑士Wind"><br><br>
来源:https://www.cnblogs.com/mengdd/p/iOS-AutoLayout-basics.html
頁:
[1]