深刻剖析Auto Layout,分析iOS各版本新增特性

先前寫到的一篇Masonry心得文章裏已經提到了不少AutoLayout相關的知識,這篇我會更加詳細的對其知識要點進行分析和整理。php

來歷

通常你們都會認爲Auto Layout這個東西是蘋果本身搞出來的,其實否則,早在1997年Alan Borning, Kim Marriott, Peter Stuckey等人就發佈了《Solving Linear Arithmetic Constraints for User Interface Applications》論文(論文地址:http://constraints.cs.washington.edu/solvers/uist97.html)提出了在解決佈局問題的Cassowary constraint-solving算法實現,而且將代碼發佈在他們搭建的Cassowary網站上http://constraints.cs.washington.edu/cassowary/。後來更多開發者用各類語言來寫Cassowary。自從它發佈以來JavaScript,.NET,JAVA,Smalltall和C++都有相應的庫。2011年蘋果將這個算法運用到了自家的佈局引擎中,美其名曰Auto Layout。html

Cassowary

Cassowary是個解析工具包,可以有效解析線性等式系統和線性不等式系統,用戶的界面中老是會出現不等關係和相等關係,Cassowary開發了一種規則系統能夠經過約束來描述視圖間關係。約束就是規則,可以表示出一個視圖相對於另外一個視圖的位置。ios

Auto Layout的生命週期

進入下面主題前能夠先介紹下加入Auto Layout的生命週期。在獲得本身的layout以前Layout Engine會將Views,約束,Priorities(優先級),instrinsicContentSize(主要是UILabel,UIImageView等)經過計算轉換成最終的效果。在Layout Engine裏會有約束變化到Deferred Layout Pass再到應用Run Loop再回到約束變化這樣的循環機制。git

約束變化

觸發約束變化包括github

  • Activating或Deactivatingobjective-c

  • 設置constant或priority算法

  • 添加和刪除視圖xcode

這個Engine遇到約束變化會從新計算layout,獲取新值後會call它的superview.setNeedsLayout()app

Deferred Layout Pass

在這個時候主要是作些容錯處理,更新約束有些沒有肯定或者缺失佈局聲明的視圖會在這裏處理。接着從上而下調用layoutSubviews()來肯定視圖各個子視圖的位置,這個過程實際上就是將subview的frame從layout engine裏拷貝出來。這裏要注意重寫layoutSubviews()或者執行相似layoutIfNeeded這樣可能會馬上喚起layoutSubviews()的方法,若是要這樣作須要注意手動處理的這個地方本身的子視圖佈局的樹狀關係是否合理。框架

生命週期中須要注意的事項

  • 不要指望frame會馬上變化。

  • 在重寫layoutSubviews()時須要很是當心。

約束

Auto Layout你的視圖層級裏全部視圖經過放置在它們裏面的約束來動態計算的它們的大小和位置。通常控件須要四個約束決定位置大小,若是定義了intrinsicContentSize的好比UILabel只須要兩個約束便可。

約束方程式

view1.attribute1 = mutiplier * view2.attribute2 + constant

redButton.left = 1.0 * yellowLabel.right + 10.0 //紅色按鈕的左側距離黃色label有10個point

使用API添加約束

使用NSLayoutConstraint類(最低支持iOS6)添加約束。NSLayoutConstraint官方參考:https://developer.apple.com/library/prerelease/ios/documentation/AppKit/Reference/NSLayoutConstraint_Class/index.html

[NSLayoutContraint constraintWithItem:view1
                                                 attribute:NSLayoutAttributeBottom
                                               relatedBy:NSLayoutRelationEqual
                                                    toItem:view2
                                                 attribute:NSLayoutAttributeBottom
                                                multiplier:1.0
                                                 constant:-5]

把約束用約束中兩個view的共同父視圖或者兩視圖中層次高視圖的- (void)addConstraint:(NSLayoutConstraint *)constraint方法將約束添加進去。

使用VFL語言添加約束

先舉個簡單的例子並排兩個view添加約束

[NSLayoutConstraint constraintWithVisualFormat:@「[view1]-[view2]"
                                                                  options:0
                                                                  metrics:nil
                                                                     views:viewsDictionary;

viewDictionary能夠經過NSDictionaryOfVariableBindings方法獲得

UIView *view1 = [[UIView alloc] init];
UIView *view2 = [[UIView alloc] init];
viewsDictionary = NSDictionaryOfVariableBindings(view1,view2);

options

能夠給這個位掩碼傳入NSLayoutFormatAlignAllTop使它們頂部對齊,這個值的默認值是NSLayoutFormatDirectionLeadingToTrailing從左到右。可使用NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom 表示兩個視圖的頂部和底部約束相同。

metrics

這個參數做用是替換VFL語句中對應的值

CGRect viewFrame = CGRectMake(50, 50, 100, 100);
NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2);
NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)),
                                           @"top": @(CGRectGetMinY(viewFrame)),
                                        @"width": @(CGRectGetWidth(viewFrame)),
                                       @"height": @(CGRectGetHeight(viewFrame))};
