變更記錄
序號 | 錄入時間 | 備注
--- | --- | --- | ---
1 | 2018-04-14 | 新建文章
2 | 2018-05-28 | 整理目錄,完善標題
UIView的
setNeedsLayout,layoutIfNeeded和layoutSubviews方法之間的關系解釋
iOS layout機制相關方法
- (CGSize)sizeThatFits:(CGSize)size
- (void)sizeToFit
- (void)layoutSubviews
- (void)layoutIfNeeded
- (void)setNeedsLayout
- (void)setNeedsDisplay
- (void)drawRect
layoutSubviews在以下情況下會被調用:
init初始化不會觸發(fā)layoutSubviews
但是是用initWithFrame 進行初始化時,當rect的值不為CGRectZero時,也會觸發(fā)——就是改變了frameaddSubview會觸發(fā)layoutSubviews
設置view的Frame會觸發(fā)layoutSubviews,當然前提是frame的值設置前后發(fā)生了變化
滾動一個UIScrollView會觸發(fā)layoutSubviews
旋轉Screen會觸發(fā)父UIView上的layoutSubviews事件
改變一個UIView大小的時候也會觸發(fā)父UIView上的layoutSubviews事件
init does not cause layoutSubviews to be called (duh)
addSubview: causes layoutSubviews to be called on the view being added, the view it’s being added to (target view), and all the subviews of the target
view setFrame intelligently calls layoutSubviews on the view having its frame set only if the size parameter of the frame is different
scrolling a UIScrollView causes layoutSubviews to be called on the scrollView, and its superview
rotating a device only calls layoutSubview on the parent view (the responding viewControllers primary view)
Resizing a view will call layoutSubviews on its superview
在蘋果的官方文檔中強調:
You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.
layoutSubviews, 當我們在某個類的內部調整子視圖位置時,需要調用。
反過來的意思就是說:如果你想要在外部設置subviews的位置,就不要重寫。
刷新子對象布局
- layoutSubviews方法:這個方法,默認沒有做任何事情,需要子類進行重寫
- setNeedsLayout方法: 標記為需要重新布局,異步調用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定會被調用
- layoutIfNeeded方法:如果,有需要刷新的標記,立即調用layoutSubviews進行布局(如果沒有標記,不會調用layoutSubviews)
如果要立即刷新,要先調用[view setNeedsLayout],把標記設為需要布局,然后馬上調用[view layoutIfNeeded],實現(xiàn)布局
在視圖第一次顯示之前,標記總是“需要刷新”的,可以直接調用[view layoutIfNeeded].
重繪
- drawRect:(CGRect)rect方法:重寫此方法,執(zhí)行重繪任務
- setNeedsDisplay方法:標記為需要重繪,異步調用drawRect
- setNeedsDisplayInRect:(CGRect)invalidRect方法:標記為需要局部重繪
sizeToFit會自動調用sizeThatFits方法;
sizeToFit不應該在子類中被重寫,應該重寫sizeThatFits
sizeThatFits傳入的參數(shù)是receiver當前的size,返回一個適合的size
sizeToFit可以被手動直接調用
sizeToFit和sizeThatFits方法都沒有遞歸,對subviews也不負責,只負責自己
———————————-
layoutSubviews對subviews重新布局
layoutSubviews方法調用先于drawRect
setNeedsLayout在receiver標上一個需要被重新布局的標記,在系統(tǒng)runloop的下一個周期自動調用layoutSubviews
layoutIfNeeded方法如其名,UIKit會判斷該receiver是否需要layout.根據(jù)Apple官方文檔,layoutIfNeeded方法應該是這樣的
layoutIfNeeded遍歷的不是superview鏈,應該是subviews鏈
drawRect是對receiver的重繪,能獲得context
setNeedDisplay在receiver標上一個需要被重新繪圖的標記,在下一個draw周期自動重繪,iphone device的刷新頻率是60hz,也就是1/60秒后重繪
最近在學習swift做動畫,用到constraint的動畫,用到layoutIfNeeded就去研究了下UIView的這幾個布局的方法。
下面是做得一個動畫,下載地址:AnimationDemo3
下面列舉下iOS layout的相關方法:
- layoutSubviews
- layoutIfNeeded
- setNeedsLayout
- setNeedsDisplay
- drawRect
- sizeThatFits
- sizeToFit
大概常用的上面幾個 , 具體的應該還有別的。
layoutSubviews
這個方法,默認沒有做任何事情,需要子類進行重寫 。 系統(tǒng)在很多時候會去調用這個方法:
- 初始化不會觸發(fā)layoutSubviews,但是如果設置了不為CGRectZero的frame的時候就會觸發(fā)。
- addSubview會觸發(fā)layoutSubviews
- 設置view的Frame會觸發(fā)layoutSubviews,當然前提是frame的值設置前后發(fā)生了變化
- 滾動一個UIScrollView會觸發(fā)layoutSubviews
- 旋轉Screen會觸發(fā)父UIView上的layoutSubviews事件
- 改變一個UIView大小的時候也會觸發(fā)父UIView上的layoutSubviews事件
在蘋果的官方文檔中強調: You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.layoutSubviews, 當我們在某個類的內部調整子視圖位置時,需要調用。反過來的意思就是說:如果你想要在外部設置subviews的位置,就不要重寫。
setNeedsLayout
標記為需要重新布局,不立即刷新,但layoutSubviews一定會被調用,配合layoutIfNeeded立即更新
layoutIfNeeded
如果,有需要刷新的標記,立即調用layoutSubviews進行布局
這個動畫中有用到 舉個栗子。
如圖 , 上面有個label ,中間有個按鈕 , label已經(jīng)被自動布局到左上角 。 然后我們那個left的constraint
@IBOutlet weak var leftContrain:NSLayoutConstraint!
在viewDidLoad中聲明好,然后在Main.storyboard中進行連線。點擊按鈕的時候 ,我們把左邊的距離改成100 。
在按鈕的點擊事件里加上這句。
leftContrain.constant = 100
然后我們想要一個動畫的效果。
如果這么做
UIView.animateWithDuration(0.8, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
self.leftContrain.constant = 100
}, completion: nil)
你會發(fā)現(xiàn)然并卵 。其實這句話self.leftContrain.constant = 100只是執(zhí)行了setNeedsLayout 標記了需要重新布局,但是沒有立即執(zhí)行。所以我們需要在動畫中調用這個方法layoutIfNeeded
所以代碼應該這么寫
leftContrain.constant = 100
UIView.animateWithDuration(0.8, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
self.view.layoutIfNeeded() //立即實現(xiàn)布局
}, completion: nil)
所以上面不管寫多少約束的改變,只需要在動畫里動用 一次self.view.layoutIfNeeded(),所有的都會已動畫的方式 。如果一些變化不想動畫 。在動畫前執(zhí)行self.view.layoutIfNeeded()
drawRect
這個方法是用來重繪的。
drawRect在以下情況下會被調用:
- 如果在UIView初始化時沒有設置rect大小,將直接導致drawRect不被自動調用。drawRect調用是在Controller->loadView, Controller->viewDidLoad 兩方法之后掉用的.所以不用擔心在控制器中,這些View的drawRect就開始畫了.這樣可以在控制器中設置一些值給View(如果這些View draw的時候需要用到某些變量值).
- 該方法在調用
sizeToFit后被調用,所以可以先調用sizeToFit計算出size。然后系統(tǒng)自動調用drawRect:方法。 - 通過設置
contentMode屬性值為UIViewContentModeRedraw。那么將在每次設置或更改frame的時候自動調用drawRect:。 - 直接調用
setNeedsDisplay,或者setNeedsDisplayInRect:觸發(fā)drawRect:,但是有個前提條件是rect不能為0。以上1,2推薦;而3,4不提倡
drawRect方法使用注意點:
- 若使用UIView繪圖,只能在drawRect:方法中獲取相應的contextRef并繪圖。如果在其他方法中獲取將獲取到一個invalidate的ref并且不能用于畫圖。drawRect:方法不能手動顯示調用,必須通過調用
setNeedsDisplay或者setNeedsDisplayInRect,讓系統(tǒng)自動調該方法。 - 若使用CALayer繪圖,只能在drawInContext: 中(類似于drawRect)繪制,或者在delegate中的相應方法繪制。同樣也是調用setNeedDisplay等間接調用以上方法
- 若要實時畫圖,不能使用
gestureRecognizer,只能使用touchbegan等方法來掉用setNeedsDisplay實時刷新屏幕
sizeToFit
- sizeToFit會自動調用sizeThatFits方法;
- sizeToFit不應該在子類中被重寫,應該重寫sizeThatFits
- sizeThatFits傳入的參數(shù)是receiver當前的size,返回一個適合的size
- sizeToFit可以被手動直接調用sizeToFit和sizeThatFits方法都沒有遞歸,對subviews也不負責,只負責自己
推薦拓展閱讀
ConvertRect
fromView
CGRect newRect = [self.view convertRect:self.blueView.frame fromView:self.redView];
這段代碼的意思算出在紅色控件里的藍色控件在控制器view中的位置(其實就是算x和y的值,因為寬高不變)
toView
CGRect newRect = [self.blueView convertRect:CGRectMake(50, 50, 100, 100) toView:self.greenView];
調用視圖 convertRect: 調用視圖相對于目標視圖的frame toview目標視圖
目標視圖為nil的時候指的是Window本身。
Runloop與UIView的繪制
也許要先從Runloop開始說,iOS的mainRunloop是一個60fps的回調,也就是說每16.7ms會繪制一次屏幕,這個時間段內要完成view的緩沖區(qū)創(chuàng)建,view內容的繪制(如果重寫了drawRect),這些CPU的工作。然后將這個緩沖區(qū)交給GPU渲染,這個過程又包括多個view的拼接(compositing),紋理的渲染(Texture)等,最終顯示在屏幕上。因此,如果在16.7ms內完不成這些操作,比如,CPU做了太多的工作,或者view層次過于多,圖片過于大,導致GPU壓力太大,就會導致“卡”的現(xiàn)象,也就是丟幀。
蘋果官方給出的最佳幀率是:60fps,也就是1幀不丟,當然這是理想中的絕佳的體驗。
這個60fps改怎么理解呢?一般來說如果幀率達到25+fps,人眼就基本感覺不到停頓了,因此,如果你能讓你ios程序穩(wěn)定的保持在30fps已經(jīng)很不錯了,注意,是“穩(wěn)定”在30fps,而不是,10fps,40fps,20fps這樣的跳動,如果幀頻不穩(wěn)就會有卡的感覺。60fps真的很難達到,尤其在iphone4,4s上。
總的來說,UIView從繪制到Render的過程有如下幾步:
每一個UIView都有一個layer,每一個layer都有個content,這個content指向的是一塊緩存,叫做backing store。
UIView的繪制和渲染是兩個過程,當UIView被繪制時,CPU執(zhí)行drawRect,通過context將數(shù)據(jù)寫入backing store
當backing store寫完后,通過render server交給GPU去渲染,將backing store中的bitmap數(shù)據(jù)顯示在屏幕上
上面提到的從CPU到GPU的過程可用下圖表示:

