繪制和渲染的流程
運(yùn)行一段動(dòng)畫(huà)的過(guò)程可以分為6個(gè)階段:
1> 布局 - 為視圖/圖層準(zhǔn)備層級(jí)關(guān)系拖陆,以及設(shè)置圖層屬性(位置抹恳,背景色,邊框等等)的階段橙困。
2> 顯示 - 圖層的寄宿圖片被繪制的階段瞧掺。繪制涉及到-drawRect:和-drawLayer:inContext:方法的調(diào)用。
3> 準(zhǔn)備 - Image decoding, Image conversion(如果圖片類(lèi)型不是GPU所支持的纷宇,需要對(duì)圖片進(jìn)行轉(zhuǎn)換)夸盟。
4> 提交 - Core Animation打包所有的圖層和動(dòng)畫(huà),然后通過(guò)IPC(進(jìn)程內(nèi)通信)發(fā)送到渲染服務(wù)(render server像捶,一個(gè)單獨(dú)管理動(dòng)畫(huà)和圖層組合的一個(gè)系統(tǒng)進(jìn)程)上陕。這個(gè)步驟是遞歸的,所以如果layer tree如果比較復(fù)雜此步驟代價(jià)比較高拓春。
上面4個(gè)步驟發(fā)生在自己的應(yīng)用程序內(nèi)部释簿,動(dòng)畫(huà)顯示到屏幕之前還有2個(gè)步驟的工作:
5> 對(duì)所有圖層屬性計(jì)算中間值,設(shè)置OpenGL幾何形狀來(lái)執(zhí)行渲染硼莽。
6> 在屏幕上渲染可見(jiàn)的三角形庶溶。
前5個(gè)階段都在軟件層面處理(通過(guò)CPU),只有最后一個(gè)階段被GPU執(zhí)行懂鸵。6個(gè)階段中只有布局和顯示兩個(gè)階段是可以被我們控制的偏螺,Core Animation框架處理剩下的事務(wù)。
CPU vs GPU
CPU和GPU在屏幕上顯示內(nèi)容扮演了重要的角色匆光,為了達(dá)到60fps套像,CPU和GPU需要在1/60=16.67ms內(nèi)完成各自的工作。在優(yōu)化iOS繪制和渲染過(guò)程中终息,需要從CPU和GPU兩方面入手夺巩,確認(rèn)是哪一部分達(dá)到了性能瓶頸影響了繪制效率。并且在可控制的布局和顯示階段周崭,決定哪些由CPU執(zhí)行柳譬,哪些交給GPU去做。
影響CPU使用率的操作
布局的計(jì)算
如果視圖層級(jí)過(guò)于復(fù)雜续镇,當(dāng)視圖呈現(xiàn)或者修改的時(shí)候美澳,計(jì)算圖層會(huì)消耗一部分時(shí)間。(UITableView的動(dòng)態(tài)計(jì)算cell高度)
解壓圖片
圖片繪制到屏幕上之前,必須把它擴(kuò)展成完整的未解壓的尺寸人柿。
圖片轉(zhuǎn)換
Session 419 WWDC 2014[3]中提到:“If an image is in a color format that the GPU can not directly work with, it will be converted in the CPU.”
也就是說(shuō)圖片的顏色格式不是32bit柴墩,那么CPU會(huì)先進(jìn)行顏色格式轉(zhuǎn)換,然后GPU才會(huì)進(jìn)行渲染凫岖。最好直接提供32bit顏色格式的圖片江咳,避免轉(zhuǎn)換,或者在非主線程中進(jìn)行格式轉(zhuǎn)換哥放。
可以通過(guò)Core Animation Instruments的Color Copied Images選項(xiàng)進(jìn)行圖片顏色格式檢測(cè)歼指。
繪制
使用CALayer進(jìn)行繪制:
實(shí)現(xiàn)了UIView的-drawRect:或者CALayerDelegate的-drawLayer:inContext:方法,為了支持對(duì)圖層內(nèi)容的任意繪制甥雕,Core Animation必須創(chuàng)建一個(gè)圖層寬*圖層高*4字節(jié)大小的寄宿圖踩身,寬高的單位均為像素。
CALayer的contents屬性就對(duì)應(yīng)于寄宿圖社露,寄宿圖是通過(guò)backing store來(lái)保存的挟阻。如果沒(méi)有實(shí)現(xiàn)-drawRect:方法,CALayer的contents為空的峭弟。(通過(guò)po CALayer會(huì)發(fā)現(xiàn)附鸽,實(shí)現(xiàn)了-drawRect:的CALayer的contents有內(nèi)容,反之則沒(méi)有瞒瘸。)
比如在iPhoneX的模擬器上創(chuàng)建一個(gè)沒(méi)有實(shí)現(xiàn)drawRect的5000*5000的視圖:
DrawRectView *drawRectView = [[DrawRectView alloc] initWithFrame:CGRectMake(0, 0, 5000, 5000)];
[self.view addSubview:drawRectView];
// 實(shí)現(xiàn)-drawRect:后的CALayer狀態(tài)
(lldb) po 0x604000239700
<CALayer:0x604000239700;
position = CGPoint (2500 2500);
bounds = CGRect (0 0; 5000 5000);
delegate = <DrawRectView: 0x7fb500410ea0;
frame = (0 0; 5000 5000);
layer = <CALayer: 0x604000239700>>;
contents = <CABackingStore 0x7fb500702100 (buffer [15000 15000] BGRX8888)>; opaque = YES;
allowsGroupOpacity = YES;
opacity = 1;
rasterizationScale = 3;
contentsScale = 3>
此時(shí)使用了內(nèi)存41M坷备;當(dāng)在DrawRectView中實(shí)現(xiàn)一個(gè)空的-drawRect:方法時(shí),此時(shí)內(nèi)存還是41M情臭;當(dāng)給drawRectView設(shè)置背景顏色后省撑,此時(shí)內(nèi)存暴漲到了899M。
使用CATileLayer進(jìn)行繪制:
在DrawRectView.m中保留-DrawRect:的同時(shí)加入如下代碼:
+ (Class)layerClass {
return [CATiledLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[(CATiledLayer *)self.layer setTileSize:CGSizeMake(100 * self.contentScaleFactor,
100 * self.contentScaleFactor)];
}
return self;
}
<CATiledLayer:0x6000004257c0;
position = CGPoint (2500 2500);
bounds = CGRect (0 0; 5000 5000);
delegate = <DrawRectView: 0x7fe31bf19e90;
frame = (0 0; 5000 5000);
layer = <CATiledLayer: 0x6000004257c0>>;
contents = <CAImageProvider 0x7fe31bf04940: 15000 x 15000>;
opaque = YES;
canDrawConcurrently = YES;
allowsGroupOpacity = YES;
opacity = 1;
tileSize = CGSize (300 300);
rasterizationScale = 3;
contentsScale = 3>
內(nèi)存使用率又會(huì)降低到41M俯在,CATiledLayer中沒(méi)有寄宿圖竟秫,contents部分是CAImageProvider。
使用CAShapeLayer進(jìn)行繪制:
1> 渲染快速跷乐。CAShapeLayer使用了硬件加速肥败,繪制同一圖形會(huì)比用Core Graphics快很多。
2> 高效使用內(nèi)存劈猿。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形,所以無(wú)論有多大潮孽,都不會(huì)占用太多的內(nèi)存揪荣。
3> 不會(huì)被圖層邊界剪裁掉。
4> 不會(huì)出現(xiàn)像素化往史。
一旦繪制結(jié)束之后仗颈,數(shù)據(jù)通過(guò)IPC傳到渲染服務(wù)。圖層每次重繪的時(shí)候都需要抹掉分配的內(nèi)存來(lái)重新分配,在此基礎(chǔ)上挨决,Core Graphics繪制就會(huì)變得十分緩慢请祖,所以提高繪制性能時(shí)需要盡量避免去繪制。
像素對(duì)齊
建議總是將layer對(duì)象的寬高設(shè)置成整數(shù)脖祈,盡管可以設(shè)置成浮點(diǎn)數(shù)肆捕,但是由于會(huì)根據(jù)layer的bounds來(lái)創(chuàng)建位圖圖片,Core Animation最終會(huì)將layer寬高轉(zhuǎn)換成整數(shù)[4]盖高。
Core Animation Instruments中的Color Misaligned Images選項(xiàng)會(huì)做出一些標(biāo)記慎陵。
洋紅色: UIView的frame像素不對(duì)齊,即不能換算成整數(shù)像素值喻奥。
黃色:UIImageView的圖片像素大小與其frame.size不對(duì)齊席纽,圖片發(fā)生了縮放。
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, 400.1000023, 100.222221110000001)];
label.text = @"{{100, 100}, {100.1000023, 400.222221110000001}}";
[self.view addSubview:label];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 250, 200, 300)];
imageView.image = [UIImage imageNamed:@"test.png"];
[self.view addSubview:imageView];
iPhoneX適配遇到的像素對(duì)齊問(wèn)題
如果是使用CATileLayer進(jìn)行繪制撞蚕,如果是水平方向等分的方式進(jìn)行繪制润梯,如下所示:
+ (Class)layerClass {
return [CATiledLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
CGFloat width = [UIScreen mainScreen].bounds.size.width / 2.0f;
[(CATiledLayer *)self.layer setTileSize:CGSizeMake(width * self.contentScaleFactor,
100 * self.contentScaleFactor)];
}
return self;
}
- (void)drawRect:(CGRect)rect {
NSLog(@"%@", NSStringFromCGRect(rect));
}
按照我們的期望,-drawRect:中打印的應(yīng)該是"{{0,0}, {187.5, 100}}和{{187.5,0},{187.5, 100}}"之類(lèi)的結(jié)果甥厦,但是真實(shí)結(jié)果卻是這樣的:
2018-01-25 11:12:34.508418+0800 {{187.33333333333331, 0}, {187.33333333333331, 100}}
2018-01-25 11:12:34.509249+0800 {{374.66666666666663, 300}, {187.33333333333337, 100}}
2018-01-25 11:12:34.509249+0800 {{187.33333333333331, 300}, {187.33333333333331, 100}}
2018-01-25 11:12:34.509921+0800 {{187.33333333333331, 400}, {187.33333333333331, 100}}
2018-01-25 11:12:34.509921+0800 {{374.66666666666663, 400}, {187.33333333333337, 100}}
2018-01-25 11:12:34.510676+0800 {{187.33333333333331, 500}, {187.33333333333331, 100}}
可以推測(cè)出纺铭,設(shè)置分塊圖片的寬度為375 / 2 * 3(PixelsPerPoint) = 562.5(像素)。分塊繪制的圖片在轉(zhuǎn)換成位圖時(shí)寬度轉(zhuǎn)換為整數(shù)變成562像素矫渔。在-drawRect:中參數(shù)rect對(duì)應(yīng)的分塊區(qū)域的寬度為:562 / 3 = 187.33333彤蔽,而不是375/2=187.5。
由于iPhoneX之前的機(jī)型水平分辨率都是偶數(shù)庙洼,所以水平均分分塊繪制不會(huì)出現(xiàn)問(wèn)題顿痪。但是iPhoneX的分辨率是1125*2436,水平方向的像素是奇數(shù)油够,所以可能會(huì)出現(xiàn)一些奇怪的現(xiàn)象蚁袭。所以涉及到像素操作的代碼要確保最后得到的像素單位是整數(shù)。
影響GPU使用率的操作
通過(guò)Instruments GPU Driver查看GPU使用率:
圖層混合
layer的混合涉及到顏色的計(jì)算石咬,兩個(gè)layer混合后每個(gè)混合后的像素顏色計(jì)算公式為:R = S + D * (1 - Sa)揩悄,(Source(top),Destination(lower))鬼悠。如果Source(top)是不透明的删性,那么R = S。
如果CALayer上的opaque屬性為YES焕窝,那么該layer就是不透明蹬挺,GPU不會(huì)做任何合成,只是簡(jiǎn)單的層拷貝它掂。CALayer上opaque的默認(rèn)值是NO巴帮,UIView的alpha默認(rèn)為1。
修改opaque屬性只是會(huì)修改Core Animation的backing store,如果CALayer的contents屬性是一張帶有alpha通道的圖片的話榕茧,圖片仍然會(huì)保留其alpha通道而忽略掉opaque屬性的值[CALayer文檔]垃沦。比如UIImageView雖然有CALayer,但是該圖層并沒(méi)有backing store用押,而是使用一個(gè)CGImageRef作為它的內(nèi)容肢簿,渲染服務(wù)會(huì)把圖片的數(shù)據(jù)繪制到幀緩沖區(qū)[2]。
通過(guò)開(kāi)啟Core Animation Instruments的Color Blended Layers選項(xiàng)來(lái)檢測(cè)圖層混合只恨,發(fā)生圖層混合會(huì)顯示紅色译仗。
UILabel在使用時(shí)避免圖層混合的設(shè)置方法:
self.likeLabel.layer.masksToBounds = YES;
self.likeLabel.backgroundColor = SNBThemeColor(SNB_BLK_LV9_COL, YES);
離屏渲染
GPU的屏幕渲染方式有兩種:
1> On-Screen Rendering即當(dāng)前屏幕渲染,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行官觅。
2> Off-Screen Rendering即離屏渲染纵菌,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開(kāi)辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。
離屏渲染的代價(jià):
1> 創(chuàng)建新的緩沖區(qū)休涤。
2> 上下文切換咱圆。離屏渲染的過(guò)程中,會(huì)發(fā)生上下文:從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen)功氨;等到離屏渲染結(jié)束以后序苏,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上,又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕捷凄。
為什么需要離屏渲染忱详?
一般情況下,OpenGL會(huì)將提交到渲染服務(wù)(Render Server)的動(dòng)畫(huà)直接渲染跺涤,但是對(duì)于一些復(fù)雜的圖像動(dòng)畫(huà)不能直接進(jìn)行疊加渲染顯示匈睁,而是需要根據(jù)Command Buffer分通道進(jìn)行渲染之后再組合,在組合過(guò)程中桶错,有些渲染通道不會(huì)直接顯示航唆,而這些沒(méi)有直接顯示在屏幕上的通道就是Offscreen Render Pass[3][6]。
Offscreen Render需要更多的渲染通道院刁,而不同的渲染通道切換需要耗費(fèi)一定的時(shí)間糯钙,這個(gè)時(shí)間內(nèi)GPU會(huì)閑置,當(dāng)通道達(dá)到一定數(shù)量退腥,對(duì)性能會(huì)有較大的影響任岸。
比如,UIBlurEffect的GPU渲染過(guò)程[3]:
UIBlurEffect需要5個(gè)通道才能合成最終的效果圖狡刘,每一個(gè)通道需要上一個(gè)通道的輸出作為輸入享潜。從“通道切換GPU的閑置”這張圖能夠看到,在16.67ms內(nèi)颓帝,Render的紅色部分分成5塊米碰,對(duì)應(yīng)著5個(gè)通道,由于第一個(gè)和最后一個(gè)通道對(duì)應(yīng)著全尺寸的圖片购城,所以這兩個(gè)通道處理的時(shí)間比其他3個(gè)要多一些吕座,反映在圖上也就是寬一些。5個(gè)紅色Bar中的4個(gè)橙色bar是在進(jìn)行渲染通道的切換瘪板,此時(shí)GPU處于閑置狀態(tài)吴趴。
使用shouldRasterize強(qiáng)制觸發(fā)離屏渲染:
將CALayer的shouldRasterize設(shè)置為YES,會(huì)把CALayer對(duì)應(yīng)的位圖放入緩存中侮攀。
什么情況下適合圖層?xùn)鸥窕?br>
1> 當(dāng)CALayer的內(nèi)容是靜態(tài)的锣枝,也就是CALayer內(nèi)容不會(huì)發(fā)生變化。
2> 圖層結(jié)構(gòu)比較復(fù)雜兰英。
3> 使用該圖層的地方比較多撇叁,存放進(jìn)緩存中的位圖可以多次命中。
參考文獻(xiàn)
[1]: iOS核心動(dòng)畫(huà)高級(jí)技巧
[2]: 繪制像素到屏幕上
[3]: Advanced Graphics and Animations for iOS Apps
[4]: Improving Animation Performance
[5]: 內(nèi)存惡鬼drawRect
[6]: 深刻理解移動(dòng)端優(yōu)化之離屏渲染