[view1 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];

使用NSDictionaryOfVariableBindings(...)快速建立

NSNumber *left = @50;
NSNumber *top = @50;
NSNumber *width = @100;
NSNumber *height = @100;

NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2);
NSDictionary *metrics = NSDictionaryOfVariableBindings(left, top, width, height);

[view1 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];

VFL幾個基本例子

  • [view1(50)]-10-[view2(100)] 表示view1寬50,view2寬100,間隔10

  • [view1(>=50@750)] 表示view1寬度大於50,約束條件優先級爲750(優先級越大優先執行該約束,最大1000)

  • V:view1 表示按照豎直排,上面是view1下面是一個和它同樣大的view2

  • H:|-[view1]-[view2]-[view3(>=20)]-| 表示按照水平排列,|表示父視圖,各個視圖之間按照默認寬度來排列

VFL介紹

不管使用哪一種方法建立約束都是NSLayoutConstraint類的成員,每一個約束都會在一個Objective-C對象中存儲y = mx + b規則,而後經過Auto Layout引擎來表達該規則,VFL也不例外。VFL由一個描述佈局的文字字符串組成,文本會指出間隔,不等量和優先級。官方對其的介紹:Visual Format Language https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage.html

VFL的語法

  • 標準間隔:[button]-[textField]

  • 寬約束:[button(>=50)]

  • 與父視圖的關係:|-50-[purpleBox]-50-|

  • 垂直佈局:V:[topField]-10-[bottomField]

  • Flush Views:maroonView

  • 權重:[button(100@20)]

  • 等寬:[button(==button2)]

  • Multiple Predicates:[flexibleButton(>=70,<=100)]

注意事項

建立這種字符串時須要注意一下幾點:

  • H:和V:每次都使用一個。

  • 視圖變量名出如今方括號中,例如[view]。

  • 字符串中順序是按照從頂到底,從左到右

  • 視圖間隔以數字常量出現,例如-10-。

  • |表示父視圖

使用Auto Layout時須要注意的點

  • 注意禁用Autoresizing Masks。對於每一個須要使用Auto Layout的視圖須要調用setTranslatesAutoresizingMaskIntoConstraints:NO

  • VFL語句裏不能包含空格和>,<這樣的約束

  • 佈局原理是由外向里布局,最早屏幕尺寸,再一層一層往裏決定各個元素大小。

  • 刪除視圖時直接使用removeConstraint和removeConstraints時須要注意這樣刪除是無法刪除視圖不支持的約束致使view中還包含着那個約束(使用第三方庫時須要特別注意下)。解決這個的辦法就是添加約束時用一個局部變量保存下,刪除時進行比較刪掉和先前那個,還有個辦法就是設置標記,constraint.identifier = @「What you want to call」。

佈局約束規則

表達佈局約束的規則可使用一些簡單的數學術語,以下表

類型 描述
屬性 視圖位置 NSLayoutAttributeLeft, NSLayoutAttributeRight, NSLayoutAttributeTop, NSLayoutAttributeBottom
屬性 視圖前面後面 NSLayoutAttributeLeading, NSLayoutAttributeTrailing
屬性 視圖的寬度和高度 NSLayoutAttributeWidth, NSLayoutAttributeHeight
屬性 視圖中心 NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
屬性 視圖的基線,在視圖底部上方放置文字的地方 NSLayoutAttributeBaseline
屬性 佔位符,在與另外一個約束的關係中沒有用到某個屬性時可使用佔位符 NSLayoutAttributeNotAnAttribute
關係 容許將屬性經過等式和不等式相互關聯 NSLayoutRelationLessThanOrEqual, NSLayoutRelationEqual, NSLayoutRelationGreaterThanOrEqual
數學運算 每一個約束的乘數和相加性常數 CGFloat值

