線圖繪制性能分析

簡(jiǎn)介

iOS中撵割,軟件繪圖通常是由Core Graphics框架完成來(lái)完成弄诲。但是菱鸥,在一些必要的情況下澎蛛,相比Core AnimationOpenGL抚垄,Core Graphics要慢了不少。

軟件繪圖不僅效率低谋逻,還會(huì)消耗可觀的內(nèi)存呆馁。CALayer只需要一些與自己相關(guān)的內(nèi)存:它的寄宿圖會(huì)消耗一定的內(nèi)存空間。

但是一旦你實(shí)現(xiàn)了CALayerDelegate協(xié)議中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其實(shí)就是前者的包裝方法)毁兆,圖層就創(chuàng)建了一個(gè)繪制上下文浙滤,這個(gè)上下文需要的大小的內(nèi)存可從這個(gè)算式得出:圖層寬圖層高4字節(jié),寬高的單位均為像素气堕。對(duì)于一個(gè)在Retina iPad上的全屏圖層來(lái)說(shuō)纺腊,這個(gè)內(nèi)存量就是 204815264字節(jié),相當(dāng)于12MB內(nèi)存茎芭,圖層每次重繪的時(shí)候都需要重新抹掉內(nèi)存然后重新分配揖膜。
軟件繪圖的代價(jià)昂貴,除非絕對(duì)必要梅桩,你應(yīng)該避免重繪你的視圖壹粟。提高繪制性能的秘訣就在于盡量避免去繪制。

繪制方法對(duì)比與選擇

Core Graphics繪制 - 如果對(duì)視圖實(shí)現(xiàn)了-drawRect:方法宿百,或者CALayerDelegate的-drawLayer:inContext:方法趁仙,那么在繪制任何東西之前都會(huì)產(chǎn)生一個(gè)巨大的性能開(kāi)銷。為了支持對(duì)圖層內(nèi)容的任意繪制垦页,Core Animation必須創(chuàng)建一個(gè)內(nèi)存中等大小的寄宿圖片雀费。然后一旦繪制結(jié)束之后,必須把圖片數(shù)據(jù)通過(guò)IPC傳到渲染服務(wù)器外臂。在此基礎(chǔ)上坐儿,Core Graphics繪制就會(huì)變得十分緩慢律胀,所以在一個(gè)對(duì)性能十分挑剔的場(chǎng)景下這樣做十分不好宋光。

圖層對(duì)比與選擇

CAShapeLayer

CAShapeLayer是一個(gè)通過(guò)矢量圖形而不是bitmap來(lái)繪制的圖層子類。你指定諸如顏色和線寬等屬性炭菌,用CGPath來(lái)定義想要繪制的圖形罪佳,最后CAShapeLayer就自動(dòng)渲染出來(lái)了。當(dāng)然黑低,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個(gè)路徑赘艳,相比直下酌毡,使用CAShapeLayer有以下一些優(yōu)點(diǎn):

  • 渲染快速。CAShapeLayer使用了硬件加速蕾管,繪制同一圖形會(huì)比用Core Graphics快很多枷踏。
  • 高效使用內(nèi)存。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形掰曾,所以無(wú)論有多大旭蠕,都不會(huì)占用太多的內(nèi)存。
  • 不會(huì)被圖層邊界剪裁掉旷坦。一個(gè)CAShapeLayer可以在邊界之外繪制掏熬。你的圖層路徑不會(huì)像在使用Core Graphics的普通CALayer一樣被剪裁掉。
  • 不會(huì)出現(xiàn)像素化秒梅。當(dāng)你給CAShapeLayer做3D變換時(shí)旗芬,它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化。
CAShapeLayer應(yīng)用對(duì)比

