UIView之drawRect: & layoutSubviews的作用和機制

重繪機制

iOS的繪圖操作是在UIView的drawRect中完成的,我們想要在UIView中完成繪圖(或者自定義控件),需要在UIView的拓展類(或者子類)中重寫drawRect函數(shù),在這里進(jìn)行繪圖的操作,系統(tǒng)會自動調(diào)用該函數(shù)進(jìn)行繪圖。
重繪也是在drawRect:中完成的,但是Apple并不建議我們直接調(diào)用drawRect:方法,如果直接調(diào)用沒有效果,Apple建議我們調(diào)用setNeedDiplay方法,調(diào)用該方法后,系統(tǒng)會自動調(diào)用drawRect:方法。
我們重寫drawRect:方法可以畫自定義的圖案,或者我們需要自定義View控件時也需要重寫該方法,通常該函數(shù)只會調(diào)用一次,當(dāng)需要手動觸發(fā)是,只需要調(diào)用setNeedDiplay方法即可。

不知道大家是否有想過下面的問題:為什么蘋果會提供drawRect機制,為什么不建議直接調(diào)用drawRect函數(shù),而是建議我們調(diào)用setNeedDisplay ?
這里允許我通俗的描述下:我們可以認(rèn)為,在在創(chuàng)建視圖時,設(shè)置frame等參數(shù)后,可以理解成只有一個點,然后晚些系統(tǒng)查看所有需要繪制的東西,并按順序排列,因為有些內(nèi)容是重疊的,最后高效的將視圖繪制出來。這樣系統(tǒng)根據(jù)層的情況優(yōu)化性能。
另外:再說一下setNeedDisplay函數(shù),加入有A、B兩個VC,如果我們在當(dāng)前顯示的VC A中調(diào)用[B.view drawRect]函數(shù),這時B回去繪制頁面,但是B并未顯示在window上,這就造成了一種資源的浪費。所以Apple建議我們調(diào)用setNeedDisplay,這樣當(dāng)B展示在Window上時再去繪制渲染視圖,充分減少資源浪費。

視圖繪制相關(guān)方法

①、- (void)drawRect:(CGRect)rect;
重寫此方法,執(zhí)行重繪任務(wù)
②、- (void)setNeedsDisplay;
將視圖標(biāo)記為需要重繪,異步調(diào)用drawRect
③、- (void)setNeedsDisplayInRect:(CGRect)rect;
將視圖標(biāo)記為需要局部重繪

drawRect調(diào)用機制

1、調(diào)用時機:loadView ->ViewDidload ->drawRect:
2、如果在UIView初始化時沒有設(shè)置rect大小,將直接導(dǎo)致drawRect:不被自動調(diào)用。
3、通過設(shè)置contentMode屬性值為UIViewContentModeRedraw。那么將在每次設(shè)置或更改frame的時候自動調(diào)用drawRect:。
4、直接調(diào)用setNeedsDisplay,或者setNeedsDisplayInRect:觸發(fā)drawRect:,但是有個前提條件是:view當(dāng)前的rect不能為nil

5、該方法在調(diào)用sizeThatFits后被調(diào)用,所以可以先調(diào)用sizeToFit計算出size。然后系統(tǒng)自動調(diào)用drawRect:方法。
這里簡單說一下sizeToFit和sizeThatFit:
sizeToFit:會計算出最優(yōu)的 size 而且會改變自己的size
sizeThatFits:會計算出最優(yōu)的 size 但是不會改變 自己的 size

注意事項:

1、若使用UIView繪圖,只能在drawRect:方法中獲取相應(yīng)的contextRef并繪圖。如果在其他方法中獲取到一個invalidate的ref保存下來,在drawRect中并不能用于畫圖。等到在這里調(diào)用時,可能當(dāng)前上下文環(huán)境已經(jīng)變化。
2、若使用CALayer繪圖,只能在drawInContext: 中(類似于drawRect)繪制,或者在delegate中的相應(yīng)方法繪制。同樣也是調(diào)用setNeedDisplay等間接調(diào)用以上方法。
3、若要實時畫圖,不能使用gestureRecognizer,只能使用touchbegan等方法來掉用setNeedsDisplay實時刷新屏幕。
4、UIImageView繼承自UIView,但是UIImageView能不重寫drawRect方法用于實現(xiàn)自定義繪圖。具體原因如下:
Apple在文檔中指出:UIImageView是專門為顯示圖片做的控件,用了最優(yōu)顯示技術(shù),是不讓調(diào)用darwrect方法, 要調(diào)用這個方法,只能從uiview里重寫。