下面具體來討論下這個過程
CPU bound:
假設我們創(chuàng)建一個UILabel:
UILabel* label = [[UILabel alloc]initWithFrame:CGRectMake(10, 50, 300, 14)];
label.backgroundColor = [UIColor whiteColor];
label.font = [UIFont systemFontOfSize:14.0f];
label.text = @"test";
[self.view addSubview:label];
這個時候不會發(fā)生任何操作,由于UILabel重寫了drawRect,因此,這個view會被marked as “dirty”:
類似這個樣子:

然后一個新的Runloop到來,上面說道在這個Runloop中需要將界面渲染上去,對于UIKit的渲染,Apple用的是它的Core Animation。
做法是在Runloop開始的時候調用:
[CATransaction begin]
在Runloop結束的時候調用
[CATransaction commit]
在begin和commit之間做的事情是將view增加到view hierarchy中,這個時候也不會發(fā)生任何繪制的操作。
當[CATransaction commit]執(zhí)行完后,CPU開始繪制這個view:
首先CPU會為layer分配一塊內存用來繪制bitmap,叫做backing store
創(chuàng)建指向這塊bitmap緩沖區(qū)的指針,叫做CGContextRef
通過Core Graphic的api,也叫Quartz2D,繪制bitmap
將layer的content指向生成的bitmap
清空dirty flag標記
這樣CPU的繪制基本上就完成了。
通過time profiler 可以完整的看到個過程:
Running Time Self Symbol Name
2.0ms 1.2% 0.0 +[CATransaction flush]
2.0ms 1.2% 0.0 CA::Transaction::commit()
2.0ms 1.2% 0.0 CA::Context::commit_transaction(CA::Transaction*)
1.0ms 0.6% 0.0 CA::Layer::layout_and_display_if_needed(CA::Transaction*)
1.0ms 0.6% 0.0 CA::Layer::display_if_needed(CA::Transaction*)
1.0ms 0.6% 0.0 -[CALayer display]
1.0ms 0.6% 0.0 CA::Layer::display()
1.0ms 0.6% 0.0 -[CALayer _display]
1.0ms 0.6% 0.0 CA::Layer::display_()
1.0ms 0.6% 0.0 CABackingStoreUpdate_
1.0ms 0.6% 0.0 backing_callback(CGContext*, void*)
1.0ms 0.6% 0.0 -[CALayer drawInContext:]
1.0ms 0.6% 0.0 -[UIView(CALayerDelegate) drawLayer:inContext:]
1.0ms 0.6% 0.0 -[UILabel drawRect:]
1.0ms 0.6% 0.0 -[UILabel drawTextInRect:]
假如某個時刻修改了label的text:
label.text = @"hello world";
由于內容變了,layer的content的bitmap的尺寸也要變化,因此這個時候當新的Runloop到來時,CPU要為layer重新創(chuàng)建一個backing store,重新繪制bitmap。
CPU這一塊最耗時的地方往往在Core Graphic的繪制上,關于Core Graphic的性能優(yōu)化是另一個話題了,又會牽扯到很多東西,就不在這里討論了。
GPU bound:
CPU完成了它的任務:將view變成了bitmap,然后就是GPU的工作了,GPU處理的單位是Texture。
基本上我們控制GPU都是通過OpenGL來完成的,但是從bitmap到Texture之間需要一座橋梁,Core Animation正好充當了這個角色:
Core Animation對OpenGL的api有一層封裝,當我們的要渲染的layer已經(jīng)有了bitmap content的時候,這個content一般來說是一個CGImageRef,CoreAnimation會創(chuàng)建一個OpenGL的Texture并將CGImageRef(bitmap)和這個Texture綁定,通過TextureID來標識。
這個對應關系建立起來之后,剩下的任務就是GPU如何將Texture渲染到屏幕上了。
GPU大致的工作模式如下:

