優(yōu)化drawRect占用內(nèi)存的問(wèn)題----以繪畫板為例

【原文標(biāo)題】?jī)?nèi)存惡鬼drawRect

該文章轉(zhuǎn)載自畢洪博的博客:

原文?

(注:續(xù)篇內(nèi)容從屏幕大小的角度來(lái)解釋了內(nèi)存增加的問(wèn)題,但遠(yuǎn)沒(méi)有第一篇的分析精彩,若有興趣可以看看:續(xù)篇

標(biāo)題有點(diǎn)嚇人,但是對(duì)于drawRect的評(píng)價(jià)倒是一點(diǎn)都不過(guò)分。在平日的開發(fā)中,隨意覆蓋drawRect方法,稍有不慎就會(huì)讓你的程序內(nèi)存暴增。下面我們來(lái)看一個(gè)例子。

去年的某天午后,北京的霧霾依舊像現(xiàn)在這樣醇厚,我的同事輝哥像往常一樣與我樓下約煙。我見(jiàn)輝哥表情凝重,便詢問(wèn)究竟。輝哥做了一個(gè)畫板功能,但是苦于內(nèi)存問(wèn)題一直得不到解決。畫板功能很簡(jiǎn)單,就是記錄手指觸摸的軌跡然后繪制在屏幕上。下面我們來(lái)看一張效果圖:

效果圖

出現(xiàn)內(nèi)存暴漲的問(wèn)題

如圖我們看到左側(cè)內(nèi)存的狀況隨著手指的繪制逐漸惡化。另外細(xì)心的同學(xué)可以觀察到,點(diǎn)擊圖中藍(lán)色矩形按鈕之后,便會(huì)彈出畫板,而這時(shí)并沒(méi)有進(jìn)行任何的手指繪制,內(nèi)存就突變?yōu)?14MB,然后每當(dāng)手指繪制開始時(shí),內(nèi)存立即增加到300MB左右穩(wěn)定下來(lái)。對(duì)于正常的iOS App來(lái)講,這么大的內(nèi)存消耗是不能容忍的。

下面分析一下原因:

可能的原因有兩個(gè),一是在手指繪制的過(guò)程中創(chuàng)建的大量點(diǎn)對(duì)象沒(méi)有及時(shí)釋放或者其他資源沒(méi)有及時(shí)釋放。

二是系統(tǒng)在繪制的過(guò)程中開始大量消耗內(nèi)存。

第一個(gè)原因,手指繪制的過(guò)程中創(chuàng)建的大量點(diǎn)對(duì)象沒(méi)有及時(shí)釋放或者其他資源沒(méi)有及時(shí)釋放。這一點(diǎn)我們暫時(shí)排除以節(jié)省時(shí)間,因?yàn)檫@個(gè)畫板功能工程是用ARC寫的,并且我們已經(jīng)做過(guò)代碼檢查和使用Instruments工具來(lái)檢測(cè)內(nèi)存使用情況,這里并沒(méi)有所謂的對(duì)象沒(méi)有及時(shí)釋放的問(wèn)題存在。

第二個(gè)原因,系統(tǒng)在繪制的過(guò)程中開始大量消耗內(nèi)存。首先我們?cè)?jīng)注意到一個(gè)詭異并且不尋常的事情就是,當(dāng)黃色的畫板剛剛彈出的時(shí)候內(nèi)存就瞬間從18MB暴增至114MB。這一點(diǎn)更加說(shuō)明第一個(gè)原因不是問(wèn)題所在,因?yàn)檫@時(shí)手指還沒(méi)有進(jìn)行任何繪制,也就是說(shuō)不存在任何點(diǎn)與線的對(duì)象,那么內(nèi)存怎么會(huì)暴增呢?

這時(shí)我們要考慮這個(gè)畫板功能是如何實(shí)現(xiàn)的,畫板分為兩步,第一步記錄用戶手指的軌跡,這一步會(huì)生成大量點(diǎn)的對(duì)象(已排除嫌疑)。第二步繪制到視圖或者圖層上,我們平常使用頻繁的繪圖方式基本上是Quarz2D的那套C語(yǔ)言框架,而繪制代碼所在的地點(diǎn)在哪呢?我們今天的主角終于上場(chǎng)了--drawRect。

下面我們來(lái)看一段畫板功能繪制的代碼:

- (void)drawRect:(CGRect)rect{

if(!self.paths.count)return;

CGContextRefctx =UIGraphicsGetCurrentContext();

for(BHBPaintPath *pathinself.paths) {

CGContextSaveGState(ctx);

[[UIColorblackColor] set];

[path stroke];//關(guān)鍵的一步繪制

CGContextRestoreGState(ctx);}

}

