前言
用Masonry
時,剛設定完佈局後想使用frame
乾點壞事,發現並不是期望的值。
- (void)viewDidLoad {
self.btn = [[UIButton alloc] init];
[self.view addSubview:self.btn];
[self.btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
NSLog(@"%@",self.btn);// 輸出frame = (0 0; 0 0)
// [self.view layoutIfNeeded];
}
複製程式碼
然後試了在viewWillAppear
首次出現介面還是沒獲取到期望值,
在viewDidAppear
才獲取到期望值。
解決方法:在viewDidLoad
定義完Masonry
的bolck
後呼叫一下[self.view layoutIfNeeded]
,就能馬上獲取到期望值。
猜測應該是VC
呼叫了某些佈局方法。
並且這些方法在viewWillAppear
和DidAppear
之間也呼叫了。
現在就來分析為什麼會這樣。
controller對view的佈局時機和所用方法
VC的生命週期
這個執行順序挺容易理解的。
alloc:建立物件,分配空間
init (xib和非xib用initWithNibName、stroyBoard用initWithCoder) :初始化物件,初始化資料
awakeFromNib:(若控制器有關聯xib才呼叫這方法)
loadView:優先從nib載入控制器檢視 ,其次程式碼
viewDidLoad:載入完成,可以進行自定義資料以及動態建立其他控制元件。
viewWillAppear:檢視將出現在螢幕之前,馬上這個檢視就會被展現在螢幕上了
viewWillLayoutSubviews:控制器的view將要佈局子控制元件
viewDidLayoutSubviews:控制器的view佈局子控制元件完成
//這期間系統可能會多次呼叫viewWillLayoutSubviews 、 viewDidLayoutSubviews 倆個方法
viewDidAppear:檢視已在螢幕上渲染完成
viewWillDisappear:檢視將被從螢幕上移除之前執行
viewDidDisappear:檢視已經被從螢幕上移除,使用者看不到這個檢視了
dealloc:檢視被銷燬,此處需要對你在init和viewDidLoad中建立的物件進行釋放
didReceiveMemoryWarning:收到記憶體警告
複製程式碼
可以看到和佈局有關的兩個方法確實夾在viewWillAppear
和viewDidAppear
之間,且有可能會呼叫多次。
// layoutSubviews呼叫時機
1. 當view被新增到另一個view上使用
2. 佈局自己子控制元件時使用
3. 螢幕打橫時
4. 當自己的frame發生變化時,例如手動修改、熱點等。
複製程式碼
(實測的時候發現若在viewDidLoad
中,self.view加了(無論多少個)子控制元件VC會分別呼叫兩次這些方法,沒新增子控制元件分別呼叫一次)。原因是layoutSubviews的呼叫時機。被新增到另一個view時呼叫一次;如果加了子控制元件,佈局子控制元件也會呼叫一次。
接下來分析[self.view layoutIfNeeded]為什麼能實現立即重新整理frame
首先,Masonry
是建立在autolayout
之上的,最終轉化為frame
。
一開始讓我驚訝的是,Masonry
約束的bolck
會馬上執行。但frame
不能立即獲取。原因是RunLoop下一個迴圈到來才會重新整理UI。
結論:
viewDidLoad
定義完Masonry
的block
後,(從上圖可以看出過了少於0.1秒的時間內)兩佈局方法就呼叫完成,frame
也被算出來並在畫面上描繪好view
了。
如果定義完後直接呼叫[self.view layoutIfNeeded]
,不用等到下一個cycle,VC會在該函式內馬上同步呼叫viewWillLayoutSubviews
和viewDidLayoutSubviews
各一次,這時候frame
就是期望值了。
以上弄清楚Masonry
不能立即獲取frame
的原因了。但都是分析VC
對UIView
的佈局方式。那麼view
中實現內部佈局又是怎麼個程式執行順序呢?
UIView內部佈局執行順序
佈局相關方法
- 可以分為三塊
updateConstraints
<-->layout
-->display
前兩個與佈局有關,第三個與渲染有關。
// 三塊的主要方法
#pragma mark - updateConstraints
//看上去好像set和get方法,但是set方法並無引數,呼叫就會標記為YES。
//init後呼叫get方法發現是YES。
setNeedsUpdateConstraints:標記需要updateConstraints。
needsUpdateConstraints:返回是否需要updateConstraints。
updateConstraintsIfNeeded:若需要,馬上updateConstraints。
updateConstraints:更新約束,自定義view應該重寫此方法在其中建立constraints. 注意:要在最後呼叫[super updateConstraints]
#pragma mark - layout
layoutIfNeeded:使用此方法強制立即進行layout,從當前view開始,此方法會遍歷整個view層次(包括superviews)請求layout。因此,呼叫此方法會強制整個view層次佈局。
setNeedsLayout:此方法會將view當前的layout設定為無效的,並在下一個upadte cycle裡去觸發layout更新。
layoutSubviews:如果你需要更精確控制子view,而不是使用限制或autoresizing行為,就需要實現該方法。
#pragma mark - display
setNeedsDisplay:標記整個檢視的邊界矩形需要重繪.
drawRect:如果你的View畫自定義的內容,就要實現該方法,否則避免覆蓋該方法。
複製程式碼
分享一下我對這些方法的理解,應該對理解後面過程有幫助。如果有錯誤的地方,歡迎指出來。
- 整體分成了 怎麼執行 、 需要執行的標記 和 馬上執行佈局。
- 怎麼執行的三類方法
layoutSubviews
、draw
和updateConstraints
只應該被過載,絕不要在程式碼中顯式地呼叫,系統會在需要的時候自動呼叫。 舉個例子
某個view.m
- (void)updateConstraints {
[self.sourceCollectionView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
//super必須寫在最後
[super updateConstraints];
}
- (void)setModelArray:(NSArray *)modelArray{
CGRect newFrame = self.frame;
newFrame.size.height = modelArray.count * 35;
self.frame = newFrame;
[self updateConstraintsIfNeeded];
}
複製程式碼
updateConstraints
裡寫好了view
內部某個Collectionview
的佈局。當傳入模型後,view
的高度改變,呼叫updateConstraintsIfNeeded
或者setNeedsUpdateConstraints
,而不要顯示呼叫updateConstraints
,在VC呼叫佈局方法時自然會跑這個方法。
- 很多情況下系統都會把view的需要執行標記置為YES。
updateConstraints
是子控制元件對父控制元件的。layoutSubviews
是父控制元件對子控制元件的。會遞迴呼叫子控制元件的layoutSubviews
。display
先渲染父控制元件,再渲染子控制元件。- 佈局執行在
update cycle
中,一般不卡的話,1/60s就會更新一遍。 view
的以上三個執行標記發生改變,要等到下一次update cycle
後,VC才會呼叫佈局方法計算好frame並渲染到螢幕上。updateConstraintsIfNeeded
、layoutIfNeeded
這兩個馬上執行方法是給我們呼叫的,告訴系統不用等到下一個update cycle
,VC馬上執行佈局方法。view
的init
後呼叫needsUpdateConstraints
返回YES
。而暴露的set方法只能標記為YES
,作用應該是告訴系統下一次cycle
要更新約束。猜測底層佈局好後會有別的set方法置為NO。
分析執行順序
- 情形1 建立HSUTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.testView = [[HSUTestView alloc] init];
self.testView.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.testView];
[self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
}
複製程式碼
可以看到init
後把三個標記都置為YES
。
然後在VC的佈局方式中,viewWillLayoutSubviews
中會呼叫updateConstraints
,在viewDidLayoutSubviews
會呼叫layoutSubviews
,draw
。所以說不要顯示呼叫 怎麼執行 這三個方法。
- 情形2 建立HSUContentView 然後add HSUTestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
HSUContentView *contentView = [[HSUContentView alloc] init];
[self.view addSubview:contentView];
[contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];
self.testView = [[HSUTestView alloc] init];
self.testView.backgroundColor = [UIColor blueColor];
[contentView addSubview:self.testView];
[self.testView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(contentView);
}];
}
複製程式碼
可以看到updateConstraints
是子到父。layoutSubviews
和drawRect
是父到子。
最後一張圖總結UIView內部佈局執行順序與VC的互動
補充 以下情形會呼叫layoutSubviews
1、init初始化不會觸發layoutSubviews
但是是用initWithFrame 進行初始化時,當rect的值不為CGRectZero時,也會觸發
2、addSubview會觸發layoutSubviews
3、設定view的Frame會觸發layoutSubviews,當然前提是frame的值設定前後發生了變化
4、滾動一個UIScrollView會觸發layoutSubviews
5、旋轉Screen會觸發父UIView上的layoutSubviews事件
6、改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件
7、直接呼叫setLayoutSubviews。
8、直接呼叫setNeedsLayout。
複製程式碼