內(nèi)存惡鬼drawRect - 談畫(huà)圖功能的內(nèi)存優(yōu)化

http://blog.csdn.net/jijiji000111/article/details/50480405


感謝原作者:http://mp.weixin.qq.com/s?__biz=MjM5NTIyNTUyMQ==&mid=447105405&idx=1&sn=054dc54289a98e8a39f2b9386f4f620e&scene=23&srcid=0108RhyzhXk9wUwQvnW3cmZT#rd

作者介紹

作者:畢洪博 ( @畢洪博 )扣囊,iOS 開(kāi)發(fā)者,pop Art 追隨者≌寥洌現(xiàn)在正在鼓搗 AVFoundation恒界,博客 bihongbo.com, 歡迎大家找我討論技術(shù)路操。

作者已將本文在微信公眾平臺(tái)的發(fā)表權(quán)「獨(dú)家代理」給 iOS 開(kāi)發(fā)微信公共號(hào)惠奸,本文的打賞歸畢洪博所有占拍,以下是文章正文勃教。

正文

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

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

如圖我們看到左側(cè)內(nèi)存的狀況隨著手指的繪制逐漸惡化炬丸。另外細(xì)心的同學(xué)可以觀察到瘫寝,點(diǎn)擊圖中藍(lán)色矩形按鈕之后,便會(huì)彈出畫(huà)板,而這時(shí)并沒(méi)有進(jìn)行任何的手指繪制矢沿,內(nèi)存就突變?yōu)?114 MB ,然后每當(dāng)手指繪制開(kāi)始時(shí)酸纲,內(nèi)存立即增加到 300 MB 左右穩(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ò)程中開(kāi)始大量消耗內(nèi)存疾嗅。

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

第二個(gè)原因掖棉,系統(tǒng)在繪制的過(guò)程中開(kāi)始大量消耗內(nèi)存。首先我們?cè)?jīng)注意到一個(gè)詭異并且不尋常的事情就是膀估,當(dāng)黃色的畫(huà)板剛剛彈出的時(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è)畫(huà)板功能是如何實(shí)現(xiàn)的饼记,畫(huà)板分為兩步香伴,第一步記錄用戶手指的軌跡,這一步會(huì)生成大量點(diǎn)的對(duì)象(已排除嫌疑)具则。第二步繪制到視圖或者圖層上瞒窒,我們平常使用頻繁的繪圖方式基本上是 Quarz2D 的那套 C 語(yǔ)言框架,而繪制代碼所在的地點(diǎn)在哪呢乡洼?我們今天的主角終于上場(chǎng)了--drawRect崇裁。

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

- (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);

}

}

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

效果立竿見(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)存暴增的真正原因:

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

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

CALayer類(lèi)的概念與UIView非常類(lèi)似,它也具有樹(shù)形的層級(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)畫(huà)底層方法的高級(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è)普通的類(lèi)细疚,它也并不能直接渲染到屏幕上蔗彤,因?yàn)槠聊簧夏闼吹降臇|西,其實(shí)都是一張張圖片惠昔。而為什么我們能看到CALayer的內(nèi)容呢幕与,是因?yàn)镃ALayer內(nèi)部有一個(gè)contents屬性挑势。contents默認(rèn)可以傳一個(gè)id類(lèi)型的對(duì)象镇防,但是只有你傳CGImage的時(shí)候,它才能夠正常顯示在屏幕上潮饱。所以最終我們的圖形渲染落點(diǎn)落在contents身上如圖来氧。

contents也被稱(chēng)為寄宿圖,除了給它賦值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)换帜,我們的畫(huà)板程序在不同模擬器下呈現(xiàn)的內(nèi)存用量不同也是因?yàn)樗? 的值。

那么回到我們的畫(huà)板程序鹤啡,當(dāng)畫(huà)板從屏幕上出現(xiàn)的時(shí)候惯驼,因?yàn)橹貙?xiě)了-drawRect:方法,-drawRect