Core Graphics做一個(gè)簡(jiǎn)單的『素描』 這樣實(shí)現(xiàn)的問(wèn)題在于捆蜀,我們畫(huà)得越多疮丛,程序就會(huì)越慢。因?yàn)槊看我苿?dòng)手指的時(shí)候都會(huì)重繪整個(gè)貝塞爾路徑(UIBezierPath)漱办,隨著路徑越來(lái)越復(fù)雜这刷,每次重繪的工作就會(huì)增加,直接導(dǎo)致了幀數(shù)的下降娩井∠疚荩看來(lái)我們需要一個(gè)更好的方法了。
Core Animation為這些圖形類型的繪制提供了專門的類洞辣,并給他們提供硬件支持咐刨。CAShapeLayer可以繪制多邊形,直線和曲線扬霜。CATextLayer可以繪制文本定鸟。CAGradientLayer用來(lái)繪制漸變。
這些總體上都比Core Graphics更快著瓶,同時(shí)他們也避免了創(chuàng)造一個(gè)寄宿圖联予。 如果用CAShapeLayer替代Core Graphics,性能就會(huì)得到提高材原,雖然隨著路徑復(fù)雜性的增加沸久,繪制性能依然會(huì)下降,但是只有當(dāng)非常非常浮躁的繪制時(shí)才會(huì)感到明顯的幀率差異余蟹。

CAShapeLayer存在的缺陷

特殊情況下用CAShapeLayer或者其他矢量圖形圖層替代Core Graphics并不是那么切實(shí)可行卷胯。比如我們的繪圖應(yīng)用:我們用線條完美地完成了矢量繪制。但是設(shè)想一下如果我們能進(jìn)一步提高應(yīng)用的性能威酒,讓它就像一個(gè)黑板一樣工作窑睁,然后用『粉筆』來(lái)繪制線條挺峡。模擬粉筆最簡(jiǎn)單的方法就是用一個(gè)『線刷』圖片然后將它粘貼到用戶手指碰觸的地方,但是這個(gè)方法用CAShapeLayer沒(méi)辦法實(shí)現(xiàn)担钮。 我們可以給每個(gè)『線刷』創(chuàng)建一個(gè)獨(dú)立的圖層橱赠,但是實(shí)現(xiàn)起來(lái)有很大的問(wèn)題。屏幕上允許同時(shí)出現(xiàn)圖層上線數(shù)量大約是幾百箫津,那樣我們很快就會(huì)超出的病线。這種情況下我們沒(méi)什么辦法,就用Core Graphics吧(除非你想用OpenGL做一些更復(fù)雜的事情)鲤嫡。

#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>
#define BRUSH_SIZE 32

@interface DrawingView ()

@property (nonatomic, strong) NSMutableArray *strokes;

@end

@implementation DrawingView