約束層級

約束引用兩視圖時,這兩個視圖須要屬於同一個視圖層次結構,對於引用兩個視圖的約束只有兩個狀況是容許的。第一種是一個視圖是另外一個視圖的父視圖,第二個狀況是兩個視圖在一個窗口下有一個非nil的共同父視圖。

優先級

哪一個約束優先級高會先知足其約束,系統內置優先級枚舉值UILayoutPriority

enum {
    UILayoutPriorityRequired = 1000, //默認的優先級,意味着默認約束一旦衝突就會crash
    UILayoutPriorityDefaultHigh = 750,
    UILayoutPriorityDefaultLow = 250,
    UILayoutPriorityFittingSizeLevel = 50,
};
typedef float UILayoutPriority;

IntrinsicContentSize / Compression Resistance Priority / Hugging Priority

具備instrinsic content size的控件,好比UILabel,UIButton,選擇控件,進度條和分段等等,能夠本身計算本身的大小,好比label設置text和font後大小是能夠計算獲得的。這時能夠經過設置Hugging priority讓這些控件不要大於某個設定的值,默認優先級爲250。設置Content Compression Resistance就是讓控件不要小於某個設定的值,默認優先級爲750。加這些值能夠看成是加了個額外的約束值來約束寬。

佈局過程

updateConstraints -> layoutSubViews -> drawRect

viewDidLayoutSubviews,-layoutSubviews

使用Auto Layout的view會在viewDidLayoutSubviews或-layoutSubview調用super轉換成具備正確顯示的frame值。

View的改變會調用哪些方法

  • 改變frame.origin不會掉用layoutSubviews

  • 改變frame.size會使 superVIew的layoutSubviews調用

  • 改變bounds.origin和bounds.size都會調用superView和本身view的layoutSubviews方法

Auto Layout的Debug

Auto Layout如下幾種狀況會出錯

  • Unsatisfiable Layouts:約束衝突,同一時刻約束無法同時知足。系統發現時會先檢測那些衝突的約束,而後會一直拆掉衝突的約束再檢查佈局直到找到合適的佈局,最後日誌會將衝突的約束和拆掉的約束打印在控制檯上。

  • Ambiguous Layouts:約束有缺失,好比說位置或者大小沒有全指定到。還有種狀況就是兩個衝突的約束的權重是同樣的就會崩。

  • Logical Errors:佈局中的邏輯錯誤。

  • 不含視圖項的約束不合法,每一個約束至少須要引用一個視圖,否則會崩。在刪除視圖時必定要注意。

Debugger

  • po [[UIWindow keyWindow] _autolayoutTrace]

參考

參考官方文檔:https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/TypesofErrors.html

容易出問題的Bug Case

  • 無共同父視圖的視圖之間相互添加約束會有問題。

  • 調用了setNeedsLayout後不能經過frame改變視圖和控件

  • 爲了讓在設置了setTranslatesAutoresizingMaskIntoConstraints:NO視圖裏更改的frame馬上生效而執行了沒有標記馬上刷新的layoutIfNeeded的方式是不可取的。

實踐中碰到的非必現低配置機器崩潰bug分析

案例一

一個視圖缺乏高寬約束,在設置完了約束後執行layoutIfNeeded,而後設置寬高,這種狀況在低配機器上可能會出現崩問題。緣由在於layoutIfNeeded須要有標記纔會馬上調用layoutSubview獲得寬高,否則是不會立刻調用的。頁面第一次顯示是會自動標記上須要刷新這個標記的,因此第一次看顯示都是看不出問題的,但頁面再次調用layoutIfNeeded時是不會馬上執行layoutSubview的(但以前加上setNeedsLayout就會馬上執行),這時改變的寬高值會在上文生命週期中提到的Auto Layout Cycle中的Engine裏的Deferred Layout Pass裏執行layoutSubview,手動設置的layoutIfNeeded也會執行一遍layoutSubview,可是這個若是發生在Deferred Layout Pass以後就會出現崩的問題,由於當視圖設置爲setTranslatesAutoresizingMaskIntoConstraints:NO時會嚴格按照約束->Engine->顯示這種流程,如在Deferred Layout Pass以前設置好是沒有問題的,以後強制執行LayoutSubview會產生一個權重和先前同樣的約束在相似動畫block裏更新佈局讓Engine執行致使Ambiguous Layouts這種權重相同衝突崩潰的狀況發生。