:方法就會(huì)自動(dòng)調(diào)用递瑰。生成一張寄宿圖后祟牲,方法里面的代碼利用Core

Graphics去繪制 n 條黑色的線,然后內(nèi)容就會(huì)緩存起來(lái)抖部,等待下次你調(diào)用-setNeedsDisplay時(shí)再進(jìn)行更新疲眷。

畫(huà)板視圖的-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)畫(huà)板視圖重繪時(shí)换淆,因?yàn)樗闹С謭D層CALayer的代理就是畫(huà)板視圖本身,所以支持圖層會(huì)請(qǐng)求畫(huà)板視圖給它一個(gè)寄宿圖來(lái)顯示几颜,它此刻會(huì)調(diào)用:

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

如果畫(huà)板視圖實(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é)跷叉,寬高的單位均為像素逸雹。而我們的畫(huà)板程序因?yàn)橐С窒裨愁}庫(kù)一樣兩指挪動(dòng)的效果,我們開(kāi)辟的畫(huà)板大小為:

_myDrawer = [[BHBMyDrawer alloc] initWithFrame:

CGRectMake(0, 0, SCREEN_SIZE.width*5, SCREEN_SIZE.height*2)];

我們的畫(huà)板程序的畫(huà)板視圖它在iPhone6s

plus機(jī)器上的上下文內(nèi)存量就是1920*2*1080*5*4

字節(jié)云挟,相當(dāng)于79MB內(nèi)存梆砸,圖層每次重繪的時(shí)候都需要重新抹掉內(nèi)存然后重新分配。它就是我們畫(huà)板程序內(nèi)存暴增的真正原因园欣。

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

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

CAShapeLayer是一個(gè)通過(guò)矢量圖形而不是bitmap來(lái)繪制的圖層子類(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)像素化敦冬。

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

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

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

利用專(zhuān)有圖層代替繪圖需求堪遂。

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

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

本文最后一個(gè)效果圖為仿寫(xiě)猿題庫(kù)練題畫(huà)板功能,demo請(qǐng)?jiān)趃ithub搜索BHBDrawBoarderDemo巍虫。

或者直接戳這里:https://github.com/bb-coder/BHBDrawBoarderDemo彭则。

好了,就是這么多占遥,如有紕漏請(qǐng)不吝指出俯抖!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市筷频,隨后出現(xiàn)的幾起案子蚌成,更是在濱河造成了極大的恐慌前痘,老刑警劉巖凛捏,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異芹缔,居然都是意外死亡坯癣,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)最欠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)示罗,“玉大人,你說(shuō)我怎么就攤上這事芝硬⊙恋悖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵拌阴,是天一觀的道長(zhǎng)绍绘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)迟赃,這世上最難降的妖魔是什么陪拘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮纤壁,結(jié)果婚禮上左刽,老公的妹妹穿的比我還像新娘。我一直安慰自己酌媒,他們只是感情好欠痴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布迄靠。 她就那樣靜靜地躺著,像睡著了一般喇辽。 火紅的嫁衣襯著肌膚如雪梨水。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,482評(píng)論 1 302
  • 那天茵臭,我揣著相機(jī)與錄音疫诽,去河邊找鬼。 笑死旦委,一個(gè)胖子當(dāng)著我的面吹牛奇徒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缨硝,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼摩钙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了查辩?” 一聲冷哼從身側(cè)響起胖笛,我...
    開(kāi)封第一講書(shū)人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宜岛,沒(méi)想到半個(gè)月后长踊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萍倡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年身弊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片列敲。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阱佛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出戴而,到底是詐尸還是另有隱情凑术,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布所意,位于F島的核電站淮逊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扁眯。R本人自食惡果不足惜壮莹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望姻檀。 院中可真熱鬧命满,春花似錦、人聲如沸绣版。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至诈唬,卻和暖如春韩脏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铸磅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工赡矢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阅仔。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓吹散,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親八酒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子空民,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

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