去掉繪圖上下文棧和其余判斷邊界的代碼,我們只是在當(dāng)前view上繪制了n條黑色的線。看起來(lái)普普通通的繪圖方式,怎么會(huì)導(dǎo)致內(nèi)存的劇增呢?我們現(xiàn)在說(shuō)罪魁禍?zhǔn)资莇rawRect證據(jù)并不充分。我們回想畫板剛彈出時(shí)的內(nèi)存狀況,接下來(lái)我們注釋掉drawRect所有的代碼。運(yùn)行的效果圖如下:

內(nèi)存沒(méi)有顯著增長(zhǎng)

效果立竿見(jiàn)影,注釋掉drawRect之后,內(nèi)存立刻恢復(fù)正常,我們終于抓到了消耗內(nèi)存的惡鬼,問(wèn)題就出在對(duì)drawRect方法的覆蓋。那么抓到了犯人,本文是否應(yīng)該完結(jié)了?非也非也,我們雖說(shuō)知道了內(nèi)存暴增的原因,但是我們并沒(méi)有深入的去分析drawRect為什么對(duì)內(nèi)存的影響這么大,而且我們也沒(méi)有給出問(wèn)題的解決方案。請(qǐng)接著往下看。

那么現(xiàn)在我們分析一下drawRect導(dǎo)致內(nèi)存暴增的真正原因:

重寫drawRect為何會(huì)導(dǎo)致內(nèi)存大量上漲?

要想搞明白這個(gè)問(wèn)題,我們需要擼一擼在iOS程序上圖形顯示的原理。在iOS系統(tǒng)中所有顯示的視圖都是從基類UIView繼承而來(lái)的,同時(shí)UIView負(fù)責(zé)接收用戶交互。但是實(shí)際上你所看到的視圖內(nèi)容,包括圖形等,都是由UIView的一個(gè)實(shí)例圖層屬性來(lái)繪制和渲染的,那就是CALayer。

CALayer類的概念與UIView非常類似,它也具有樹形的層級(jí)關(guān)系,并且可以包含圖片文本、背景色等。它與UIView最大的不同在于它不能響應(yīng)用戶交互,可以說(shuō)它根本就不知道響應(yīng)鏈的存在,它的API雖然提供了“某點(diǎn)是否在圖層范圍內(nèi)的方法”,但是它并不具有響應(yīng)的能力。

在每一個(gè)UIView實(shí)例當(dāng)中,都有一個(gè)默認(rèn)的支持圖層,UIView負(fù)責(zé)創(chuàng)建并且管理這個(gè)圖層。實(shí)際上這個(gè)CALayer圖層才是真正用來(lái)在屏幕上顯示的,UIView僅僅是對(duì)它的一層封裝,實(shí)現(xiàn)了CALayer的delegate,提供了處理事件交互的具體功能,還有動(dòng)畫底層方法的高級(jí)API。

可以說(shuō)CALayer是UIView的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。

腦補(bǔ)了這么多,它與今天的主題drawRect有何關(guān)系呢?別著急,我們既然已經(jīng)確定CALayer才是最終顯示到屏幕上的,只要順藤摸瓜,即可分析清楚。CALayer其實(shí)也只是iOS當(dāng)中一個(gè)普通的類,它也并不能直接渲染到屏幕上,因?yàn)槠聊簧夏闼吹降臇|西,其實(shí)都是一張張圖片。而為什么我們能看到CALayer的內(nèi)容呢,是因?yàn)镃ALayer內(nèi)部有一個(gè)contents屬性。contents默認(rèn)可以傳一個(gè)id類型的對(duì)象,但是只有你傳CGImage的時(shí)候,它才能夠正常顯示在屏幕上。所以最終我們的圖形渲染落點(diǎn)落在contents身上如圖。

圖像顯示分析

contents也被稱為寄宿圖,除了給它賦值CGImage之外,我們也可以直接對(duì)它進(jìn)行繪制,繪制的方法正是這次問(wèn)題的關(guān)鍵,通過(guò)繼承UIView并實(shí)現(xiàn)-drawRect:方法即可自定義繪制。-drawRect:方法沒(méi)有默認(rèn)的實(shí)現(xiàn),因?yàn)閷?duì)UIView來(lái)說(shuō),寄宿圖并不是必須的,UIView不關(guān)心繪制的內(nèi)容。如果UIView檢測(cè)到-drawRect:方法被調(diào)用了,它就會(huì)為視圖分配一個(gè)寄宿圖,這個(gè)寄宿圖的像素尺寸等于視圖大小乘以contentsScale(這個(gè)屬性與屏幕分辨率有關(guān),我們的畫板程序在不同模擬器下呈現(xiàn)的內(nèi)存用量不同也是因?yàn)樗?的值。