案例二

將多個有相互約束關係視圖removeFromSuperView後更新佈局在低配機器上出現崩的問題。這個緣由主要是根據不含視圖項的約束不合法這個原則來的,同時會拋出野指針的錯誤。在內存吃緊機器上,當應用佔內存較多系統會抓住任何能夠釋放heap區內存的機會視圖被移除後會馬上被清空,這時約束若是尚未被釋就知足不含視圖項的約束會崩的狀況了。

推薦Auto Layout第三方庫

Masonry

Github地址:https://github.com/SnapKit/Masonry

Cartography

Github地址:https://github.com/robb/Cartography

Masonry

能夠參看我上篇文章《AutoLayout框架Masonry使用心得》:http://www.starming.com/index.php?v=index&view=81

各版本iOS中AutoLayout的區別

完整記錄能夠到官方網站進行覈對和查找:What’s New in iOS https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Introduction/Introduction.html

iOS6

蘋果在這個版本引入Auto Layout,具有了全部核心功能。

iOS7

  • NavigationBar,TabBar和ToolBar的translucent屬性默認爲YES,當前ViewController的高度是整個屏幕的高度,爲了確保不被這些Bar覆蓋能夠在佈局中使用topLayoutGuide和bottomLayoutGuide屬性。

[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-[view1]" options:0 metrics:nil views:view2];

iOS8

  • Self Sizing Cells http://www.appcoda.com/self-sizing-cells/

  • UIViewController新增兩個方法,用來處理UITraitEnvironment協議,UIKit裏有UIScreen,UIViewController,UIView和UIPresentationController支持這個協議,當視圖traitCollection改變時UIViewController時能夠捕獲到這個消息進行處理的。

- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
  • Size Class的出現UIViewController提供了一組新協議來支持UIContentContainer

- (void)systemLayoutFittingSizeDidChangeForChildContentContainer:(id )container NS_AVAILABLE_IOS(8_0);
- (CGSize)sizeForChildContentContainer:(id )container withParentContainerSize:(CGSize)parentSize NS_AVAILABLE_IOS(8_0);
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator NS_AVAILABLE_IOS(8_0);
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id )coordinator NS_AVAILABLE_IOS(8_0);
  • UIView的Margin新增了3個API,NSLayoutMargins能夠定義view之間的距離,這個只對Auto Layout有效,而且默認值爲{8,8,8,8}。NSLayoutAttribute的枚舉值也有相應的更新

//UIView的3個Margin相關API
@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);
@property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);
- (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);
//NSLayoutAttribute的枚舉值更新
NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),

iOS9

UIStackView

蘋果一直但願可以讓更多的人來用Auto Layout,除了弄出一個VFL如今又弄出一個不須要約束的方法,使用Stack view使你們使用Auto Layout時不用觸碰到約束,官方口號是「Start with Stack View, use constraints as needed」。 更多細節能夠查看官方介紹:UIKit Framework Reference UIStackView Class Referencehttps://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutWithoutConstraints.html

Stack Views :https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html

Stack View提供了更加簡便的自動佈局方法好比Alignment的Fill,Leading,Center,Trailing。Distribution的Fill,Fill Equally,Fill Proportionally,Equal Spacing。

若是但願在iOS9以前的系統也可以使用Stack view能夠用sunnyxx的FDStackViewhttps://github.com/forkingdog/FDStackView,利用運行時替換元素的方法來支持iOS6+系統。

NSLayoutAnchorAPI

新增這個API可以讓約束的聲明更加清晰,還可以經過靜態類型檢查確保約束的正常工做。具體能夠查看官方文檔https://developer.apple.com/library/ios/documentation/AppKit/Reference/NSLayoutAnchor_ClassReference/

NSLayoutConstraint *constraint = [view1.leadingAnchor constraintEqualToAnchor:view2.topAnchor];

參考

官方文檔

WWDC視頻

相關文章
相關標籤/搜索