前言
移動(dòng)端FPS優(yōu)化已經(jīng)是一個(gè)老生常談的話題了润歉,但在相當(dāng)長(zhǎng)一段時(shí)間內(nèi)卻一直是一個(gè)不過(guò)期的話題,除非硬件強(qiáng)大到可以幫我們抹平屏幕成像和渲染上的性能損耗嵌屎。身為一個(gè)移動(dòng)互聯(lián)網(wǎng)從業(yè)者新蟆,對(duì)FPS的認(rèn)識(shí)和優(yōu)化依舊是很有限的砚哗,深感不安和羞愧饰及,本文整理了之前的一些工作筆記蔗坯,結(jié)合一些大牛們的優(yōu)秀文章,希望能夠起到復(fù)習(xí)和深化的作用燎含。內(nèi)容不實(shí)之處還請(qǐng)大家及時(shí)指出宾濒,感謝!
幀率
即 Frame Rate屏箍,單位 fps绘梦,是指 gpu 生成幀的速率,如 33 fps赴魁,60fps谚咬,越高越好。
屏幕刷新頻率
即 Refresh Rate 或 Scanning Frequency尚粘,單位赫茲/Hz,是指設(shè)備刷新屏幕的頻率敲长,該值對(duì)于特定的設(shè)備來(lái)說(shuō)是個(gè)常量郎嫁,如 60hz。
如下圖祈噪,屏幕的刷新過(guò)程是每一行從左到右(行刷新泽铛,水平刷新,Horizontal Scanning)辑鲤,從上到下(屏幕刷新盔腔,垂直刷新,Vertical Scanning)月褥。當(dāng)整個(gè)屏幕刷新完畢弛随,即一個(gè)垂直刷新周期完成,會(huì)有短暫的空白期宁赤,此時(shí)發(fā)出 VSync 信號(hào)舀透。所以,VSync 中的 V 指的是垂直刷新中的垂直/Vertical决左。
垂直同步信號(hào)
那什么是Vsync/垂直同步信號(hào)呢愕够?
iOS和Android系統(tǒng)中有 2 種 VSync 信號(hào)走贪,屏幕產(chǎn)生的硬件VSync信號(hào)和負(fù)責(zé)給GPU的軟件信號(hào)(CADisplayLink)。
VSync: 垂直同步信號(hào)惑芭,又叫做幀同步信號(hào),表示掃描1幀的開始坠狡,一幀也就是LCD顯示的一個(gè)畫面。Vsync信號(hào)是由硬件時(shí)鐘產(chǎn)生的一個(gè)脈沖信號(hào)遂跟,起到開關(guān)或觸發(fā)某種操作的作用逃沿。Vsync會(huì)以固定的頻率產(chǎn)生,不受軟件的影響(只要有電就會(huì)產(chǎn)生)漩勤。這個(gè)固定的頻率叫做屏幕刷新頻率(refresh rate或者Scanning Frequency)感挥。通常情況下,這個(gè)頻率是60hz越败。也就是1/60s == 16.666ms就會(huì)產(chǎn)生一個(gè)垂直同步信號(hào)触幼。ps:另外還有幀率/frame rate ,單位 fps究飞,是指 gpu 生成幀的速率置谦,如 33 fps,60fps亿傅,越高越好媒峡。屏幕刷新頻率和幀率沒(méi)有什么關(guān)系。
另外還有水平同步信號(hào)HSync葵擎,如下是工作原理圖:
PS:更多信息請(qǐng)自行復(fù)習(xí)《計(jì)算機(jī)組成原理》或《數(shù)字電路與邏輯設(shè)計(jì)》等大學(xué)教材谅阿。
屏幕顯示圖像的原理
通常來(lái)時(shí),計(jì)算機(jī)系統(tǒng)的CPU酬滤、GPU签餐、顯示器是以一種類似于串行的方式協(xié)同工作的。如下圖盯串,CPU計(jì)算好顯示的內(nèi)容提交給GPU氯檐;GPU把CPU提交過(guò)來(lái)的內(nèi)容渲染成顯示器可以顯示的格式(也就是我們常說(shuō)的一幀)。GPU渲染完成后將渲染結(jié)果(也就是一幀畫面)放到屏幕的幀緩沖區(qū)(此處的幀緩沖區(qū)和離屏渲染的屏幕緩沖區(qū)体捏、屏幕外緩沖區(qū)是一回事)冠摄;隨后視頻控制器會(huì)按照VSync(垂直同步信號(hào))讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過(guò)數(shù)模轉(zhuǎn)換傳遞給顯示器顯示几缭。
單緩沖機(jī)制
單緩沖機(jī)制河泳。幀緩沖區(qū)只有一個(gè),GPU向幀緩沖區(qū)提交渲染好的數(shù)據(jù)年栓,視頻控制器從幀緩沖區(qū)讀取數(shù)據(jù)顯示到屏幕上(典型的生產(chǎn)者—消費(fèi)者模型)乔询。這時(shí)幀緩沖區(qū)的讀取和刷新都都會(huì)有比較大的效率問(wèn)題。
雙緩沖機(jī)制
注意韵洋,此處的“雙緩沖”和計(jì)算機(jī)組成原理中的“二級(jí)緩存”是兩回事竿刁。三重緩存也是如此黄锤。
為了解決單緩沖的效率問(wèn)題,顯示系統(tǒng)通常會(huì)引入兩個(gè)緩沖區(qū)食拜,即雙緩沖機(jī)制鸵熟,實(shí)際上iOS設(shè)備也是這么做的。雙緩沖機(jī)制下负甸,GPU 會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi)流强,讓視頻控制器讀取,當(dāng)下一幀渲染好后呻待,GPU 會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器打月。如此一來(lái)效率會(huì)有很大的提升。(iOS 保持界面流暢的技巧)
雙緩沖雖然能解決單緩沖區(qū)效率問(wèn)題蚕捉,但會(huì)引入一個(gè)新的問(wèn)題奏篙。當(dāng)視頻控制器還未讀取完成時(shí),即屏幕內(nèi)容剛顯示一半時(shí)迫淹,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個(gè)緩沖區(qū)進(jìn)行交換后秘通,視頻控制器就會(huì)把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成“畫面撕裂”現(xiàn)象敛熬,我們稱之為“screen tearing”肺稀,如下圖(google搜索畫面撕裂即可):
PS:實(shí)際上,單緩沖區(qū)機(jī)制也是存在畫面撕裂現(xiàn)象的应民。例如话原,當(dāng)幀率大于刷新頻率,當(dāng)屏幕還沒(méi)有刷新第 n-1 幀的時(shí)候诲锹,GPU 已經(jīng)在生成第 n 幀了稿静,從上往下開始覆蓋第 n-1 幀的數(shù)據(jù),當(dāng)屏幕開始刷新第 n-1 幀的時(shí)候辕狰,Buffer 中的數(shù)據(jù)上半部分是第 n 幀數(shù)據(jù),而下半部分是第 n-1 幀的數(shù)據(jù)控漠,顯示出來(lái)的圖像就會(huì)出現(xiàn)上半部分和下半部分明顯偏差的現(xiàn)象蔓倍,我們稱之為 “tearing”。
雙緩沖+VSync同步機(jī)制
兩個(gè)緩存區(qū)分別為 Back Buffer 和 Frame Buffer盐捷。GPU 向 Back Buffer 中寫數(shù)據(jù)偶翅,屏幕從 Frame Buffer 中讀數(shù)據(jù)。VSync 信號(hào)負(fù)責(zé)調(diào)度從 Back Buffer 到 Frame Buffer 的復(fù)制操作碉渡,可認(rèn)為該復(fù)制操作在瞬間完成聚谁。其實(shí),該復(fù)制操作是等價(jià)后的效果滞诺,實(shí)際上雙緩沖的實(shí)現(xiàn)方式是交換 Back Buffer 和 Frame Buffer 的名字形导,更具體的說(shuō)是交換內(nèi)存地址(有沒(méi)有聯(lián)想到那道經(jīng)典的筆試題目:“有兩個(gè)整型數(shù)环疼,如何用最優(yōu)的方法交換二者的值?”)朵耕,通過(guò)按位運(yùn)算“與”即可完成炫隶,所以可認(rèn)為是瞬間完成。
雙緩沖的模型下阎曹,工作流程這樣的:
在某個(gè)時(shí)間點(diǎn)伪阶,一個(gè)屏幕刷新周期完成,進(jìn)入短暫的刷新空白期处嫌。此時(shí)栅贴,VSync 信號(hào)產(chǎn)生,先完成復(fù)制操作(交換緩沖區(qū)內(nèi)容)熏迹,然后通知 CPU/GPU 繪制下一幀圖像檐薯。復(fù)制操作完成后屏幕開始下一個(gè)刷新周期,即將剛復(fù)制到 Frame Buffer 的數(shù)據(jù)顯示到屏幕上癣缅。
在這種模型下厨剪,只有當(dāng) VSync 信號(hào)產(chǎn)生時(shí),CPU/GPU 才會(huì)開始繪制友存。這樣祷膳,當(dāng)幀率大于刷新頻率時(shí),幀率就會(huì)被迫跟刷新頻率保持同步屡立,從而避免“tearing”現(xiàn)象直晨。總結(jié)一下膨俐,開啟VSync的本質(zhì)就是強(qiáng)制拉平我們的GPU每秒繪制的幀數(shù)和屏幕的刷新頻率勇皇。
為什么我的游戲會(huì)出現(xiàn)畫面撕裂
可能你還會(huì)問(wèn),為什么我的顯卡和顯示器配置都很高焚刺,玩游戲時(shí)還是會(huì)存在畫面撕裂的現(xiàn)象呢敛摘?這里需要強(qiáng)調(diào)下,顯卡性能高和顯示器頻率高并不代表不會(huì)出現(xiàn)畫面撕裂乳愉,如果沒(méi)有開啟VSync就會(huì)存在畫面撕裂的情況兄淫。所以,如果你發(fā)現(xiàn)你的玩游戲的時(shí)候出現(xiàn)了畫面撕裂蔓姚,可以檢查下是否開啟了VSync捕虽。如下,是某款游戲的VSync開關(guān):
注意坡脐,當(dāng) VSync 信號(hào)發(fā)出時(shí)泄私,如果 GPU/CPU 正在生產(chǎn)幀數(shù)據(jù),此時(shí)不會(huì)發(fā)生復(fù)制操作。屏幕進(jìn)入下一個(gè)刷新周期時(shí)晌端,從 Frame Buffer 中取出的是“老”數(shù)據(jù)捅暴,而非正在產(chǎn)生的幀數(shù)據(jù),即兩個(gè)刷新周期顯示的是同一幀數(shù)據(jù)斩松。這是我們稱發(fā)生了“掉幀”(Dropped Frame伶唯,Skipped Frame,Jank)現(xiàn)象惧盹。
另外還有triple buffer(三重緩存)乳幸,但是iOS設(shè)備采用的是雙重緩存,Android設(shè)備采用的三重緩存钧椰,在這里不作講解粹断。
卡頓產(chǎn)生的原因和優(yōu)化方案
在 VSync 信號(hào)到來(lái)后,系統(tǒng)圖形服務(wù)會(huì)通過(guò) CADisplayLink 等機(jī)制通知 App嫡霞,App 主線程開始在 CPU 中計(jì)算顯示內(nèi)容瓶埋,比如視圖的創(chuàng)建、布局計(jì)算诊沪、圖片解碼养筒、文本繪制等。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去端姚,由 GPU 進(jìn)行變換晕粪、合成、渲染渐裸。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去巫湘,等待下一次 VSync 信號(hào)到來(lái)時(shí)顯示到屏幕上。由于垂直同步的機(jī)制昏鹃,如果在一個(gè) VSync 時(shí)間內(nèi)尚氛,CPU 或者 GPU 沒(méi)有完成內(nèi)容提交,則那一幀就會(huì)被丟棄洞渤,等待下一次機(jī)會(huì)再顯示阅嘶,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因载迄。
從上面的圖中可以看到讯柔,CPU 和 GPU 不論哪個(gè)阻礙了顯示流程,都會(huì)造成掉幀現(xiàn)象宪巨。所以開發(fā)時(shí),也需要分別對(duì) CPU 和 GPU 壓力進(jìn)行評(píng)估和優(yōu)化溜畅。iOS 保持界面流暢的技巧
FPS優(yōu)化Tips
CPU優(yōu)化
盡量使用基本數(shù)據(jù)類型這種輕量級(jí)的類型捏卓,避免使用對(duì)象類型,比如使用int而不是NSNumber。
避免UIView屬性的頻繁調(diào)整或設(shè)置怠晴,頻繁冗余的設(shè)置屬性frame遥金、bounds、transform會(huì)頻繁的浪費(fèi)CPU的計(jì)算能力蒜田,會(huì)導(dǎo)致額外的CPU開銷稿械。因?yàn)镃PU需要先計(jì)算好UIView的這些屬性,然后才會(huì)交由GPU渲染冲粤。
對(duì)象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方美莫。這里特別說(shuō)一下 CALayer:CALayer 內(nèi)部并沒(méi)有屬性,當(dāng)調(diào)用屬性方法時(shí)梯捕,它內(nèi)部是通過(guò)運(yùn)行時(shí) resolveInstanceMethod 為對(duì)象臨時(shí)添加一個(gè)方法厢呵,并把對(duì)應(yīng)屬性值保存到內(nèi)部的一個(gè) Dictionary 里,同時(shí)還會(huì)通知 delegate傀顾、創(chuàng)建動(dòng)畫等等襟铭,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實(shí)際上都是 CALayer 屬性映射來(lái)的短曾,所以對(duì) UIView 的這些屬性進(jìn)行調(diào)整時(shí)寒砖,消耗的資源要遠(yuǎn)大于一般的屬性。對(duì)此你在應(yīng)用中嫉拐,應(yīng)該盡量減少不必要的屬性修改哩都。視圖無(wú)交互時(shí)盡量使用CALayer,比如使用CALayer代替UIView\UILabel\UIImageView椭岩。
盡量提前計(jì)算好布局茅逮,一次性設(shè)置給UIView,避免多次設(shè)置判哥。如下:
- (void)layoutSubviews
{
[super layoutSubviews];
// 正例:
CGFloat headerWidth = 97.f;
CGFloat headerHeight = 86.f;
self.headerImageView.frame = CGRectMake(16.f, 15.f, headerWidth, headerHeight);
// ...
// 反例:
self.priceView.top = 65.f;
self.priceView.left = 16.f;
}
復(fù)雜的頁(yè)面推薦使用frame布局献雅,盡量不要使用autolayout。autolayout會(huì)比f(wàn)rame布局消耗更多的CPU資源塌计。
盡量把耗時(shí)的操作放到子線程挺身。比如文本處理(包括尺寸計(jì)算和文本繪制)、圖片處理(包括解碼和繪制)
- 盡量在子線程計(jì)算文本尺寸锌仅,比如boundingRect方法的調(diào)用章钾,可以放到子線程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 尺寸計(jì)算
[@"xxx" boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName : HEXCOLOR(0x333333)} context:nil];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 文本繪制
[@"xxx" drawWithRect:CGRectMake(0, 0, SCREEN_WIDTH, 50) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
});
- 盡量在子線程對(duì)圖片進(jìn)行解碼(UIImage只有在顯示的時(shí)候才會(huì)解碼,而這個(gè)操作一般是在主線程热芹,所以容易造成卡頓)
說(shuō)明:[UIImage imageNamed:@"xxx"]方式加載進(jìn)來(lái)的圖片是不能直接顯示到屏幕上的贱傀,imageNamed:加載進(jìn)來(lái)的是壓縮過(guò)的圖片的二進(jìn)制數(shù)據(jù),想要把image渲染到屏幕上還需要對(duì)二進(jìn)制數(shù)據(jù)進(jìn)行解碼,而這個(gè)解碼過(guò)程往往是在主線程中執(zhí)行的伊脓。如果圖片比較多或者比較大府寒,也有可能極大地消耗CPU的資源,造成卡頓。
解決思路:在子線程解碼株搔。
@implementation UIImageView (FPS)
- (void)fps_setImage:(UIImage *)image {
if (image == nil) {
return;
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 獲取CGImage
CGImageRef cgImage = image.CGImage;
// alphaInfo
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// bitmapInfo
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// size
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
// context
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
// draw 把cgImage繪制到上下文會(huì)觸發(fā)解碼
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
// get CGImage
cgImage = CGBitmapContextCreateImage(context);
// into UIImage
UIImage *newImage = [UIImage imageWithCGImage:cgImage];
// release
CGContextRelease(context);
CGImageRelease(cgImage);
// back to the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.image = newImage;
});
});
}
@end
圖片的size最好剛好和UIImageView的size一致剖淀。盡量避免圖片尺寸伸縮。
如果確定子視圖大小和位置是固定的纤房,那么避免在cell的layoutSubViews中設(shè)置子視圖的位置和大小纵隔。因?yàn)閠ableView滾動(dòng)時(shí)候會(huì)調(diào)用cell的layoutSubView方法。cell的layoutSubViews方法中布局代碼太多比較耗時(shí)炮姨。
如果一個(gè)對(duì)象(比如subview)在父對(duì)象init時(shí)就要?jiǎng)?chuàng)建捌刮,那么避免使用懶加載的方式。因?yàn)槭潞箢l繁的判斷懶加載的if也是耗性能的剑令。
依賴于其他數(shù)據(jù)的對(duì)象或者初始化比較復(fù)雜的對(duì)象糊啡,能懶加載的就懶加載,能延后加載的就延后加載吁津。
-
后臺(tái)釋放大對(duì)象棚蓄,比如較大的圖片。ASDK認(rèn)為碍脏,大圖在主線程釋放的時(shí)候會(huì)消耗更高的性能和時(shí)間梭依,此處最小尺寸是20x20。
static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; // ASDK這樣做的: void ASPerformBlockOnDeallocationQueue(void (^block)()) { static dispatch_queue_t queue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ queue = dispatch_queue_create("org.AsyncDisplayKit.deallocationQueue", DISPATCH_QUEUE_SERIAL); }); dispatch_async(queue, block); } // 如何使用: - (void)_clearImage { // Destruction of bigger images on the main thread can be expensive // and can take some time, so we dispatch onto a bg queue to // actually dealloc. __block UIImage *image = self.image; CGSize imageSize = image.size; BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || imageSize.height > kMinReleaseImageOnBackgroundSize.height; if (shouldReleaseImageOnBackgroundThread) { ASPerformBlockOnDeallocationQueue(^{ image = nil; }); } ///TODO ///
12.避免UI上使用過(guò)多的RAC信號(hào)典尾,UI上綁定RAC信號(hào)太多也會(huì)影響FPS役拴。
13.控制線程的最大并發(fā)數(shù)量。
GPU優(yōu)化
盡量減少視圖數(shù)量和層次钾埂。
盡量避免短時(shí)間內(nèi)大量圖片的顯示河闰,可以的話將多張圖片合成一張顯示。
GPU能處理的最大文理尺寸是4096*4096褥紫,一旦超過(guò)這個(gè)尺寸姜性,就會(huì)占用CPU資源進(jìn)行處理
4.減少透明的視圖(alpha<1), 不透明的視圖就設(shè)置opaque = YES(默認(rèn)為YES)盡量避免離屏渲染
哪些操作會(huì)觸發(fā)離屏渲染髓考?
光柵化 layer.shouldRasterize = YES 會(huì)觸發(fā)離屏渲染
遮罩 layer.mask = xxx 也會(huì)觸發(fā)離屏渲染
圓角 同時(shí)設(shè)置了layer.masksToBounds = YES部念、layer.cornerRadius大于0會(huì)觸發(fā)離屏渲染。只設(shè)置layer.masksToBounds = YES或者layer.cornerRadius大于0不會(huì)觸發(fā)離屏渲染 (如果需要圓角氨菇,可以使用CoreGraphics繪制裁剪圓角或者讓UI提供圓角圖片)
陰影 layer.shadowXXX 比如layer.shadowColor儡炼、layer.shadowOffset 都會(huì)觸發(fā)離屏渲染
如果設(shè)置了layer.shadowPath就不會(huì)觸發(fā)離屏渲染
綜上,開發(fā)中應(yīng)該盡量避免以上操作查蓉。
離屏渲染的概念
在OpenGL中乌询,GPU有兩種渲染方式:
On-Screen Render: 當(dāng)前屏幕渲染,即在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行渲染豌研。
Off-Screen Render:離屏渲染妹田,在當(dāng)前屏幕緩沖區(qū)外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染竣灌。
離屏渲染消耗性能的原因:
GPU需要?jiǎng)?chuàng)建新的緩沖區(qū)
離屏渲染的整個(gè)過(guò)程,需要多次切換上下文環(huán)境秆麸,先是從當(dāng)前屏幕緩沖區(qū)(On-Screen)切換到離屏狀態(tài)(Off-Screen),等到離屏渲染結(jié)束后(即在屏幕外緩沖區(qū)把內(nèi)容渲染好了)需要將離屏緩沖區(qū)渲染的結(jié)果顯示到屏幕上及汉,又需要將上下文環(huán)境從離屏屏幕外緩沖區(qū)切換到當(dāng)前屏幕(當(dāng)前屏幕的緩沖區(qū))沮趣。這里有一個(gè)背景:屏幕視頻控制器只會(huì)從屏幕對(duì)應(yīng)的幀緩存中一幀一幀的取數(shù)據(jù),而不會(huì)從其他的緩沖區(qū)中取數(shù)據(jù)坷随,所以我們想把其他緩沖區(qū)(也就是屏幕外緩沖區(qū))中的內(nèi)容顯示到屏幕上房铭,需要把屏幕外緩沖區(qū)渲染的結(jié)果提交到屏幕的緩沖區(qū),然后供視頻控制器去取温眉。
CALayer和UIView除了對(duì)事件的處理之外缸匪,無(wú)差別。CALayer用來(lái)顯示內(nèi)容的类溢,UIView是用來(lái)監(jiān)聽點(diǎn)擊事件的凌蔬,如果內(nèi)容和用戶無(wú)交互,可以考慮使用CALayer闯冷。
通常(默認(rèn))情況下砂心,狹義的離屏渲染是指發(fā)生在GPU的離屏渲染。廣義的離屏渲染既包括GPU離屏渲染也包括CPU離屏渲染蛇耀。
離屏渲染并不意味著軟件繪制辩诞,但是它意味著圖層必須在被顯示之前在一個(gè)屏幕外上下文中被渲染(不論CPU還是GPU)。所謂軟件繪制就是代碼繪制纺涤,因?yàn)榇a都是被CPU運(yùn)行的译暂,所以軟件繪制即是CPU繪制,硬件繪制即指GPU繪制撩炊。
CPU渲染
如果將不在GPU的當(dāng)前屏幕緩沖區(qū)中進(jìn)行的渲染都稱為離屏渲染外永,那么就還有另一種特殊的“離屏渲染”方式: CPU渲染。
如果我們重寫了drawRect方法衰抑,并且使用任何Core Graphics的技術(shù)進(jìn)行了繪制操作象迎,就涉及到了CPU渲染。整個(gè)渲染過(guò)程由CPU在App內(nèi) 同步地
完成呛踊,渲染得到的bitmap最后再交由GPU用于顯示砾淌。因?yàn)镃ore Graphics是線程安全的,所以我們可以在子線程使用CG API進(jìn)行異步繪制谭网。
YYText上的FPS解決方案
YYText實(shí)現(xiàn)了一個(gè)異步繪制的layer—YYAsyncLayer汪厨。
YYAsyncLayer內(nèi)部持有一個(gè)sentinel(使用OSAtomicIncrement32保證線程安全),該變量自增愉择,起到標(biāo)記作用劫乱。當(dāng)layer調(diào)用dealloc织中、setNeedsDisplay、就會(huì)遞增這個(gè)變量衷戈,異步繪制過(guò)程中會(huì)多次檢查這個(gè)變量來(lái)判斷此次繪制任務(wù)是否應(yīng)該取消狭吼。
重寫CALayer的display方法,在display方法中異步繪制殖妇。
- (void)display {
[self _displayAsync:_displaysAsynchronously];
}
- (void)_displayAsync:(BOOL)async {
// 向delegate也就是YYLabel獲取更新任務(wù)刁笙,newAsyncDisplayTask會(huì)返回一個(gè)新的繪制的任務(wù)
__strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate;
YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
if (!task.display) {
if (task.willDisplay) task.willDisplay(self);
self.contents = nil;
if (task.didDisplay) task.didDisplay(self, YES);
return;
}
if (task.willDisplay) task.willDisplay(self);
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
// 判斷是否該取消異步繪制任務(wù)
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
if (size.width < 1 || size.height < 1) {
CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
self.contents = nil;
if (image) {
dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
CFRelease(image);
});
}
if (task.didDisplay) task.didDisplay(self, YES);
CGColorRelease(backgroundColor);
return;
}
// 異步繪制
dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
if (isCancelled()) {
CGColorRelease(backgroundColor);
return;
}
// 開啟圖片上下文
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (opaque && context) {
CGContextSaveGState(context);
{
if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
if (backgroundColor) {
CGContextSetFillColorWithColor(context, backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
}
CGContextRestoreGState(context);
CGColorRelease(backgroundColor);
}
task.display(context, size, isCancelled);
if (isCancelled()) {
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
// 從當(dāng)前上下文獲取圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (isCancelled()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (isCancelled()) {
if (task.didDisplay) task.didDisplay(self, NO);
} else {
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
});
});
}
后續(xù)會(huì)結(jié)合代碼和效果圖補(bǔ)充一些FPS優(yōu)化的Tip,敬請(qǐng)持續(xù)關(guān)注和討論!
參考文章
vsync, hsync, VBLANK
VSYNC和HSYNC
垂直同步是什么谦趣?造成游戲畫面撕裂的原因
什么是畫面撕裂疲吸?垂直同步,G-sync前鹅,F(xiàn)reesync到底有啥用摘悴?
iOS 保持界面流暢的技巧