那么回到我們的畫板程序,當(dāng)畫板從屏幕上出現(xiàn)的時(shí)候,因?yàn)橹貙懥?drawRect:方法,-drawRect :方法就會(huì)自動(dòng)調(diào)用。生成一張寄宿圖后,方法里面的代碼利用Core Graphics去繪制n條黑色的線,然后內(nèi)容就會(huì)緩存起來(lái),等待下次你調(diào)用-setNeedsDisplay時(shí)再進(jìn)行更新。

畫板視圖的-drawRect:方法的背后實(shí)際上都是底層的CALayer進(jìn)行了重繪和保存中間產(chǎn)生的圖片,CALayer的delegate屬性默認(rèn)實(shí)現(xiàn)了CALayerDelegate協(xié)議,當(dāng)它需要內(nèi)容信息的時(shí)候會(huì)調(diào)用協(xié)議中的方法來(lái)拿。當(dāng)畫板視圖重繪時(shí),因?yàn)樗闹С謭D層CALayer的代理就是畫板視圖本身,所以支持圖層會(huì)請(qǐng)求畫板視圖給它一個(gè)寄宿圖來(lái)顯示,它此刻會(huì)調(diào)用:

- (void)displayLayer:(CALayer*)layer;

如果畫板視圖實(shí)現(xiàn)了這個(gè)方法,就可以拿到layer來(lái)直接設(shè)置contents寄宿圖,如果這個(gè)方法沒(méi)有實(shí)現(xiàn),支持圖層CALayer會(huì)嘗試調(diào)用:

- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx;

這個(gè)方法調(diào)用之前,CALayer創(chuàng)建了一個(gè)合適尺寸的空寄宿圖(尺寸由bounds和contentsScale決定)和一個(gè)Core Graphics的繪制上下文環(huán)境,為繪制寄宿圖做準(zhǔn)備,它作為ctx參數(shù)傳入。在這一步生成的空寄宿圖內(nèi)存是相當(dāng)巨大的,它就是本次內(nèi)存問(wèn)題的關(guān)鍵,一旦你實(shí)現(xiàn)了CALayerDelegate協(xié)議中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其實(shí)就是前者的包裝方法),圖層就創(chuàng)建了一個(gè)繪制上下文,這個(gè)上下文需要的內(nèi)存可從這個(gè)公式得出:圖層寬*圖層高*4字節(jié),寬高的單位均為像素。而我們的畫板程序因?yàn)橐С窒裨愁}庫(kù)一樣兩指挪動(dòng)的效果,我們開辟的畫板大小為:

_myDrawer = [[BHBMyDrawer alloc] initWithFrame:CGRectMake(0,0, SCREEN_SIZE.width*5, SCREEN_SIZE.height*2)];

我們的畫板程序的畫板視圖它在iPhone6s plus機(jī)器上的上下文內(nèi)存量就是1920*2*1080*5*4字節(jié),相當(dāng)于79MB內(nèi)存,圖層每次重繪的時(shí)候都需要重新抹掉內(nèi)存然后重新分配。它就是我們畫板程序內(nèi)存暴增的真正原因。

最終我們將內(nèi)存暴增的原因找出來(lái)了,那么我們有沒(méi)有合理的解決方案呢?

我認(rèn)為最合理的辦法處理類似于畫板這樣畫線條的需求直接用專有圖層CAShapeLayer。讓我們看看它是什么:

CAShapeLayer是一個(gè)通過(guò)矢量圖形而不是bitmap來(lái)繪制的圖層子類。用CGPath來(lái)定義想要繪制的圖形,CAShapeLayer會(huì)自動(dòng)渲染。它可以完美替代我們的直接使用Core Graphics繪制layer,對(duì)比之下使用CAShapeLayer有以下優(yōu)點(diǎn):

渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會(huì)比用Core Graphics快很多。

高效使用內(nèi)存。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形,所以無(wú)論有多大,都不會(huì)占用太多的內(nèi)存。

不會(huì)被圖層邊界剪裁掉。

不會(huì)出現(xiàn)像素化。

所以最終我們的畫板程序使用CAShapeLayer來(lái)實(shí)現(xiàn)線條的繪制,性能非常穩(wěn)定,效果圖如下:

最終的解決方案

總結(jié)一下繪制性能優(yōu)化原則:

1.繪制圖形性能的優(yōu)化最好的辦法就是不去繪制。

2.利用專有圖層代替繪圖需求。

3.不得不用到繪圖盡量縮小視圖面積,并且盡量降低重繪頻率。

4.異步繪制,推測(cè)內(nèi)容,提前在其他線程繪制圖片,在主線程中直接設(shè)置圖片。

本文最后一個(gè)效果圖為仿寫猿題庫(kù)練題畫板功能,demo請(qǐng)?jiān)趃ithub搜索BHBDrawBoarderDemo。或者直接戳這里

參考:

iOS Core Animation: Advanced Technique

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容