前兩天QQ群大佬提出一個問題雌澄,如何解決CALayer的contents屬性賦值之后的內(nèi)存暴增問題斋泄,百度了一下無頭緒,偶然看到了唐巧的文章镐牺,感覺很有意思炫掐,特此記錄一下。原文在此:內(nèi)存惡鬼drawRect - 談畫圖功能的內(nèi)存優(yōu)化
正文:
先來說一下contens
是個啥東西:CoreAnimation:CALayer的contents
唐巧文章里寫的很清楚了睬涧,在這里不再過多的贅述募胃,大概是這樣的:
- 搞了一個簡易功能的畫板,記錄手指觸摸的軌跡然后繪制在屏幕上畦浓。
- 發(fā)現(xiàn)兩個問題:當畫板彈出痹束,其余無任何操作時,內(nèi)存激增讶请,然后當手指繪制開始時祷嘶,內(nèi)存又激增。
- 分析原因可能有兩個:一是在手指繪制的過程中創(chuàng)建的大量點對象沒有及時釋放或者其他資源沒有及時釋放夺溢。這一點因為工程為
ARC
以Instruments
工具得以排除论巍。二是系統(tǒng)在繪制過程中開始大量消耗內(nèi)存,但是明顯可以發(fā)現(xiàn)是畫板創(chuàng)建之后就會有內(nèi)存激增风响,所以矛頭直指drawRect
环壤。
這是畫板繪制功能的一段代碼:
- (void)drawRect:(CGRect)rect
{
if (!self.paths.count) return;
CGContextRef ctx = UIGraphicsGetCurrentContext();
for (BHBPaintPath *path in self.paths) {
CGContextSaveGState(ctx);
[[UIColor blackColor] set];
[path stroke]; // 關(guān)鍵的一步繪制
CGContextRestoreGState(ctx);
}
}
- 分析為何
drawRect
會出現(xiàn)內(nèi)存暴增的真正原因:
簡單來說,咱們所看到的屏幕上的東西钞诡,其實都是一張圖片,是由CALayer
的contents
屬性掌管著湃崩,最終圖形渲染落點落在了contents
身上荧降。 -
contents
也被稱為寄宿圖,除了給它賦值CGImage
以外攒读,我們也可以直接對它進行繪制朵诫,繪制的方法正式此次問題的關(guān)鍵,通過集成UIView
并實現(xiàn)-drawRect:
方法即可自定義繪制薄扁。-drawRect:
方法沒有默認的實現(xiàn)剪返,因此對UIView
來說废累,寄宿圖并不是必須的,UIView
不關(guān)心繪制的內(nèi)容脱盲。如果UIView
檢測到-drawRect:
方法被調(diào)用了邑滨,他就會為視圖分配一個寄宿圖,這個寄宿圖的像素尺寸等于視圖大小乘以contentsScale
(這個屬性與屏幕分辨率有關(guān)钱反,我們的畫板程序在不同模擬器下呈現(xiàn)的內(nèi)存用量不同也是因為它)的值掖看。
那么我們重回畫板程序,當畫板從屏幕上出現(xiàn)的時候面哥,因為重寫了-drawRect:
方法哎壳,-drawRect:
方法就會自動調(diào)用。生成一張寄宿圖后尚卫,方法里面的代碼利用Core Graphics
去繪制n條黑色的線归榕,然后 內(nèi)容就會緩存起來,等待下次你調(diào)用-setNeedsDisplay
時再進行更新吱涉。
畫板視圖的-drawRect:
方法的背后實際上都是底層的CALayer
進行了重繪和保存中間產(chǎn)生的圖片刹泄,CALayer
的delegate
屬性默認實現(xiàn)了CALyaerDelegate
協(xié)議,當它需要內(nèi)容信息的時候回調(diào)用協(xié)議中的方法來拿邑飒。當畫板重繪時循签,因為他的支持圖層CALayer
的代理就是畫板視圖的本身,所以支持圖層會請求畫板視圖給它一個寄宿圖來顯示疙咸,它此刻會調(diào)用:
- (void)displayLayer:(CALayer *)layer;
如果畫板試圖實現(xiàn)了這個方法县匠,就可以拿到layer
來直接設(shè)置contents
寄宿圖,如果這個方法沒有實現(xiàn)撒轮,支持圖層CALayer
會嘗試調(diào)用:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
這個方法調(diào)用之前乞旦,CALayer
創(chuàng)建了一個合適尺寸的空寄宿圖(尺寸由bounds
和contentsScale
決定)和一個Core Graphics
的繪制山下文環(huán)境,為繪制寄宿圖做準備题山,它作為ctx
參數(shù)傳入兰粉。在這一步生成的空寄宿圖內(nèi)存是相當巨大的,它就是本次內(nèi)存問題的關(guān)鍵顶瞳,一旦你實現(xiàn)了CALayerDelegate
協(xié)議中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其實就是前者的包裝方法)玖姑,圖層就創(chuàng)建了一個繪制上下文,這個上下文需要的內(nèi)存可從這個公式得出:圖層寬圖層高4字節(jié)慨菱,寬高的單位均為像素焰络。所以圖層在每次繪制的時候都需要重新抹掉內(nèi)存然后重新分配。它就是我們畫板程序內(nèi)存暴增的真正原因符喝。
- 解決方法
處理類似于畫板這樣畫線條的需求直接用專用圖層CAShapeLayer
闪彼。
來看看這是個什么東西:
CAShapeLayer
是一個通過矢量圖形而不是bitmap
來繪制的圖層子類。用CGPath
來定義想要繪制的圖形协饲,CAShapeLayer
會自動渲染畏腕。他可以完美替代我們直接使用Core Graphics
繪制layer
缴川,相比之下使用CAShapeLayer
有以下優(yōu)點:
- 渲染快速。
CAShapeLayer
使用了硬件加速描馅,繪制同一圖形會比用Core Graphics
快很多把夸。 - 高效使用內(nèi)存。一個
CAShapeLayer
不需要像普通CALayer
一樣創(chuàng)建一個寄宿圖形流昏,所以無論有多大扎即,都不會占用太多的內(nèi)存。 - 不會被圖形邊界剪裁掉况凉。
- 不會出現(xiàn)像素化谚鄙。
- 總結(jié)一下繪制性能優(yōu)化原則:
- 繪制圖形性能的優(yōu)化最好的辦法就是不去繪制。
- 利用專有圖層代替繪圖需求刁绒。
- 不得不用到的繪圖盡量縮小試圖面積闷营,并且盡量降低重繪頻率。
- 異步繪制知市,推測內(nèi)容傻盟,提前在其他線程繪制圖片,在主線程中直接設(shè)置圖片嫂丙。
- 最后附上畫板-demo