整個過程也就是一件事:CPU將準備好的bitmap放到RAM里,GPU去搬這快內存到VRAM中處理。
而這個過程GPU所能承受的極限大概在16.7ms完成一幀的處理,所以最開始提到的60fps其實就是GPU能處理的最高頻率。
因此,GPU的挑戰(zhàn)有兩個:
將數(shù)據(jù)從RAM搬到VRAM中
將Texture渲染到屏幕上
這兩個中瓶頸基本在第二點上。渲染Texture基本要處理這么幾個問題:
Compositing:
Compositing是指將多個紋理拼到一起的過程,對應UIKit,是指處理多個view合到一起的情況,如
[self.view addsubview : subview]。
如果view之間沒有疊加,那么GPU只需要做普通渲染即可。 如果多個view之間有疊加部分,GPU需要做blending。
加入兩個view大小相同,一個疊加在另一個上面,那么計算公式如下:
R = S+D*(1-Sa)
R: 為最終的像素值
S: 代表 上面的Texture(Top Texture)
D: 代表下面的Texture(lower Texture)
其中S,D都已經(jīng)pre-multiplied各自的alpha值。
Sa代表Texture的alpha值。
假如Top Texture(上層view)的alpha值為1,即不透明。那么它會遮住下層的Texture。即,R = S。是合理的。 假如Top Texture(上層view)的alpha值為0.5,S 為 (1,0,0),乘以alpha后為(0.5,0,0)。D為(0,0,1)。 得到的R為(0.5,0,0.5)。
基本上每個像素點都需要這么計算一次。
因此,view的層級很復雜,或者view都是半透明的(alpha值不為1)都會帶來GPU額外的計算工作。
Size
這個問題,主要是處理image帶來的,假如內存里有一張400x400的圖片,要放到100x100的imageview里,如果不做任何處理,直接丟進去,問題就大了,這意味著,GPU需要對大圖進行縮放到小的區(qū)域顯示,需要做像素點的sampling,這種smapling的代價很高,又需要兼顧pixel alignment。計算量會飆升。
Offscreen Rendering And Mask
如果我們對layer做這樣的操作:
label.layer.cornerRadius = 5.0f;
label.layer.masksToBounds = YES;
會產生offscreen rendering,它帶來的最大的問題是,當渲染這樣的layer的時候,需要額外開辟內存,繪制好radius,mask,然后再將繪制好的bitmap重新賦值給layer。
因此繼續(xù)性能的考慮,Quartz提供了優(yōu)化的api:
label.layer.cornerRadius = 5.0f;
label.layer.masksToBounds = YES;
label.layer.shouldRasterize = YES;
label.layer.rasterizationScale = label.layer.contentsScale;
簡單的說,這是一種cache機制。
同樣GPU的性能也可以通過instrument去衡量:
紅色代表GPU需要做額外的工作來渲染View,綠色代表GPU無需做額外的工作來處理bitmap。
That’s all
layoutSubviews調用總結
- 自身的frame發(fā)生變化, 會重新布局
layoutSubviews - 添加視圖,調用
addSubView的時候 - 滾動一個UIScrollView會觸發(fā)
- 子視圖frame發(fā)生變化,會調用父視圖的
addSubView
Its own bounds (not frame) changed.
The bounds of one of its direct subviews changed.
A subview is added to the view or removed from the view.
- init does not cause layoutSubviews to be called (duh)
- addSubview causes layoutSubviews to be called on the view being added, the view it’s being added to (target view), and all the subviews of the target view
- setFrame intelligently calls layoutSubviews on the view having it’s frame set only if the size parameter of the frame is different
- scrolling a UIScrollView causes layoutSubviews to be called on the scrollView, and it’s superview
- rotating a device only calls layoutSubview on the parent view (the responding viewControllers primary view)
- removeFromSuperview – layoutSubviews is called on superview only (not show in table)