變更記錄
序號(hào) | 錄入時(shí)間 | 備注
--- | --- | --- | ---
1 | 2018-04-14 | 新建文章
2 | 2018-05-28 | 整理目錄,完善標(biāo)題
UIView的
setNeedsLayout
,layoutIfNeeded
和layoutSubviews
方法之間的關(guān)系解釋
iOS layout機(jī)制相關(guān)方法
- (CGSize)sizeThatFits:(CGSize)size
- (void)sizeToFit
- (void)layoutSubviews
- (void)layoutIfNeeded
- (void)setNeedsLayout
- (void)setNeedsDisplay
- (void)drawRect
layoutSubviews
在以下情況下會(huì)被調(diào)用:
init初始化不會(huì)觸發(fā)layoutSubviews
但是是用initWithFrame 進(jìn)行初始化時(shí),當(dāng)rect的值不為CGRectZero時(shí),也會(huì)觸發(fā)——就是改變了frameaddSubview會(huì)觸發(fā)layoutSubviews
設(shè)置view的Frame會(huì)觸發(fā)layoutSubviews,當(dāng)然前提是frame的值設(shè)置前后發(fā)生了變化
滾動(dòng)一個(gè)UIScrollView會(huì)觸發(fā)layoutSubviews
旋轉(zhuǎn)Screen會(huì)觸發(fā)父UIView上的layoutSubviews事件
改變一個(gè)UIView大小的時(shí)候也會(huì)觸發(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
在蘋果的官方文檔中強(qiáng)調(diào):
You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.
layoutSubviews, 當(dāng)我們?cè)谀硞€(gè)類的內(nèi)部調(diào)整子視圖位置時(shí),需要調(diào)用。
反過(guò)來(lái)的意思就是說(shuō):如果你想要在外部設(shè)置subviews的位置,就不要重寫。
刷新子對(duì)象布局
- layoutSubviews方法:這個(gè)方法,默認(rèn)沒(méi)有做任何事情,需要子類進(jìn)行重寫
- setNeedsLayout方法: 標(biāo)記為需要重新布局,異步調(diào)用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定會(huì)被調(diào)用
- layoutIfNeeded方法:如果,有需要刷新的標(biāo)記,立即調(diào)用layoutSubviews進(jìn)行布局(如果沒(méi)有標(biāo)記,不會(huì)調(diào)用layoutSubviews)
如果要立即刷新,要先調(diào)用[view setNeedsLayout]
,把標(biāo)記設(shè)為需要布局,然后馬上調(diào)用[view layoutIfNeeded]
,實(shí)現(xiàn)布局
在視圖第一次顯示之前,標(biāo)記總是“需要刷新”的,可以直接調(diào)用[view layoutIfNeeded]
.
重繪
- drawRect:(CGRect)rect方法:重寫此方法,執(zhí)行重繪任務(wù)
- setNeedsDisplay方法:標(biāo)記為需要重繪,異步調(diào)用drawRect
- setNeedsDisplayInRect:(CGRect)invalidRect方法:標(biāo)記為需要局部重繪
sizeToFit
會(huì)自動(dòng)調(diào)用sizeThatFits
方法;
sizeToFit
不應(yīng)該在子類中被重寫,應(yīng)該重寫sizeThatFits
sizeThatFits
傳入的參數(shù)是receiver當(dāng)前的size,返回一個(gè)適合的size
sizeToFit
可以被手動(dòng)直接調(diào)用
sizeToFit
和sizeThatFits
方法都沒(méi)有遞歸,對(duì)subviews也不負(fù)責(zé),只負(fù)責(zé)自己
———————————-
layoutSubviews
對(duì)subviews重新布局
layoutSubviews
方法調(diào)用先于drawRect
setNeedsLayout
在receiver標(biāo)上一個(gè)需要被重新布局的標(biāo)記,在系統(tǒng)runloop的下一個(gè)周期自動(dòng)調(diào)用layoutSubviews
layoutIfNeeded
方法如其名,UIKit會(huì)判斷該receiver是否需要layout.根據(jù)Apple官方文檔,layoutIfNeeded方法應(yīng)該是這樣的
layoutIfNeeded
遍歷的不是superview鏈,應(yīng)該是subviews鏈
drawRect是對(duì)receiver的重繪,能獲得context
setNeedDisplay在receiver標(biāo)上一個(gè)需要被重新繪圖的標(biāo)記,在下一個(gè)draw周期自動(dòng)重繪,iphone device的刷新頻率是60hz,也就是1/60秒后重繪
最近在學(xué)習(xí)swift做動(dòng)畫,用到constraint的動(dòng)畫,用到layoutIfNeeded就去研究了下UIView的這幾個(gè)布局的方法。
下面是做得一個(gè)動(dòng)畫,下載地址:AnimationDemo3
下面列舉下iOS layout的相關(guān)方法:
- layoutSubviews
- layoutIfNeeded
- setNeedsLayout
- setNeedsDisplay
- drawRect
- sizeThatFits
- sizeToFit
大概常用的上面幾個(gè) , 具體的應(yīng)該還有別的。
layoutSubviews
這個(gè)方法,默認(rèn)沒(méi)有做任何事情,需要子類進(jìn)行重寫 。 系統(tǒng)在很多時(shí)候會(huì)去調(diào)用這個(gè)方法:
- 初始化不會(huì)觸發(fā)layoutSubviews,但是如果設(shè)置了不為CGRectZero的frame的時(shí)候就會(huì)觸發(fā)。
- addSubview會(huì)觸發(fā)layoutSubviews
- 設(shè)置view的Frame會(huì)觸發(fā)layoutSubviews,當(dāng)然前提是frame的值設(shè)置前后發(fā)生了變化
- 滾動(dòng)一個(gè)UIScrollView會(huì)觸發(fā)layoutSubviews
- 旋轉(zhuǎn)Screen會(huì)觸發(fā)父UIView上的layoutSubviews事件
- 改變一個(gè)UIView大小的時(shí)候也會(huì)觸發(fā)父UIView上的layoutSubviews事件
在蘋果的官方文檔中強(qiáng)調(diào): You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.layoutSubviews, 當(dāng)我們?cè)谀硞€(gè)類的內(nèi)部調(diào)整子視圖位置時(shí),需要調(diào)用。反過(guò)來(lái)的意思就是說(shuō):如果你想要在外部設(shè)置subviews的位置,就不要重寫。
setNeedsLayout
標(biāo)記為需要重新布局,不立即刷新,但layoutSubviews
一定會(huì)被調(diào)用,配合layoutIfNeeded
立即更新
layoutIfNeeded
如果,有需要刷新的標(biāo)記,立即調(diào)用layoutSubviews
進(jìn)行布局
這個(gè)動(dòng)畫中有用到 舉個(gè)栗子。
如圖 , 上面有個(gè)label ,中間有個(gè)按鈕 , label已經(jīng)被自動(dòng)布局到左上角 。 然后我們那個(gè)left的constraint
@IBOutlet weak var leftContrain:NSLayoutConstraint!
在viewDidLoad中聲明好,然后在Main.storyboard中進(jìn)行連線。點(diǎn)擊按鈕的時(shí)候 ,我們把左邊的距離改成100 。
在按鈕的點(diǎn)擊事件里加上這句。
leftContrain.constant = 100
然后我們想要一個(gè)動(dòng)畫的效果。
如果這么做
UIView.animateWithDuration(0.8, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
self.leftContrain.constant = 100
}, completion: nil)
你會(huì)發(fā)現(xiàn)然并卵 。其實(shí)這句話self.leftContrain.constant = 100
只是執(zhí)行了setNeedsLayout
標(biāo)記了需要重新布局,但是沒(méi)有立即執(zhí)行。所以我們需要在動(dòng)畫中調(diào)用這個(gè)方法layoutIfNeeded
所以代碼應(yīng)該這么寫
leftContrain.constant = 100
UIView.animateWithDuration(0.8, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
self.view.layoutIfNeeded() //立即實(shí)現(xiàn)布局
}, completion: nil)
所以上面不管寫多少約束的改變,只需要在動(dòng)畫里動(dòng)用 一次self.view.layoutIfNeeded()
,所有的都會(huì)已動(dòng)畫的方式 。如果一些變化不想動(dòng)畫 。在動(dòng)畫前執(zhí)行self.view.layoutIfNeeded()
drawRect
這個(gè)方法是用來(lái)重繪的。
drawRect在以下情況下會(huì)被調(diào)用:
- 如果在UIView初始化時(shí)沒(méi)有設(shè)置rect大小,將直接導(dǎo)致drawRect不被自動(dòng)調(diào)用。drawRect調(diào)用是在Controller->loadView, Controller->viewDidLoad 兩方法之后掉用的.所以不用擔(dān)心在控制器中,這些View的drawRect就開(kāi)始畫了.這樣可以在控制器中設(shè)置一些值給View(如果這些View draw的時(shí)候需要用到某些變量值).
- 該方法在調(diào)用
sizeToFit
后被調(diào)用,所以可以先調(diào)用sizeToFit
計(jì)算出size。然后系統(tǒng)自動(dòng)調(diào)用drawRect:
方法。 - 通過(guò)設(shè)置
contentMode
屬性值為UIViewContentModeRedraw
。那么將在每次設(shè)置或更改frame的時(shí)候自動(dòng)調(diào)用drawRect:
。 - 直接調(diào)用
setNeedsDisplay
,或者setNeedsDisplayInRect:
觸發(fā)drawRect:
,但是有個(gè)前提條件是rect不能為0。以上1,2推薦;而3,4不提倡
drawRect方法使用注意點(diǎn):
- 若使用UIView繪圖,只能在drawRect:方法中獲取相應(yīng)的contextRef并繪圖。如果在其他方法中獲取將獲取到一個(gè)invalidate的ref并且不能用于畫圖。drawRect:方法不能手動(dòng)顯示調(diào)用,必須通過(guò)調(diào)用
setNeedsDisplay
或者setNeedsDisplayInRect
,讓系統(tǒng)自動(dòng)調(diào)該方法。 - 若使用CALayer繪圖,只能在drawInContext: 中(類似于drawRect)繪制,或者在delegate中的相應(yīng)方法繪制。同樣也是調(diào)用setNeedDisplay等間接調(diào)用以上方法
- 若要實(shí)時(shí)畫圖,不能使用
gestureRecognizer
,只能使用touchbegan
等方法來(lái)掉用setNeedsDisplay
實(shí)時(shí)刷新屏幕
sizeToFit
- sizeToFit會(huì)自動(dòng)調(diào)用sizeThatFits方法;
- sizeToFit不應(yīng)該在子類中被重寫,應(yīng)該重寫sizeThatFits
- sizeThatFits傳入的參數(shù)是receiver當(dāng)前的size,返回一個(gè)適合的size
- sizeToFit可以被手動(dòng)直接調(diào)用sizeToFit和sizeThatFits方法都沒(méi)有遞歸,對(duì)subviews也不負(fù)責(zé),只負(fù)責(zé)自己
推薦拓展閱讀
ConvertRect
fromView
CGRect newRect = [self.view convertRect:self.blueView.frame fromView:self.redView];
這段代碼的意思算出在紅色控件里的藍(lán)色控件在控制器view中的位置(其實(shí)就是算x和y的值,因?yàn)閷捀卟蛔儯?toView
CGRect newRect = [self.blueView convertRect:CGRectMake(50, 50, 100, 100) toView:self.greenView];
調(diào)用視圖 convertRect
: 調(diào)用視圖相對(duì)于目標(biāo)視圖的frame toview目標(biāo)視圖
目標(biāo)視圖為nil
的時(shí)候指的是Window本身。
Runloop與UIView的繪制
也許要先從Runloop開(kāi)始說(shuō),iOS的mainRunloop是一個(gè)60fps的回調(diào),也就是說(shuō)每16.7ms會(huì)繪制一次屏幕,這個(gè)時(shí)間段內(nèi)要完成view的緩沖區(qū)創(chuàng)建,view內(nèi)容的繪制(如果重寫了drawRect),這些CPU的工作。然后將這個(gè)緩沖區(qū)交給GPU渲染,這個(gè)過(guò)程又包括多個(gè)view的拼接(compositing),紋理的渲染(Texture)等,最終顯示在屏幕上。因此,如果在16.7ms內(nèi)完不成這些操作,比如,CPU做了太多的工作,或者view層次過(guò)于多,圖片過(guò)于大,導(dǎo)致GPU壓力太大,就會(huì)導(dǎo)致“卡”的現(xiàn)象,也就是丟幀。
蘋果官方給出的最佳幀率是:60fps,也就是1幀不丟,當(dāng)然這是理想中的絕佳的體驗(yàn)。
這個(gè)60fps改怎么理解呢?一般來(lái)說(shuō)如果幀率達(dá)到25+fps,人眼就基本感覺(jué)不到停頓了,因此,如果你能讓你ios程序穩(wěn)定的保持在30fps已經(jīng)很不錯(cuò)了,注意,是“穩(wěn)定”在30fps,而不是,10fps,40fps,20fps這樣的跳動(dòng),如果幀頻不穩(wěn)就會(huì)有卡的感覺(jué)。60fps真的很難達(dá)到,尤其在iphone4,4s上。
總的來(lái)說(shuō),UIView從繪制到Render的過(guò)程有如下幾步:
每一個(gè)UIView都有一個(gè)layer,每一個(gè)layer都有個(gè)content,這個(gè)content指向的是一塊緩存,叫做backing store。
UIView的繪制和渲染是兩個(gè)過(guò)程,當(dāng)UIView被繪制時(shí),CPU執(zhí)行drawRect,通過(guò)context將數(shù)據(jù)寫入backing store
當(dāng)backing store寫完后,通過(guò)render server交給GPU去渲染,將backing store中的bitmap數(shù)據(jù)顯示在屏幕上
上面提到的從CPU到GPU的過(guò)程可用下圖表示:
下面具體來(lái)討論下這個(gè)過(guò)程
CPU bound:
假設(shè)我們創(chuàng)建一個(gè)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];
這個(gè)時(shí)候不會(huì)發(fā)生任何操作,由于UILabel重寫了drawRect,因此,這個(gè)view會(huì)被marked as “dirty”:
類似這個(gè)樣子:
然后一個(gè)新的Runloop到來(lái),上面說(shuō)道在這個(gè)Runloop中需要將界面渲染上去,對(duì)于UIKit的渲染,Apple用的是它的Core Animation。
做法是在Runloop開(kāi)始的時(shí)候調(diào)用:
[CATransaction begin]
在Runloop結(jié)束的時(shí)候調(diào)用
[CATransaction commit]
在begin和commit之間做的事情是將view增加到view hierarchy中,這個(gè)時(shí)候也不會(huì)發(fā)生任何繪制的操作。
當(dāng)[CATransaction commit]執(zhí)行完后,CPU開(kāi)始繪制這個(gè)view:
首先CPU會(huì)為layer分配一塊內(nèi)存用來(lái)繪制bitmap,叫做backing store
創(chuàng)建指向這塊bitmap緩沖區(qū)的指針,叫做CGContextRef
通過(guò)Core Graphic的api,也叫Quartz2D,繪制bitmap
將layer的content指向生成的bitmap
清空dirty flag標(biāo)記
這樣CPU的繪制基本上就完成了。
通過(guò)time profiler 可以完整的看到個(gè)過(guò)程:
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:]
假如某個(gè)時(shí)刻修改了label的text:
label.text = @"hello world";
由于內(nèi)容變了,layer的content的bitmap的尺寸也要變化,因此這個(gè)時(shí)候當(dāng)新的Runloop到來(lái)時(shí),CPU要為layer重新創(chuàng)建一個(gè)backing store,重新繪制bitmap。
CPU這一塊最耗時(shí)的地方往往在Core Graphic的繪制上,關(guān)于Core Graphic的性能優(yōu)化是另一個(gè)話題了,又會(huì)牽扯到很多東西,就不在這里討論了。
GPU bound:
CPU完成了它的任務(wù):將view變成了bitmap,然后就是GPU的工作了,GPU處理的單位是Texture。
基本上我們控制GPU都是通過(guò)OpenGL來(lái)完成的,但是從bitmap到Texture之間需要一座橋梁,Core Animation正好充當(dāng)了這個(gè)角色:
Core Animation對(duì)OpenGL的api有一層封裝,當(dāng)我們的要渲染的layer已經(jīng)有了bitmap content的時(shí)候,這個(gè)content一般來(lái)說(shuō)是一個(gè)CGImageRef,CoreAnimation會(huì)創(chuàng)建一個(gè)OpenGL的Texture并將CGImageRef(bitmap)和這個(gè)Texture綁定,通過(guò)TextureID來(lái)標(biāo)識(shí)。
這個(gè)對(duì)應(yīng)關(guān)系建立起來(lái)之后,剩下的任務(wù)就是GPU如何將Texture渲染到屏幕上了。
GPU大致的工作模式如下:
整個(gè)過(guò)程也就是一件事:CPU將準(zhǔn)備好的bitmap放到RAM里,GPU去搬這快內(nèi)存到VRAM中處理。
而這個(gè)過(guò)程GPU所能承受的極限大概在16.7ms完成一幀的處理,所以最開(kāi)始提到的60fps其實(shí)就是GPU能處理的最高頻率。
因此,GPU的挑戰(zhàn)有兩個(gè):
將數(shù)據(jù)從RAM搬到VRAM中
將Texture渲染到屏幕上
這兩個(gè)中瓶頸基本在第二點(diǎn)上。渲染Texture基本要處理這么幾個(gè)問(wèn)題:
Compositing:
Compositing是指將多個(gè)紋理拼到一起的過(guò)程,對(duì)應(yīng)UIKit,是指處理多個(gè)view合到一起的情況,如
[self.view addsubview : subview]。
如果view之間沒(méi)有疊加,那么GPU只需要做普通渲染即可。 如果多個(gè)view之間有疊加部分,GPU需要做blending。
加入兩個(gè)view大小相同,一個(gè)疊加在另一個(gè)上面,那么計(jì)算公式如下:
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,即不透明。那么它會(huì)遮住下層的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)。
基本上每個(gè)像素點(diǎn)都需要這么計(jì)算一次。
因此,view的層級(jí)很復(fù)雜,或者view都是半透明的(alpha值不為1)都會(huì)帶來(lái)GPU額外的計(jì)算工作。
Size
這個(gè)問(wèn)題,主要是處理image帶來(lái)的,假如內(nèi)存里有一張400x400的圖片,要放到100x100的imageview里,如果不做任何處理,直接丟進(jìn)去,問(wèn)題就大了,這意味著,GPU需要對(duì)大圖進(jìn)行縮放到小的區(qū)域顯示,需要做像素點(diǎn)的sampling,這種smapling的代價(jià)很高,又需要兼顧pixel alignment。計(jì)算量會(huì)飆升。
Offscreen Rendering And Mask
如果我們對(duì)layer做這樣的操作:
label.layer.cornerRadius = 5.0f;
label.layer.masksToBounds = YES;
會(huì)產(chǎn)生offscreen rendering,它帶來(lái)的最大的問(wèn)題是,當(dāng)渲染這樣的layer的時(shí)候,需要額外開(kāi)辟內(nèi)存,繪制好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;
簡(jiǎn)單的說(shuō),這是一種cache機(jī)制。
同樣GPU的性能也可以通過(guò)instrument去衡量:
紅色代表GPU需要做額外的工作來(lái)渲染View,綠色代表GPU無(wú)需做額外的工作來(lái)處理bitmap。
That’s all
layoutSubviews
調(diào)用總結(jié)
- 自身的frame發(fā)生變化, 會(huì)重新布局
layoutSubviews
- 添加視圖,調(diào)用
addSubView
的時(shí)候 - 滾動(dòng)一個(gè)UIScrollView會(huì)觸發(fā)
- 子視圖frame發(fā)生變化,會(huì)調(diào)用父視圖的
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)