layoutSubviews

這個方法是用來對subviews重新布局,默認(rèn)沒有做任何事情,需要子類進(jìn)行重寫。
當(dāng)我們在某個類的內(nèi)部調(diào)整子視圖位置時,需要調(diào)用。
反過來的意思就是說:如果你想要在外部設(shè)置subviews的位置,就不要重寫。

視圖布局相關(guān)方法:

①、- (void)layoutSubviews;
對subview重新布局
②、- (void)setNeedsLayout;
將視圖標(biāo)記為需要重新布局, 這個方法會在系統(tǒng)runloop的下一個周期自動調(diào)用layoutSubviews。
③、- (void)layoutIfNeeded;
如果有需要刷新的標(biāo)記,立即調(diào)用layoutSubviews進(jìn)行布局(如果沒有標(biāo)記,不會調(diào)用layoutSubviews)這里注意一個點:標(biāo)記,沒有標(biāo)記,即使我們掉了該函數(shù)也不起作用
如果要立即刷新,要先調(diào)用[view setNeedsLayout],把標(biāo)記設(shè)為需要布局,然后馬上調(diào)用[view layoutIfNeeded],實現(xiàn)布局.
在視圖第一次顯示之前,標(biāo)記總是“需要刷新”的,可以直接調(diào)用[view layoutIfNeeded]

這里有必要描述下三者之間的關(guān)系:
在沒有外界干預(yù)的情況下,一個view的frame或者bounds發(fā)生變化時,系統(tǒng)會先去標(biāo)記flag這個view,等下一次渲染時機到來時(也就是runloop的下一次循環(huán)),會去按照最新的布局去重新布局視圖。
setNeedLayout就是給這個view添加一個標(biāo)記,告訴系統(tǒng)下一次渲染時機需要重新布局這個視圖。
layoutIfNeed就是告訴系統(tǒng),如果已經(jīng)設(shè)置了flag,那不用等待下個渲染時機到來,立即重新渲染。前提是設(shè)置了flag。
layoutSubviews則是由系統(tǒng)去調(diào)用,不需要我們主動調(diào)用,我們只需要調(diào)用layoutIfNeed,告訴系統(tǒng)是否立即執(zhí)行重新布局的操作。

layoutSubviews調(diào)用時機

結(jié)論是經(jīng)過搜索得到的,基于此筆者進(jìn)行了驗證,并得到了些結(jié)果:
1、init初始化不會觸發(fā)layoutSubviews。
2、addSubview會觸發(fā)layoutSubviews。(當(dāng)然這里frame為0,是不會調(diào)用的,同上面的drawrect:一樣)
3、設(shè)置view的Frame會觸發(fā)layoutSubviews,(當(dāng)然前提是frame的值設(shè)置前后發(fā)生了變化。)
4、滾動一個UIScrollView會觸發(fā)layoutSubviews。
5、旋轉(zhuǎn)屏幕會觸發(fā)父UIView上的layoutSubviews事件。(這個我們開發(fā)中會經(jīng)常遇到,比如屏幕旋轉(zhuǎn)時,為了界面美觀我們需要修改子view的frame,那就會在layoutSubview中做相應(yīng)的操作)
6、改變一個UIView大小的時候也會觸發(fā)父UIView上的layoutSubviews事件。
7、直接調(diào)用setLayoutSubviews。(Apple是不建議這么做的)

這里需要補充一點:layoutSubview是布局相關(guān),而drawRect則是負(fù)責(zé)繪制。因此從調(diào)用時序上來講,layoutSubviews要早于drawRect:函數(shù)。

關(guān)于LayoutSubView我們再來看一個例子:
1、另同時用上一套的場景舉個例,當(dāng)想知道tableView reloadData后的contentSize的話可以在reloadData后用這兩個方法,然后就可以直接提取contentSize了。
2、demo完善中,稍后奉上

