視圖渲染框架
UIKit是常用的框架,顯示读拆、動(dòng)畫(huà)都通過(guò)CoreAnimation擅憔。CoreAnimation是核心動(dòng)畫(huà),依賴(lài)于OpenGL ES做GPU渲染檐晕,CoreGraphics做CPU渲染暑诸;最底層的GraphicsHardWare是圖形硬件。
下圖是另外一種表現(xiàn)的形式辟灰。在屏幕上顯示視圖个榕,需要CPU和GPU一起協(xié)作。一部數(shù)據(jù)通過(guò)CoreGraphics芥喇、CoreImage由CPU預(yù)處理西采。最終通過(guò)OpenGL ES將數(shù)據(jù)傳送到 GPU,最終顯示到屏幕继控。
CoreImage支持CPU械馆、GPU兩種處理模式。
顯示邏輯
1武通、CoreAnimation提交會(huì)話(huà)霹崎,包括自己和子樹(shù)(view hierarchy)的layout狀態(tài)等;
2冶忱、RenderServer解析提交的子樹(shù)狀態(tài)尾菇,生成繪制指令;
3、GPU執(zhí)行繪制指令错沽;
4簿晓、顯示渲染后的數(shù)據(jù);
提交流程(以動(dòng)畫(huà)為例)
第2步為prepare to commit animation (layoutSubviews,drawRect:)千埃;
1憔儿、布局(Layout)
調(diào)用layoutSubviews方法;調(diào)用addSubview:方法放可;
會(huì)造成CPU和I/O瓶頸谒臼;
2、顯示(Display)
通過(guò)drawRect繪制視圖耀里;繪制string(字符串)蜈缤;
會(huì)造成CPU和內(nèi)存瓶頸;每個(gè)UIView都有CALayer冯挎,同時(shí)圖層有一個(gè)像素存儲(chǔ)空間底哥,存放視圖;調(diào)用-setNeedsDisplay的時(shí)候房官,僅會(huì)設(shè)置圖層為dirty趾徽。當(dāng)渲染系統(tǒng)準(zhǔn)備就緒,調(diào)用視圖的-display方法翰守,同時(shí)裝配像素存儲(chǔ)空間孵奶,建立一個(gè)CoreGraphics上下文(CGContextRef),將上下文push進(jìn)上下文堆棧蜡峰,繪圖程序進(jìn)入對(duì)應(yīng)的內(nèi)存存儲(chǔ)空間了袁。
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(20, 20)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];
在-drawRect方法中實(shí)現(xiàn)如上代碼,UIKit會(huì)將自動(dòng)生成的CGContextRef 放入上下文堆棧湿颅。當(dāng)繪制完成后载绿,視圖的像素會(huì)被渲染到屏幕上;當(dāng)下次再次調(diào)用視圖的-setNeedsDisplay肖爵,將會(huì)再次調(diào)用-drawRect方法卢鹦。
3、準(zhǔn)備提交(Prepare)
解碼圖片劝堪;圖片格式轉(zhuǎn)換冀自;
GPU不支持的某些圖片格式,盡量使用GPU能支持的圖片格式秒啦;
4熬粗、提交(Commit)
打包layers并發(fā)送到渲染server;遞歸提交子樹(shù)的layers余境;
如果子樹(shù)太復(fù)雜驻呐,會(huì)消耗很大灌诅,對(duì)性能造成影響;
盡可能簡(jiǎn)化viewTree含末;
當(dāng)顯示一個(gè)UIImageView時(shí)猜拾,Core Animation會(huì)創(chuàng)建一個(gè)OpenGL ES紋理,并確保在這個(gè)圖層中的位圖被上傳到對(duì)應(yīng)的紋理中佣盒。當(dāng)你重寫(xiě)-drawInContext
方法時(shí)挎袜,Core Animation會(huì)請(qǐng)求分配一個(gè)紋理,同時(shí)確保Core Graphics會(huì)將你在-drawInContext
中繪制的東西放入到紋理的位圖數(shù)據(jù)中肥惭。
渲染總流程
在 VSync 信號(hào)到來(lái)后盯仪,系統(tǒng)圖形服務(wù)會(huì)通過(guò) CADisplayLink 等機(jī)制通知 App,App 主線(xiàn)程開(kāi)始在 CPU 中計(jì)算顯示內(nèi)容蜜葱,比如視圖的創(chuàng)建全景、布局計(jì)算、圖片解碼牵囤、文本繪制等爸黄。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換揭鳞、合成馆纳、渲染。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去汹桦,等待下一次 VSync 信號(hào)到來(lái)時(shí)顯示到屏幕上。由于垂直同步的機(jī)制鉴裹,如果在一個(gè) VSync 時(shí)間內(nèi)舞骆,CPU 或者 GPU 沒(méi)有完成內(nèi)容提交,則那一幀就會(huì)被丟棄径荔,等待下一次機(jī)會(huì)再顯示督禽,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因总处。
渲染時(shí)機(jī)
上面已經(jīng)提到過(guò):Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer 監(jiān)聽(tīng) BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件 狈惫。當(dāng)在操作 UI 時(shí),比如改變了 Frame鹦马、更新了 UIView/CALayer 的層次時(shí)胧谈,或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理荸频,并被提交到一個(gè)全局的容器去菱肖。當(dāng)Oberver監(jiān)聽(tīng)的事件到來(lái)時(shí),回調(diào)執(zhí)行函數(shù)中會(huì)遍歷所有待處理的UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整旭从,并更新 UI 界面稳强。
這個(gè)函數(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];
渲染具體步驟
動(dòng)畫(huà)和屏幕上組合的圖層實(shí)際上被一個(gè)單獨(dú)的進(jìn)程管理场仲,即所謂的渲染服務(wù)。
當(dāng)運(yùn)行一段動(dòng)畫(huà)時(shí)退疫,這個(gè)過(guò)程會(huì)被四個(gè)分離的階段打破:
- 布局--準(zhǔn)備視圖的層級(jí)關(guān)系渠缕,設(shè)置圖層屬性
- 顯示--圖層的寄宿圖片被繪制的階段。涉及到-drawRect和-drawLayer:inContext:等方法
- 準(zhǔn)備--準(zhǔn)備發(fā)送動(dòng)畫(huà)數(shù)據(jù)給渲染服務(wù)的階段褒繁。比如圖片解碼
- 提交--打包所有圖層和動(dòng)畫(huà)屬性亦鳞,通過(guò)IPC發(fā)送到渲染服務(wù)
渲染服務(wù)拿到數(shù)據(jù)后,反序列化成一個(gè)叫做渲染樹(shù)的圖層樹(shù)澜汤,使用這個(gè)樹(shù)狀結(jié)構(gòu)蚜迅,渲染服務(wù)隊(duì)動(dòng)畫(huà)的每一幀做如下工作:
- 對(duì)所有的圖層屬性計(jì)算中間值,設(shè)置OpenGL幾何形狀(紋理化三角形)來(lái)執(zhí)行渲染
- 在屏幕上渲染可見(jiàn)的三角形
所以一共六個(gè)階段:最后兩個(gè)階段在動(dòng)畫(huà)過(guò)程中不停地重復(fù)俊抵,前五個(gè)階段都在軟件層面處理(通過(guò)CPU)谁不,只有最后一個(gè)被GPU執(zhí)行。而且徽诲,你真正只能控制前兩個(gè)階段:布局和顯示刹帕。剩下的在CoreAnimation內(nèi)部處理。
CADisplayLink簡(jiǎn)介
當(dāng)你設(shè)置一個(gè)NSTimer谎替,他會(huì)被插入到當(dāng)前任務(wù)列表中偷溺,然后直到指定時(shí)間過(guò)去之后才會(huì)被執(zhí)行。但是何時(shí)啟動(dòng)定時(shí)器并沒(méi)有一個(gè)時(shí)間上限钱贯,而且它只會(huì)在列表中上一個(gè)任務(wù)完成之后開(kāi)始執(zhí)行挫掏。這通常會(huì)導(dǎo)致有幾毫秒的延遲,但是如果上一個(gè)任務(wù)過(guò)了很久才完成就會(huì)導(dǎo)致延遲很長(zhǎng)一段時(shí)間秩命。
用CADisplayLink而不是NSTimer尉共,會(huì)保證幀率足夠連續(xù),使得動(dòng)畫(huà)看起來(lái)更加平滑弃锐,但即使CADisplayLink也不能保證每一幀都按計(jì)劃執(zhí)行袄友,一些失去控制的離散的任務(wù)或者事件(例如資源緊張的后臺(tái)程序)可能會(huì)導(dǎo)致動(dòng)畫(huà)偶爾地丟幀。當(dāng)使用NSTimer的時(shí)候霹菊,一旦有機(jī)會(huì)計(jì)時(shí)器就會(huì)開(kāi)啟剧蚣,但是CADisplayLink卻不一樣:如果它丟失了幀,就會(huì)直接忽略它們旋廷,然后在下一次更新的時(shí)候接著運(yùn)行鸠按。