視圖渲染框架
UIKit是常用的框架,顯示、動畫都通過CoreAnimation。CoreAnimation是核心動畫,依賴于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染;最底層的GraphicsHardWare是圖形硬件。

下圖是另外一種表現(xiàn)的形式。在屏幕上顯示視圖,需要CPU和GPU一起協(xié)作。一部數(shù)據(jù)通過CoreGraphics、CoreImage由CPU預(yù)處理。最終通過OpenGL ES將數(shù)據(jù)傳送到 GPU,最終顯示到屏幕。
CoreImage支持CPU、GPU兩種處理模式。

顯示邏輯
1、CoreAnimation提交會話,包括自己和子樹(view hierarchy)的layout狀態(tài)等;
2、RenderServer解析提交的子樹狀態(tài),生成繪制指令;
3、GPU執(zhí)行繪制指令;
4、顯示渲染后的數(shù)據(jù);

提交流程(以動畫為例)
第2步為prepare to commit animation (layoutSubviews,drawRect:);

1、布局(Layout)
調(diào)用layoutSubviews方法;調(diào)用addSubview:方法;
會造成CPU和I/O瓶頸;
2、顯示(Display)
通過drawRect繪制視圖;繪制string(字符串);
會造成CPU和內(nèi)存瓶頸;每個UIView都有CALayer,同時圖層有一個像素存儲空間,存放視圖;調(diào)用-setNeedsDisplay的時候,僅會設(shè)置圖層為dirty。當(dāng)渲染系統(tǒng)準(zhǔn)備就緒,調(diào)用視圖的-display方法,同時裝配像素存儲空間,建立一個CoreGraphics上下文(CGContextRef),將上下文push進(jìn)上下文堆棧,繪圖程序進(jìn)入對應(yīng)的內(nèi)存存儲空間。
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方法中實現(xiàn)如上代碼,UIKit會將自動生成的CGContextRef 放入上下文堆棧。當(dāng)繪制完成后,視圖的像素會被渲染到屏幕上;當(dāng)下次再次調(diào)用視圖的-setNeedsDisplay,將會再次調(diào)用-drawRect方法。
3、準(zhǔn)備提交(Prepare)
解碼圖片;圖片格式轉(zhuǎn)換;
GPU不支持的某些圖片格式,盡量使用GPU能支持的圖片格式;
4、提交(Commit)
打包layers并發(fā)送到渲染server;遞歸提交子樹的layers;
如果子樹太復(fù)雜,會消耗很大,對性能造成影響;
盡可能簡化viewTree;
當(dāng)顯示一個UIImageView時,Core Animation會創(chuàng)建一個OpenGL ES紋理,并確保在這個圖層中的位圖被上傳到對應(yīng)的紋理中。當(dāng)你重寫-drawInContext
方法時,Core Animation會請求分配一個紋理,同時確保Core Graphics會將你在-drawInContext
中繪制的東西放入到紋理的位圖數(shù)據(jù)中。
渲染總流程



在 VSync 信號到來后,系統(tǒng)圖形服務(wù)會通過 CADisplayLink 等機(jī)制通知 App,App 主線程開始在 CPU 中計算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換、合成、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機(jī)制,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機(jī)會再顯示,而這時顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因。
渲染時機(jī)
上面已經(jīng)提到過:Core Animation 在 RunLoop 中注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件 。當(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 界面。
這個函數(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];
渲染具體步驟
動畫和屏幕上組合的圖層實際上被一個單獨的進(jìn)程管理,即所謂的渲染服務(wù)。
當(dāng)運(yùn)行一段動畫時,這個過程會被四個分離的階段打破:
- 布局--準(zhǔn)備視圖的層級關(guān)系,設(shè)置圖層屬性
- 顯示--圖層的寄宿圖片被繪制的階段。涉及到-drawRect和-drawLayer:inContext:等方法
- 準(zhǔn)備--準(zhǔn)備發(fā)送動畫數(shù)據(jù)給渲染服務(wù)的階段。比如圖片解碼
- 提交--打包所有圖層和動畫屬性,通過IPC發(fā)送到渲染服務(wù)
渲染服務(wù)拿到數(shù)據(jù)后,反序列化成一個叫做渲染樹的圖層樹,使用這個樹狀結(jié)構(gòu),渲染服務(wù)隊動畫的每一幀做如下工作:
- 對所有的圖層屬性計算中間值,設(shè)置OpenGL幾何形狀(紋理化三角形)來執(zhí)行渲染
- 在屏幕上渲染可見的三角形
所以一共六個階段:最后兩個階段在動畫過程中不停地重復(fù),前五個階段都在軟件層面處理(通過CPU),只有最后一個被GPU執(zhí)行。而且,你真正只能控制前兩個階段:布局和顯示。剩下的在CoreAnimation內(nèi)部處理。
CADisplayLink簡介
當(dāng)你設(shè)置一個NSTimer,他會被插入到當(dāng)前任務(wù)列表中,然后直到指定時間過去之后才會被執(zhí)行。但是何時啟動定時器并沒有一個時間上限,而且它只會在列表中上一個任務(wù)完成之后開始執(zhí)行。這通常會導(dǎo)致有幾毫秒的延遲,但是如果上一個任務(wù)過了很久才完成就會導(dǎo)致延遲很長一段時間。
用CADisplayLink而不是NSTimer,會保證幀率足夠連續(xù),使得動畫看起來更加平滑,但即使CADisplayLink也不能保證每一幀都按計劃執(zhí)行,一些失去控制的離散的任務(wù)或者事件(例如資源緊張的后臺程序)可能會導(dǎo)致動畫偶爾地丟幀。當(dāng)使用NSTimer的時候,一旦有機(jī)會計時器就會開啟,但是CADisplayLink卻不一樣:如果它丟失了幀,就會直接忽略它們,然后在下一次更新的時候接著運(yùn)行。