VC的佈局時機、所用方法以及UIView內部佈局執行順序

Hsusue發表於2018-08-02

前言

聽說首圖能吸引人點進來

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定義完Masonrybolck後呼叫一下[self.view layoutIfNeeded],就能馬上獲取到期望值。 猜測應該是VC呼叫了某些佈局方法。 並且這些方法在viewWillAppearDidAppear之間也呼叫了。 現在就來分析為什麼會這樣。

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:收到記憶體警告
複製程式碼

呼叫順序

可以看到和佈局有關的兩個方法確實夾在viewWillAppearviewDidAppear之間,且有可能會呼叫多次。

// 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。

frame生成的過程

結論:

viewDidLoad定義完Masonryblock後,(從上圖可以看出過了少於0.1秒的時間內)兩佈局方法就呼叫完成frame也被算出來並在畫面上描繪好view了。 如果定義完後直接呼叫[self.view layoutIfNeeded],不用等到下一個cycle,VC會在該函式內馬上同步呼叫viewWillLayoutSubviewsviewDidLayoutSubviews各一次,這時候frame就是期望值了。


以上弄清楚Masonry不能立即獲取frame原因了。但都是分析VCUIView佈局方式。那麼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畫自定義的內容,就要實現該方法,否則避免覆蓋該方法。
複製程式碼

分享一下我對這些方法的理解,應該對理解後面過程有幫助。如果有錯誤的地方,歡迎指出來。

  • 整體分成了 怎麼執行需要執行的標記馬上執行佈局
  • 怎麼執行的三類方法layoutSubviewsdrawupdateConstraints只應該被過載,絕不要在程式碼中顯式地呼叫,系統會在需要的時候自動呼叫。 舉個例子
某個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是父控制元件對子控制元件的。會遞迴呼叫子控制元件的layoutSubviewsdisplay先渲染父控制元件,再渲染子控制元件。
  • 佈局執行在update cycle中,一般不卡的話,1/60s就會更新一遍。
  • view的以上三個執行標記發生改變,要等到下一次update cycle後,VC才會呼叫佈局方法計算好frame並渲染到螢幕上。
  • updateConstraintsIfNeededlayoutIfNeeded這兩個馬上執行方法是給我們呼叫的,告訴系統不用等到下一個update cycle,VC馬上執行佈局方法。
  • viewinit後呼叫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);
    }];
}
複製程式碼

UIView內部執行順序

可以看到init後把三個標記都置為YES。 然後在VC的佈局方式中,viewWillLayoutSubviews中會呼叫updateConstraints,在viewDidLayoutSubviews會呼叫layoutSubviewsdraw。所以說不要顯示呼叫 怎麼執行 這三個方法。

  • 情形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);
    }];
}
複製程式碼

view加view的執行順序
可以看到updateConstraints是子到父。layoutSubviewsdrawRect是父到子。

最後一張圖總結UIView內部佈局執行順序與VC的互動

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。
複製程式碼

參考

相關文章