重繪機制
iOS的繪圖操作是在UIView的drawRect中完成的,我們想要在UIView中完成繪圖(或者自定義控件)含懊,需要在UIView的拓展類(或者子類)中重寫drawRect函數(shù)身冬,在這里進行繪圖的操作,系統(tǒng)會自動調(diào)用該函數(shù)進行繪圖岔乔。
重繪也是在drawRect:中完成的酥筝,但是Apple并不建議我們直接調(diào)用drawRect:方法,如果直接調(diào)用沒有效果雏门,Apple建議我們調(diào)用setNeedDiplay方法嘿歌,調(diào)用該方法后剿配,系統(tǒng)會自動調(diào)用drawRect:方法。
我們重寫drawRect:方法可以畫自定義的圖案阅束,或者我們需要自定義View控件時也需要重寫該方法呼胚,通常該函數(shù)只會調(diào)用一次,當需要手動觸發(fā)是息裸,只需要調(diào)用setNeedDiplay方法即可蝇更。
不知道大家是否有想過下面的問題:為什么蘋果會提供drawRect機制,為什么不建議直接調(diào)用drawRect函數(shù)呼盆,而是建議我們調(diào)用setNeedDisplay ?
這里允許我通俗的描述下:我們可以認為年扩,在在創(chuàng)建視圖時,設(shè)置frame等參數(shù)后访圃,可以理解成只有一個點厨幻,然后晚些系統(tǒng)查看所有需要繪制的東西,并按順序排列,因為有些內(nèi)容是重疊的况脆,最后高效的將視圖繪制出來饭宾。這樣系統(tǒng)根據(jù)層的情況優(yōu)化性能。
另外:再說一下setNeedDisplay函數(shù)格了,加入有A看铆、B兩個VC,如果我們在當前顯示的VC A中調(diào)用[B.view drawRect]函數(shù)盛末,這時B回去繪制頁面弹惦,但是B并未顯示在window上,這就造成了一種資源的浪費悄但。所以Apple建議我們調(diào)用setNeedDisplay棠隐,這樣當B展示在Window上時再去繪制渲染視圖,充分減少資源浪費算墨。
視圖繪制相關(guān)方法
①宵荒、- (void)drawRect:(CGRect)rect;
重寫此方法,執(zhí)行重繪任務
②净嘀、- (void)setNeedsDisplay;
將視圖標記為需要重繪报咳,異步調(diào)用drawRect
③、- (void)setNeedsDisplayInRect:(CGRect)rect;
將視圖標記為需要局部重繪
drawRect調(diào)用機制
1挖藏、調(diào)用時機:loadView ->ViewDidload ->drawRect:
2暑刃、如果在UIView初始化時沒有設(shè)置rect大小,將直接導致drawRect:不被自動調(diào)用膜眠。
3岩臣、通過設(shè)置contentMode屬性值為UIViewContentModeRedraw。那么將在每次設(shè)置或更改frame
的時候自動調(diào)用drawRect:
宵膨。
4架谎、直接調(diào)用setNeedsDisplay,或者setNeedsDisplayInRect:觸發(fā)drawRect:辟躏,但是有個前提條件是:view當前的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:方法中獲取相應的contextRef并繪圖末秃。如果在其他方法中獲取到一個invalidate的ref保存下來,在drawRect中并不能用于畫圖籽御。等到在這里調(diào)用時练慕,可能當前上下文環(huán)境已經(jīng)變化惰匙。
2、若使用CALayer繪圖贺待,只能在drawInContext: 中(類似于drawRect)繪制徽曲,或者在delegate中的相應方法繪制。同樣也是調(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重新布局
茴她,默認沒有做任何事情寻拂,需要子類進行重寫。
當我們在某個類的內(nèi)部調(diào)整子視圖位置時丈牢,需要調(diào)用祭钉。
反過來的意思就是說:如果你想要在外部設(shè)置subviews的位置,就不要重寫己沛。
視圖布局相關(guān)方法:
①慌核、- (void)layoutSubviews;
對subview重新布局
②、- (void)setNeedsLayout;
將視圖標記為需要重新布局申尼, 這個方法會在系統(tǒng)runloop的下一個周期自動調(diào)用layoutSubviews垮卓。
③、- (void)layoutIfNeeded;
如果有需要刷新的標記
师幕,立即調(diào)用layoutSubviews進行布局(如果沒有標記粟按,不會調(diào)用layoutSubviews)這里注意一個點:標記,沒有標記们衙,即使我們掉了該函數(shù)也不起作用
钾怔。
如果要立即刷新碱呼,要先調(diào)用[view setNeedsLayout]蒙挑,把標記設(shè)為需要布局,然后馬上調(diào)用[view layoutIfNeeded]愚臀,實現(xiàn)布局.
在視圖第一次顯示之前忆蚀,標記總是“需要刷新”的,可以直接調(diào)用[view layoutIfNeeded]
這里有必要描述下三者之間的關(guān)系:
在沒有外界干預的情況下,一個view的frame或者bounds發(fā)生變化時馋袜,系統(tǒng)會先去標記flag這個view,等下一次渲染時機到來時(也就是runloop的下一次循環(huán))男旗,會去按照最新的布局去重新布局視圖。
setNeedLayout
就是給這個view添加一個標記欣鳖,告訴系統(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)過搜索得到的,基于此筆者進行了驗證样眠,并得到了些結(jié)果:
1友瘤、init初始化不會觸發(fā)layoutSubviews。
2吹缔、addSubview會觸發(fā)layoutSubviews商佑。(當然這里frame為0,是不會調(diào)用的厢塘,同上面的drawrect:一樣)
3茶没、設(shè)置view的Frame會觸發(fā)layoutSubviews,(當然前提是frame的值設(shè)置前后發(fā)生了變化晚碾。)
4抓半、滾動一個UIScrollView會觸發(fā)layoutSubviews。
5格嘁、旋轉(zhuǎn)屏幕會觸發(fā)父UIView上的layoutSubviews事件笛求。(這個我們開發(fā)中會經(jīng)常遇到,比如屏幕旋轉(zhuǎn)時糕簿,為了界面美觀我們需要修改子view的frame探入,那就會在layoutSubview中做相應的操作)
6、改變一個UIView大小的時候也會觸發(fā)父UIView上的layoutSubviews事件。
7跟衅、直接調(diào)用setLayoutSubviews闷畸。(Apple是不建議這么做的)
這里需要補充一點:
layoutSubview是布局相關(guān),而drawRect則是負責繪制植旧。因此從調(diào)用時序上來講辱揭,layoutSubviews要早于drawRect:函數(shù)。
關(guān)于LayoutSubView我們再來看一個例子:
1病附、另同時用上一套的場景舉個例问窃,當想知道tableView reloadData后的contentSize的話可以在reloadData后用這兩個方法,然后就可以直接提取contentSize了完沪。
2域庇、demo完善中,稍后奉上
渲染的時機
了解了drawRect:和layoutSubviews:的原理后覆积,我們是否會想跟進一步的去了解:我在使用setNeedDisplay和setNeedLayout分別標記了需要重繪和需要重新布局后较剃,那到底什么時間去執(zhí)行的渲染操作呢?我們接下里詳細拆分講解
iOS顯示系統(tǒng):
1技健、如何讓App渲染的代碼定時執(zhí)行(例如:每秒執(zhí)行60次)写穴?
iOS 的顯示系統(tǒng)是由 VSync 信號驅(qū)動的,VSync 信號由硬件時鐘生成雌贱,每秒鐘發(fā)出 60 次(這個值取決設(shè)備硬件啊送,比如 iPhone 真機上通常是 59.97)。iOS 圖形服務接收到 VSync 信號后欣孤,會通過 IPC 通知到 App 內(nèi)馋没。App 的 Runloop 在啟動后會注冊基于端口的源也就是source1,Vsync信號則通過 mach_port 端口傳遞過來降传,同時喚醒runloop篷朵,隨后 Source1 的回調(diào)會驅(qū)動整個 App 的動畫與顯示。
tips:圖形服務同APP Process是兩個進程婆排,他們之間通信的方式是IPC,了解WKWebview實現(xiàn)機制的同學會發(fā)現(xiàn)声旺,WebContent process 同App process進行通信的方式也是通過IPC來實現(xiàn)的。有興趣的同學可以參考我的另一篇博客:關(guān)于wkwebview講解段只。
2腮猖、通過mach_port端口發(fā)送消息,喚醒Runloop后赞枕,做了一些修改view和layer的工作澈缺,并提交到全局容器,等待渲染時機到來炕婶。
Core Animation 在 RunLoop 中注冊了一個 Observer姐赡,監(jiān)聽了 BeforeWaiting 和 Exit 事件。當一個觸摸事件到來時(也可以理解成Vsync信號喚起)柠掂,RunLoop 被喚醒项滑,App 中的代碼會執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級陪踩、設(shè)置 UIView 的 frame杖们、修改 CALayer 的透明度、為視圖添加一個動畫肩狂;這些操作最終都會被 CALayer 標記摘完,并通過 CATransaction 提交到一個中間狀態(tài)去。
當上面所有操作結(jié)束后傻谁,RunLoop 即將進入休眠(或者退出)時孝治,關(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)絡)
渲染時機
1、Core Animation 在 RunLoop 中注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 瘩蚪。
2泉懦、當在操作 UI 時,比如改變了 Frame疹瘦、更新了 UIView/CALayer 的層次時崩哩,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理言沐,并被提交到一個全局的容器去邓嘹。當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鲸阻、檢查是否有標記為需要重新繪制和布局的Layer
3、如果有則執(zhí)行l(wèi)ayout和redraw操作缨睡。
另外從這上面我們也可以看到:一定是先有布局鸟悴,再去繪制圖形。即:layout調(diào)用一定是在drawRect:之前奖年。