卡頓原因分析:
1.屏幕顯示圖像的原理:
CPU:負(fù)責(zé)對(duì)象的創(chuàng)建和銷毀、對(duì)象屬性的調(diào)整、布局計(jì)算到千、文本的計(jì)算和排版、圖片的格式轉(zhuǎn)換和解碼赴穗、圖像的繪制(Core Graphics)
GPU: 負(fù)責(zé)紋理的渲染(將數(shù)據(jù)渲染到屏幕))
-
CPU 和 GPU 的協(xié)作:
由上圖可知憔四,要在屏幕上顯示視圖,需要CPU和GPU一起協(xié)作般眉,CPU計(jì)算好顯示的內(nèi)容提交到GPU了赵,GPU渲染完成后將結(jié)果放到幀緩存區(qū),隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù)甸赃,經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示柿汛。 垂直同步技術(shù): 讓CPU和GPU在收到vSync信號(hào)后再開始準(zhǔn)備數(shù)據(jù),防止撕裂感和跳幀埠对,通俗來講就是保證每秒輸出的幀數(shù)不高于屏幕顯示的幀數(shù)络断。
雙緩沖技術(shù): iOS是雙緩沖機(jī)制,前幀緩存和后幀緩存,CPU計(jì)算完GPU渲染后放入緩沖區(qū)中,當(dāng)GPU下一幀已經(jīng)渲染完放入緩沖區(qū)项玛,且視頻控制器已經(jīng)讀完前幀貌笨,GPU會(huì)等待vSync(垂直同步信號(hào))信號(hào)發(fā)出后,瞬間切換前后幀緩存,并讓CPU開始準(zhǔn)備下一幀數(shù)據(jù)
安卓4.0后采用三重緩沖襟沮,多了一個(gè)后幀緩沖锥惋,可降低連續(xù)丟幀的可能性,但會(huì)占用更多的CPU和GPU-
屏幕顯示圖像的原理: 圖像的顯示可以簡(jiǎn)單理解成先經(jīng)過CPU的計(jì)算/排版/編解碼等操作臣嚣,然后交由GPU去完成渲染放入緩沖中净刮,當(dāng)視頻控制器接受到vSync時(shí)會(huì)從緩沖中讀取已經(jīng)渲染完成的幀并顯示到屏幕上。
iOS手機(jī)默認(rèn)刷新率是60hz硅则,所以GPU渲染只要達(dá)到60fps就不會(huì)產(chǎn)生卡頓淹父。
以60fps為例,vSync會(huì)每16.67ms發(fā)出怎虫,如在16.67ms內(nèi)沒有準(zhǔn)備好下一幀數(shù)據(jù)就會(huì)使畫面停留在上一幀暑认,產(chǎn)生卡頓困介,例如圖中第3幀渲染完成之前一直顯示的是第2幀的內(nèi)容。
2.圖片加載流程
- 假設(shè)我們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片蘸际,這個(gè)時(shí)候的圖片并沒有解壓縮座哩;
- 然后將生成的 UIImage 賦值給 UIImageView
- 接著一個(gè)隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化
- 在主線程的下一個(gè) runloop 到來時(shí),Core Animation 提交了這個(gè)隱式的 transaction 粮彤,這個(gè)過程可能會(huì)對(duì)圖片進(jìn)行 copy 操作根穷,而受圖片是否字節(jié)對(duì)齊等因素的影響,這個(gè) copy 操作可能會(huì)涉及以下部分或全部步驟:
(1).分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作
(2). 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中导坟;
(3).將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式屿良,這是一個(gè)非常耗時(shí)的 CPU 操作漆魔;
(4). 最后 Core Animation 中CALayer使用未壓縮的位圖數(shù)據(jù)渲染 UIImageView 的圖層然评。
(5). CPU計(jì)算好圖片的Frame,對(duì)圖片解壓之后.就會(huì)交給GPU來做圖片渲染 - 渲染流程:
(1).GPU獲取獲取圖片的坐標(biāo)
(2).將坐標(biāo)交給頂點(diǎn)著色器(頂點(diǎn)計(jì)算)
(3).將圖片光柵化(獲取圖片對(duì)應(yīng)屏幕上的像素點(diǎn))
(4). 片元著色器計(jì)算(計(jì)算每個(gè)像素點(diǎn)的最終顯示的顏色值)
(5).從幀緩存區(qū)中渲染到屏幕上
為什么要解壓縮圖片:
既然圖片的解壓縮需要消耗大量的 CPU 時(shí)間,那么我們?yōu)槭裁催€要對(duì)圖片進(jìn)行解壓縮呢笙瑟?是否可以不經(jīng)過解壓縮递递,而直接將圖片顯示到屏幕上呢喷橙?答案是否定的。要想弄明白這個(gè)問題登舞,我們首先需要知道什么是位圖
其實(shí)贰逾,位圖就是一個(gè)像素?cái)?shù)組,數(shù)組中的每個(gè)像素就代表著圖片中的一個(gè)點(diǎn)逊躁。我們?cè)趹?yīng)用中經(jīng)常用到的 JPEG 和 PNG 圖片就是位圖似踱。
不管是 JPEG 還是 PNG 圖片,都是一種壓縮的位圖圖形格式稽煤。只不過 PNG 圖片是無損壓縮核芽,并且支持 alpha 通道,而 JPEG 圖片則是有損壓縮酵熙,可以指定 0-100% 的壓縮比轧简。
所以在將磁盤中的圖片渲染到屏幕之前,必須先要得到圖片的原始像素?cái)?shù)據(jù)匾二,才能執(zhí)行后續(xù)的繪制操作哮独,這就是為什么需要對(duì)圖片解壓縮的原因。
3. 卡頓原因:
上面講解了圖片顯示的原理和屏幕渲染的原理察藐,造成卡頓的原因有很多皮璧,最主要的原因是因?yàn)榘l(fā)生了掉幀。
由上面屏幕顯示的原理分飞,采用了垂直同步機(jī)制的手機(jī)設(shè)備悴务。在 VSync 信號(hào)到來后,系統(tǒng)圖形服務(wù)會(huì)通過 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)到來時(shí)顯示到屏幕上午乓。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi)闸准,CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會(huì)被丟棄梢灭,等待下一次機(jī)會(huì)再顯示夷家,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因敏释。
在開發(fā)中库快,CPU和GPU中任何一個(gè)壓力過大,都會(huì)導(dǎo)致掉幀現(xiàn)象钥顽,所以在開發(fā)時(shí)义屏,也需要分別對(duì)CPU和GPU壓力進(jìn)行評(píng)估和優(yōu)化。
卡頓監(jiān)控:
- 主線程卡頓監(jiān)控蜂大。通過子線程監(jiān)測(cè)主線程的runLoop闽铐,判斷兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否達(dá)到一定閾值。
- FPS監(jiān)控奶浦。要保持流暢的UI交互兄墅,App 刷新率應(yīng)該當(dāng)努力保持在 60fps。FPS的監(jiān)控實(shí)現(xiàn)原理澳叉,上面已經(jīng)探討過這里略過隙咸。
1.RunLoop監(jiān)測(cè)卡頓
loop的狀態(tài)
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // 進(jìn)入 loop
kCFRunLoopBeforeTimers , // 觸發(fā) Timer 回調(diào)
kCFRunLoopBeforeSources , // 觸發(fā) Source0 回調(diào)
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
kCFRunLoopExit , // 退出 loop
kCFRunLoopAllActivities // loop 所有狀態(tài)改變
}
NSRunLoop調(diào)用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之后,也就是如果我們發(fā)現(xiàn)這兩個(gè)時(shí)間內(nèi)耗時(shí)太長(zhǎng),那么就可以判定出此時(shí)主線程卡頓。
實(shí)現(xiàn)思路:只需要另外再開啟一個(gè)線程,實(shí)時(shí)計(jì)算這兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否到達(dá)某個(gè)閥值,便能揪出這些性能殺手成洗。
閥值設(shè)定:假定連續(xù)5次超時(shí)50ms認(rèn)為卡頓(當(dāng)然也包含了單次超時(shí)250ms)
2.FPS && CADisplayLink
CADisplayLink
是CoreAnimation提供的另一個(gè)類似于NSTimer的類五督,它總是在屏幕完成一次更新之前啟動(dòng),它的接口設(shè)計(jì)的和NSTimer很類似瓶殃,所以它實(shí)際上就是一個(gè)內(nèi)置實(shí)現(xiàn)的替代充包,但是和timeInterval以秒為單位不同,CADisplayLink有一個(gè)整型的frameInterval屬性碌燕,指定了間隔多少幀之后才執(zhí)行误证。默認(rèn)值是1继薛,意味著每次屏幕更新之前都會(huì)執(zhí)行一次。但是如果動(dòng)畫的代碼執(zhí)行起來超過了六十分之一秒愈捅,你可以指定frameInterval為2遏考,就是說動(dòng)畫每隔一幀執(zhí)行一次(一秒鐘30幀)。使用CADisplayLink監(jiān)控界面的FPS值.
卡頓優(yōu)化:
1.CPU優(yōu)化:
盡量用輕量級(jí)的對(duì)象蓝谨,比如用不到事件處理的地方使用CALayer取代UIView
盡量提前計(jì)算好布局(例如cell行高)
不要頻繁地調(diào)用和調(diào)整UIView的相關(guān)屬性灌具,比如frame、bounds譬巫、transform等屬性咖楣,盡量減少不必要的調(diào)用和修改(UIView的顯示屬性實(shí)際都是CALayer的映射,而CALayer本身是沒有這些屬性的芦昔,都是初次調(diào)用屬性時(shí)通過resolveInstanceMethod添加并創(chuàng)建Dictionary保存的诱贿,耗費(fèi)資源)
Autolayout會(huì)比直接設(shè)置frame消耗更多的CPU資源,當(dāng)視圖數(shù)量增長(zhǎng)時(shí)會(huì)呈指數(shù)級(jí)增長(zhǎng).
圖片的size最好剛好跟UIImageView的size保持一致咕缎,減少圖片顯示時(shí)的處理計(jì)算
控制一下線程的最大并發(fā)數(shù)量
盡量把耗時(shí)的操作放到子線程
文本處理(尺寸計(jì)算珠十、繪制、CoreText和YYText):
(1). 計(jì)算文本寬高boundingRectWithSize:options:context: 和文本繪制drawWithRect:options:context:放在子線程操作
(2). 使用CoreText自定義文本空間凭豪,在對(duì)象創(chuàng)建過程中可以緩存寬高等信息焙蹭,避免像UILabel/UITextView需要多次計(jì)算(調(diào)整和繪制都要計(jì)算一次),且CoreText直接使用了CoreGraphics占用內(nèi)存小嫂伞,效率高孔厉。(YYText)圖片處理(解碼、繪制)
圖片都需要先解碼成bitmap才能渲染到UI上帖努,iOS創(chuàng)建UIImage撰豺,不會(huì)立刻進(jìn)行解碼,只有等到顯示前才會(huì)在主線程進(jìn)行解碼然磷,固可以使用Core Graphics中的CGBitmapContextCreate相關(guān)操作提前在子線程中進(jìn)行強(qiáng)制解壓縮獲得位圖.TableViewCell 復(fù)用: 在cellForRowAtIndexPath:回調(diào)的時(shí)候只創(chuàng)建實(shí)例郑趁,快速返回cell,不綁定數(shù)據(jù)姿搜。在willDisplayCell: forRowAtIndexPath:的時(shí)候綁定數(shù)據(jù)(賦值)
高度緩存: 在tableView滑動(dòng)時(shí)寡润,會(huì)不斷調(diào)用heightForRowAtIndexPath:,當(dāng) cell 高度需要自適應(yīng)時(shí)舅柜,每次回調(diào)都要計(jì)算高度梭纹,會(huì)導(dǎo)致 UI 卡頓。為了避免重復(fù)無意義的計(jì)算致份,需要緩存高度变抽。
視圖層級(jí)優(yōu)化: 不要?jiǎng)討B(tài)創(chuàng)建視圖,在內(nèi)存可控的前提下,緩存subview。善用hidden绍载。
減少視圖層級(jí): 減少subviews個(gè)數(shù)诡宗,用layer繪制元素. 少用 clearColor,maskToBounds击儡,陰影效果等塔沃。
減少多余的繪制操作.
圖片優(yōu)化:
(1)不要用JPEG的圖片,應(yīng)當(dāng)使用PNG圖片阳谍。
(2)子線程預(yù)解碼(Decode)蛀柴,主線程直接渲染。因?yàn)楫?dāng)image沒有Decode矫夯,直接賦值給imageView會(huì)進(jìn)行一個(gè)Decode操作鸽疾。
(3)優(yōu)化圖片大小,盡量不要?jiǎng)討B(tài)縮放(contentMode)训貌。
(4)盡可能將多張圖片合成為一張進(jìn)行顯示制肮。減少透明 view: 使用透明view會(huì)引起blending,在iOS的圖形處理中递沪,blending主要指的是混合像素顏色的計(jì)算弄企。最直觀的例子就是,我們把兩個(gè)圖層疊加在一起区拳,如果第一個(gè)圖層的透明的,則最終像素的顏色計(jì)算需要將第二個(gè)圖層也考慮進(jìn)來意乓。這一過程即為Blending樱调。
理性使用-drawRect:
當(dāng)你使用UIImageView在加載一個(gè)視圖的時(shí)候,這個(gè)視圖雖然依然有CALayer届良,但是卻沒有申請(qǐng)到一個(gè)后備的存儲(chǔ)笆凌,取而代之的是使用一個(gè)使用屏幕外渲染,將CGImageRef作為內(nèi)容士葫,并用渲染服務(wù)將圖片數(shù)據(jù)繪制到幀的緩沖區(qū)乞而,就是顯示到屏幕上,當(dāng)我們滾動(dòng)視圖的時(shí)候慢显,這個(gè)視圖將會(huì)重新加載爪模,浪費(fèi)性能。所以對(duì)于使用-drawRect:方法荚藻,更傾向于使用CALayer來繪制圖層屋灌。因?yàn)槭褂肅ALayer的-drawInContext:,Core Animation將會(huì)為這個(gè)圖層申請(qǐng)一個(gè)后備存儲(chǔ)应狱,用來保存那些方法繪制進(jìn)來的位圖共郭。那些方法內(nèi)的代碼將會(huì)運(yùn)行在 CPU上,結(jié)果將會(huì)被上傳到GPU。這樣做的性能更為好些除嘹。
靜態(tài)界面建議使用-drawRect:的方式写半,動(dòng)態(tài)頁面不建議。按需加載:
局部刷新尉咕,刷新一個(gè)cell就能解決的叠蝇,堅(jiān)決不刷新整個(gè) section 或者整個(gè)tableView,刷新最小單元元素龙考。
利用runloop提高滑動(dòng)流暢性蟆肆,在滑動(dòng)停止的時(shí)候再加載內(nèi)容,像那種一閃而過的(快速滑動(dòng))晦款,就沒有必要加載炎功,可以使用默認(rèn)的占位符填充內(nèi)容。RunLoop優(yōu)化tableview加載多圖:如果要從網(wǎng)絡(luò)加載高清大圖到UITableViewCell上缓溅,而且每個(gè)Cell上面加載多張圖片蛇损,當(dāng)cell數(shù)量過多的時(shí)候,我們需要保持流暢度和加載速度坛怪。
1,因?yàn)檫@里用到了Runloop循環(huán),那么我們可以監(jiān)聽到runloop的每次循環(huán)淤齐,
在每一次循環(huán)當(dāng)中我們考慮去進(jìn)行一次圖片下載和布局。
2,既然要在每次循環(huán)執(zhí)行一次任務(wù)袜匿,
我們可以先把所有圖片加載的任務(wù)代碼塊添加到一個(gè)數(shù)組當(dāng)中更啄,
每次循環(huán)取出第一個(gè)任務(wù)進(jìn)行執(zhí)行。*
3,因?yàn)閞unloop在閑置的時(shí)候會(huì)自動(dòng)休眠居灯,
所以我們要想辦法讓runloop始終處于循環(huán)中的狀態(tài)祭务。
2.GPU 優(yōu)化
盡量避免短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進(jìn)行顯示
GPU
能處理的最大紋理尺寸是4096x4096怪嫌,一旦超過這個(gè)尺寸义锥,就會(huì)占用CPU
資源進(jìn)行處理,所以紋理盡量不要超過這個(gè)尺寸GPU
會(huì)將多個(gè)視圖混合在一起再去顯示岩灭,混合的過程會(huì)消耗CPU資源拌倍,盡量減少視圖數(shù)量和層次減少透明的視圖(
alpha
<1),不透明的就設(shè)置opaque
為YES
噪径,GPU
就不會(huì)去進(jìn)行alpha
的通道合成盡量避免出現(xiàn)離屏渲染.
合理使用光柵化
shouldRasterize
: 光柵化是把GPU的操作轉(zhuǎn)到CPU上柱恤,生成位圖緩存,直接讀取復(fù)用熄云。CALayer
會(huì)被光柵化為bitmap
膨更,shadows
、cornerRadius
等效果會(huì)被緩存缴允。 更新已經(jīng)光柵化的layer
荚守,會(huì)造成離屏渲染珍德。bitmap
超過100ms沒有使用就會(huì)移除。 受系統(tǒng)限制矗漾,緩存的大小為 2.5X Screen Size锈候。shouldRasterize
適合靜態(tài)頁面顯示,動(dòng)態(tài)頁面會(huì)增加開銷敞贡。如果設(shè)置了shouldRasterize
為 YES泵琳,那也要記住設(shè)置rasterizationScale
為contentsScale
。異步渲染.在子線程繪制誊役,主線程渲染获列。例如 VVeboTableViewDemo
什么是離屏渲染?
在OpenGL中蛔垢,GPU有2種渲染方式
On-Screen Rendering:當(dāng)前屏幕渲染击孩,在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行渲染操作
Off-Screen Rendering:離屏渲染,在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作
離屏渲染消耗性能的原因
需要?jiǎng)?chuàng)建新的緩沖區(qū)
離屏渲染的整個(gè)過程鹏漆,需要多次切換上下文環(huán)境巩梢,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后艺玲,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上括蝠,又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕什么操作會(huì)導(dǎo)致離屏渲染?
1.光柵化饭聚,layer.shouldRasterize = YES
2.遮罩忌警,layer.mask
3.圓角,同時(shí)設(shè)置layer.masksToBounds = YES秒梳、layer.cornerRadius大于0. 考慮通過CoreGraphics繪制裁剪圓角慨蓝,或者叫美工提供圓角圖片
4.陰影,layer.shadowXXX端幼,如果設(shè)置了layer.shadowPath就不會(huì)產(chǎn)生離屏渲染.
5.layer.allowsGroupOpacity為YES,layer.opacity的值小于1.0