渲染的時機

了解了drawRect:和layoutSubviews:的原理后,我們是否會想跟進(jìn)一步的去了解:我在使用setNeedDisplay和setNeedLayout分別標(biāo)記了需要重繪和需要重新布局后,那到底什么時間去執(zhí)行的渲染操作呢?我們接下里詳細(xì)拆分講解

iOS顯示系統(tǒng):

1、如何讓App渲染的代碼定時執(zhí)行(例如:每秒執(zhí)行60次)?
iOS 的顯示系統(tǒng)是由 VSync 信號驅(qū)動的,VSync 信號由硬件時鐘生成,每秒鐘發(fā)出 60 次(這個值取決設(shè)備硬件,比如 iPhone 真機上通常是 59.97)。iOS 圖形服務(wù)接收到 VSync 信號后,會通過 IPC 通知到 App 內(nèi)。App 的 Runloop 在啟動后會注冊基于端口的源也就是source1,Vsync信號則通過 mach_port 端口傳遞過來,同時喚醒runloop,隨后 Source1 的回調(diào)會驅(qū)動整個 App 的動畫與顯示。
tips:圖形服務(wù)同APP Process是兩個進(jìn)程,他們之間通信的方式是IPC,了解WKWebview實現(xiàn)機制的同學(xué)會發(fā)現(xiàn),WebContent process 同App process進(jìn)行通信的方式也是通過IPC來實現(xiàn)的。有興趣的同學(xué)可以參考我的另一篇博客:關(guān)于wkwebview講解。
2、通過mach_port端口發(fā)送消息,喚醒Runloop后,做了一些修改view和layer的工作,并提交到全局容器,等待渲染時機到來。
Core Animation 在 RunLoop 中注冊了一個 Observer,監(jiān)聽了 BeforeWaiting 和 Exit 事件。當(dāng)一個觸摸事件到來時(也可以理解成Vsync信號喚起),RunLoop 被喚醒,App 中的代碼會執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級、設(shè)置 UIView 的 frame、修改 CALayer 的透明度、為視圖添加一個動畫;這些操作最終都會被 CALayer 標(biāo)記,并通過 CATransaction 提交到一個中間狀態(tài)去。當(dāng)上面所有操作結(jié)束后,RunLoop 即將進(jìn)入休眠(或者退出)時,關(guān)注該事件的 Observer 都會得到通知。這時 Core Animation 注冊的那個 Observer 就會在回調(diào)中,把所有的中間狀態(tài)合并提交到 GPU 去顯示;
如果此處有動畫,通過 DisplayLink 穩(wěn)定的刷新機制會不斷的喚醒runloop,使得不斷的有機會觸發(fā)observer回調(diào),從而根據(jù)時間來不斷更新這個動畫的屬性值并 繪制出來。
注:動畫由CADisplayLink來不斷喚醒runloop。
3、具體邏輯圖:(來源于網(wǎng)絡(luò))

image

渲染時機

1、Core Animation 在 RunLoop 中注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件 。
2、當(dāng)在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個全局的容器去。當(dāng)Oberver監(jiān)聽的事件到來時,回調(diào)執(zhí)行函數(shù)中會遍歷所有待處理的UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整,并更新 UI 界面。
3、回調(diào)函數(shù)內(nèi)部調(diào)用棧大致如下:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
   QuartzCore:CA::Transaction::observer_callback:
       CA::Transaction::commit();
           CA::Context::commit_transaction();
               CA::Layer::layout_and_display_if_needed();
                   CA::Layer::layout_if_needed();
                         [CALayer layoutSublayers];
                         [UIView layoutSubviews];
                   CA::Layer::display_if_needed();
                         [CALayer display];
                         [UIView drawRect];

簡單解釋下:

1、首先是通過CATransaction提交到全局的容器中
2、檢查是否有標(biāo)記為需要重新繪制和布局的Layer
3、如果有則執(zhí)行l(wèi)ayout和redraw操作。
另外從這上面我們也可以看到:一定是先有布局,再去繪制圖形。即:layout調(diào)用一定是在drawRect:之前。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容