一、UIView如何顯示內(nèi)容
當(dāng)我們操作UI時窄做,例如改變frame队询、更新UIView/CALayer,或者自己去調(diào)用setNeedsLayout/setNeedsDisplay
方法泼差,UIView會調(diào)用-[CALayer setNeedsLayout]/-[CALayer setNeedsDisplay]
方法贵少,給layer上打上一個臟標(biāo)記,意味著需要重繪堆缘。但是只有在下一次runloop即將結(jié)束的時候才會調(diào)用[CALayer display]
,而這個方法會判斷是否實現(xiàn)了displayLayer這個方法滔灶,如果沒有實現(xiàn),那么走系統(tǒng)調(diào)用吼肥,如果實現(xiàn)了就為我們提供了異步繪制的入口录平。具體可以參看下面的流程圖
系統(tǒng)繪制:
我們首先看一下系統(tǒng)繪制,當(dāng)
[CALayer dispaly]
方法調(diào)用的時候缀皱,他會檢查-dispalyerLayer
方法是否被實現(xiàn)了斗这,若沒有實現(xiàn)則我們調(diào)用系統(tǒng)的繪制方法。首先 CALayer會生成一個backing store(CGContextRef)啤斗,每個layer都有一個content表箭,這個content指向的一塊緩存稱為backing store。如果layer有delegate争占,則調(diào)用delegate的- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
方法燃逻,否則調(diào)用-[CALayer drawInContext:]
方法,進而調(diào)用[UIView drawRect:]
方法序目。UIKit會將這個conext推到系統(tǒng)的context堆棧中,如果在draw rect中通過UIGraphicsGetCurrentContext() 取得的CGContextRef就是CALayer生成的這個實例伯襟。所有的繪制操作也會在這塊Context上生效猿涨。
CPU 執(zhí)行完draw rect之后,通過context將數(shù)據(jù)寫入backing store姆怪。當(dāng)backing store寫完之后叛赚,通過rendserver交給GPU去渲染,將backing store中的bitmap數(shù)據(jù)顯示在屏幕上稽揭。
二俺附、UIKit遇到的問題
iOS的mainLoop是一個60fps的回掉,即16.7毫秒繪制一次屏幕溪掀。這個時間內(nèi)要完成view的緩沖區(qū)創(chuàng)建事镣,view內(nèi)容的繪制,這些是cpu的工作揪胃,cpu完成之后交給GPU去渲染璃哟,這個過程又包含了多個view的拼接,紋理的渲染喊递,最終渲染在屏幕上随闪。如果這個時候,cpu做了很多工作骚勘,view層次過于復(fù)雜铐伴,圖片過大,導(dǎo)致gpu壓力也很大俏讹,那么就會出現(xiàn)迪奧幀的現(xiàn)象当宴,也就是表現(xiàn)在我們的眼里的“卡”。
如果我們所有的繪制任務(wù)都交給UIKit去做藐石,因為UIKit不是線程安全的即供,所以官方也建議我們只在主線程操作定拟。那么就無法利用cpu多核的優(yōu)勢于微,無法異步的進行繪制,但是通過對UIView繪制原理的了解我們知道青自,在異步繪制是有他的理論基礎(chǔ)的株依。
三、異步繪制的原理
好延窜,我們現(xiàn)在說一下異步繪制的原理恋腕。
我們不能在非主線程將內(nèi)容繪制到layer的context上,但是我們可以將需要繪制的內(nèi)容繪制在一個自己創(chuàng)建的跑private_context上逆瑞。通過CGBitmapContextCreate()
可以創(chuàng)建一個CGCentextRef
荠藤,在異步線程使用這個context進行繪制伙单,最后通過CGBitmapContextCreateImage()
創(chuàng)建一個CGImageRef
,并在主線程設(shè)置給layer的contents,完成異步繪制哈肖。
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
四吻育、CPU相關(guān)優(yōu)化
view的繪制與CPU和GPU都有很強的關(guān)系,但是具體是哪些呢淤井?了解了之后我們才能為卡頓問題的優(yōu)化提成更好的解決方案布疼。
4.1創(chuàng)建對象
對象的創(chuàng)建會分配內(nèi)存、調(diào)整屬性币狠、甚至?xí)赡苡蠭/O操作游两,比較消耗CPU資源。所以要盡量用輕量的對象代替重量的對象漩绵,可以對性能有所優(yōu)化贱案。比如 CALayer 比 UIView 要輕量,如果不需要響應(yīng)觸摸事件止吐,用 CALayer 顯示會更加合適轰坊。如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建祟印,但如果是包含了 CALayer 的控件肴沫,都只能在主線程創(chuàng)建和操作。盡量不使用storyboard創(chuàng)建視圖對象蕴忆。使用懶加載颤芬,將不重要對象的創(chuàng)建時機延后。
4.2調(diào)整對象視圖層級
對象的視圖層級變化也會增加cpu的運算套鹅,應(yīng)盡量減少addsubview站蝠,removesubview等操作。減少視圖的層級卓鹿,避免過多的調(diào)整菱魔。
4.3調(diào)整對象布局
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局吟孙、并且對視圖布局進行緩存澜倦,那么這個地方基本就不會產(chǎn)生性能問題了。
不論通過何種技術(shù)對視圖進行布局杰妓,其最終都會落到對 UIView.frame/bounds/center 等屬性的調(diào)整上藻治。上面也說過,對這些屬性的調(diào)整非常消耗資源巷挥,所以盡量提前計算好布局桩卵,在需要時一次性調(diào)整好對應(yīng)屬性,而不要多次、頻繁的計算和調(diào)整這些屬性雏节。
4.4文本計算胜嗓、文本渲染
如果一個界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計算會占用很大一部分資源钩乍,并且不可避免兼蕊。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高件蚕,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本孙技。盡管這兩個方法性能不錯,但仍舊需要放到后臺線程進行以避免阻塞主線程排作。
如果你用 CoreText 繪制文本牵啦,那就可以先生成 CoreText 排版對象,然后自己計算了妄痪,并且 CoreText 對象還能保留以供稍后繪制使用哈雏。
屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView衫生,在底層都是通過 CoreText 排版裳瘪、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel罪针、UITextView 等)彭羹,其排版和繪制都是在主線程進行的,當(dāng)顯示大量文本時泪酱,CPU 的壓力會非常大派殷。對此解決方案只有一個,那就是自定義文本控件墓阀,用 TextKit 或最底層的 CoreText 對文本異步繪制毡惜。盡管這實現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢也非常大斯撮,CoreText 對象創(chuàng)建好后经伙,能直接獲取文本的寬高等信息,避免了多次計算(調(diào)整 UILabel 大小時算一遍勿锅、UILabel 繪制時內(nèi)部再算一遍)帕膜;CoreText 對象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染粱甫。
4.4圖像繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中泳叠,然后從畫布創(chuàng)建圖片并顯示這樣一個過程。這個過程我們可以用異步繪制的思想解決這個問題茶宵,發(fā)揮cpu多核的優(yōu)勢。
五宗挥、GPU相關(guān)優(yōu)化
相對于 CPU 來說乌庶,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形)种蝶,應(yīng)用變換(transform)、混合并渲染瞒大,然后輸出到屏幕上螃征。
GPU處理的單位是Texture,基本上我們控制GPU都是通過OpenGL來完成的,但是從bitmap到Texture之間需要一座橋梁透敌,Core Animation正好充當(dāng)了這個角色:
Core Animation對OpenGL的api有一層封裝盯滚,當(dāng)我們的要渲染的layer已經(jīng)有了bitmap content的時候,這個content一般來說是一個CGImageRef酗电,CoreAnimation會創(chuàng)建一個OpenGL的Texture并將CGImageRef(bitmap)和這個Texture綁定魄藕,通過TextureID來標(biāo)識。
這個對應(yīng)關(guān)系建立起來之后撵术,剩下的任務(wù)就是GPU如何將Texture渲染到屏幕上了背率。
5.1視圖混合(Composing)
當(dāng)多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起嫩与。如果視圖結(jié)構(gòu)過于復(fù)雜寝姿,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗划滋,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次饵筑,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無用的 Alpha 通道合成。當(dāng)然处坪,這也可以用上面的方法翻翩,把多個視圖預(yù)先渲染為一張圖片來顯示。
5.2圖形的生成
CALayer 的 border稻薇、圓角嫂冻、陰影、遮罩(mask)塞椎,CASharpLayer 的矢量圖形顯示桨仿,通常會觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中案狠。當(dāng)一個列表視圖中出現(xiàn)大量圓角的 CALayer服傍,并且快速滑動時,可以觀察到 GPU 資源已經(jīng)占滿骂铁,而 CPU 資源消耗很少吹零。這時界面仍然能正常滑動拉庵,但平均幀數(shù)會降到很低灿椅。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去茫蛹。對于只需要圓角的某些場合操刀,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法婴洼,就是把需要顯示的圖形在后臺線程繪制為圖片骨坑,避免使用圓角、陰影柬采、遮罩等屬性欢唾。
5.3紋理的渲染
所有的 Bitmap,包括圖片粉捻、文本礁遣、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存杀迹,綁定為 GPU Texture亡脸。不論是提交到顯存的過程,還是 GPU 調(diào)整和渲染 Texture 的過程树酪,都要消耗不少 GPU 資源浅碾。當(dāng)在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低续语,GPU 占用非常高垂谢,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內(nèi)大量圖片的顯示疮茄,盡可能將多張圖片合成為一張進行顯示滥朱。