- (void)awakeFromNib
{
    //create array
    self.strokes = [NSMutableArray array];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the starting point
    CGPoint point = [[touches anyObject] locationInView:self];

    //add brush stroke
    [self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //get the touch point
    CGPoint point = [[touches anyObject] locationInView:self];

    //add brush stroke
    [self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
    //add brush stroke to array
    [self.strokes addObject:[NSValue valueWithCGPoint:point]];

    //needs redraw
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    //redraw strokes
    for (NSValue *value in self.strokes) {
        //get point
        CGPoint point = [value CGPointValue];

        //get brush rect
        CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);

        //draw brush stroke    
        [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
    }
}
@end

程序繪制一個(gè)簡(jiǎn)單的『素描』 這個(gè)實(shí)現(xiàn)在模擬器上表現(xiàn)還不錯(cuò)送挑,但是在真實(shí)設(shè)備上就沒(méi)那么好了。問(wèn)題在于每次手指移動(dòng)的時(shí)候我們就會(huì)重繪之前的線刷暖眼,即使場(chǎng)景的大部分并沒(méi)有改變惕耕。我們繪制地越多,就會(huì)越慢诫肠。隨著時(shí)間的增加每次重繪需要更多的時(shí)間司澎,幀數(shù)也會(huì)下降(見(jiàn)圖13.3),如何提高性能呢栋豫?


FPS明顯很低

為了減少不必要的繪制挤安,Mac OSiOS設(shè)備將會(huì)把屏幕區(qū)分為需要重繪的區(qū)域不需要重繪的區(qū)域那些需要重繪的部分被稱作『臟區(qū)域』丧鸯。在實(shí)際應(yīng)用中蛤铜,鑒于非矩形區(qū)域邊界裁剪和混合的復(fù)雜性,通常會(huì)區(qū)分出包含指定視圖的矩形位置丛肢,而這個(gè)位置就是『臟矩形』围肥。*** 當(dāng)一個(gè)視圖被改動(dòng)過(guò)了,TA可能需要重繪蜂怎。但是很多情況下穆刻,只是這個(gè)視圖的一部分被改變了,所以重繪整個(gè)寄宿圖就太浪費(fèi)了杠步。***
但是Core Animation通常并不了解你的自定義繪圖代碼氢伟,它也不能自己計(jì)算出臟區(qū)域的位置。然而幽歼,你的確可以提供這些信息朵锣。 當(dāng)你檢測(cè)到指定視圖或圖層的指定部分需要被重繪,你直接調(diào)用【-setNeedsDisplayInRect:】來(lái)標(biāo)記它试躏,然后將影響到的矩形作為參數(shù)傳入猪勇。這樣就會(huì)在一次視圖刷新時(shí)調(diào)用視圖的-drawRect:(或圖層代理的-drawLayer:inContext:方法)设褐。 傳入-drawLayer:inContext:CGContext參數(shù)會(huì)自動(dòng)被裁切以適應(yīng)對(duì)應(yīng)的矩形颠蕴。為了確定矩形的尺寸大小泣刹,你可以用CGContextGetClipBoundingBox()方法來(lái)從上下文獲得大小。調(diào)用-drawRect()會(huì)更簡(jiǎn)單犀被,因?yàn)?code>CGRect會(huì)作為參數(shù)直接傳入椅您。 你應(yīng)該將你的繪制工作限制在這個(gè)矩形中。任何在此區(qū)域之外的繪制都將被自動(dòng)無(wú)視寡键,但是這樣CPU花在計(jì)算和拋棄上的時(shí)間就浪費(fèi)了掀泳,實(shí)在是太不值得了。 相比依賴于Core Graphics為你重繪西轩,裁剪出自己的繪制區(qū)域可能會(huì)讓你避免不必要的操作员舵。那就是說(shuō),如果你的裁剪邏輯相當(dāng)復(fù)雜藕畔,那還是讓Core Graphics來(lái)代勞吧马僻,記住:當(dāng)你能高效完成的時(shí)候才這樣做注服。 代碼 展示了一個(gè)-addBrushStrokeAtPoint:方法的升級(jí)版韭邓,它只重繪當(dāng)前線刷的附近區(qū)域。另外也會(huì)刷新之前線刷的附近區(qū)域溶弟,我們也可以用【CGRectIntersectsRect()】來(lái)避免重繪任何舊的線刷以不至于覆蓋已更新過(guò)的區(qū)域女淑。這樣做會(huì)顯著地提高繪制效率,下面胡代碼 用-setNeedsDisplayInRect:來(lái)減少不必要的繪制辜御。

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
    //add brush stroke to array
    [self.strokes addObject:[NSValue valueWithCGPoint:point]];

    //set dirty rect
    [self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}

- (CGRect)brushRectForPoint:(CGPoint)point
{
    return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}

- (void)drawRect:(CGRect)rect
{
    //redraw strokes
    for (NSValue *value in self.strokes) {
        //get point
        CGPoint point = [value CGPointValue];

        //get brush rect
        CGRect brushRect = [self brushRectForPoint:point];
        
        //only draw brush stroke if it intersects dirty rect
        if (CGRectIntersectsRect(rect, brushRect)) {
            //draw brush stroke
            [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
        }
    }
}
FPS正常

異步繪制

UIKit的單線程天性意味著寄宿圖通暢要在主線程上更新鸭你,這意味著繪制會(huì)打斷用戶交互,甚至讓整個(gè)app看起來(lái)處于無(wú)響應(yīng)狀態(tài)擒权。我們對(duì)此無(wú)能為力苇本,但是如果能避免用戶等待繪制完成就好多了。 針對(duì)這個(gè)問(wèn)題菜拓,有一些方法可以用到:一些情況下瓣窄,我們可以推測(cè)性地提前在另外一個(gè)線程上繪制內(nèi)容,然后將由此繪出的圖片直接設(shè)置為圖層的內(nèi)容纳鼎。這實(shí)現(xiàn)起來(lái)可能不是很方便俺夕,但是在特定情況下是可行的。Core Animation提供了一些選擇:CATiledLayerdrawsAsynchronously屬性贱鄙。

  • CATiledLayer

我們?cè)诘诹潞?jiǎn)單探索了一下CATiledLayer劝贸。除了將圖層再次分割成獨(dú)立更新的小塊(類似于臟矩形自動(dòng)更新的概念),CATiledLayer還有一個(gè)有趣的特性:在多個(gè)線程中為每個(gè)小塊同時(shí)調(diào)用-drawLayer:inContext:方法逗宁。這就避免了阻塞用戶交互而且能夠利用多核心新片來(lái)更快地繪制映九。只有一個(gè)小塊的CATiledLayer是實(shí)現(xiàn)異步更新圖片視圖的簡(jiǎn)單方法。

  • drawsAsynchronously

iOS 6中瞎颗,蘋(píng)果為CALayer引入了這個(gè)令人好奇的屬性件甥,drawsAsynchronously屬性對(duì)傳入-drawLayer:inContext:的CGContext進(jìn)行改動(dòng)捌议,允許CGContext延緩繪制命令的執(zhí)行以至于不阻塞用戶交互。 它與CATiledLayer使用的異步繪制并不相同引有。它自己的-drawLayer:inContext:方法只會(huì)在主線程調(diào)用瓣颅,但是CGContext并不等待每個(gè)繪制命令的結(jié)束。相反地譬正,它會(huì)將命令加入隊(duì)列宫补,當(dāng)方法返回時(shí),在后臺(tái)線程逐個(gè)執(zhí)行真正的繪制曾我。 根據(jù)蘋(píng)果的說(shuō)法粉怕。這個(gè)特性在需要頻繁重繪的視圖上效果最好(比如我們的繪圖應(yīng)用,或者諸如UITableViewCell之類的)抒巢,對(duì)那些只繪制一次或很少重繪的圖層內(nèi)容來(lái)說(shuō)沒(méi)什么太大的幫助斋荞。

總結(jié)

本章我們主要圍繞用Core Graphics軟件繪制討論了一些性能挑戰(zhàn),然后探索了一些改進(jìn)方法:比如提高繪制性能或者減少需要繪制的數(shù)量虐秦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末平酿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子悦陋,更是在濱河造成了極大的恐慌蜈彼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俺驶,死亡現(xiàn)場(chǎng)離奇詭異幸逆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)暮现,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門还绘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人栖袋,你說(shuō)我怎么就攤上這事拍顷。” “怎么了塘幅?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵昔案,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我电媳,道長(zhǎng)踏揣,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任匾乓,我火速辦了婚禮捞稿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己娱局,他們只是感情好彰亥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著铃辖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猪叙。 梳的紋絲不亂的頭發(fā)上娇斩,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音穴翩,去河邊找鬼犬第。 笑死,一個(gè)胖子當(dāng)著我的面吹牛芒帕,可吹牛的內(nèi)容都是我干的歉嗓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼背蟆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鉴分!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起带膀,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤志珍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后垛叨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體伦糯,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年嗽元,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了敛纲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡剂癌,死狀恐怖淤翔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佩谷,我是刑警寧澤办铡,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站琳要,受9級(jí)特大地震影響寡具,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜稚补,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一童叠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦厦坛、人聲如沸五垮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)放仗。三九已至,卻和暖如春撬碟,著一層夾襖步出監(jiān)牢的瞬間诞挨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工呢蛤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惶傻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓其障,卻偏偏與公主長(zhǎng)得像银室,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子励翼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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