序言
開始之前, 簡要介紹一下移動(dòng)客戶端的動(dòng)態(tài)化排版方案.為滿足UI布局的靈活和后端可控性, 移動(dòng)端開發(fā)了基于Card的動(dòng)態(tài)排版渲染引擎:前后端制定好協(xié)議, 客戶端解析后端下發(fā)的描述信息,構(gòu)建和拼接不同UI元素。 相較于Native客戶端固化布局, 動(dòng)態(tài)化方案由于事先不知道UI屬性和確切尺寸,需要?jiǎng)討B(tài)創(chuàng)建并計(jì)算UI元素顯示區(qū)域。 這對代碼性能優(yōu)化提出了更高的要求. 本文就幀率測試方法和優(yōu)化經(jīng)驗(yàn)做下總結(jié).
工具選擇
檢測幀率,使用CADisplayLink API
檢測函數(shù)執(zhí)行耗時(shí)情況,使用XCode自帶的TimeProfiler工具
檢測渲染問題,使用模擬器Debug菜單下自帶的離屏及圖層顏色混合檢測工具
大家平時(shí)檢測幀率可能常用TimeProfile。該工具雖然功能強(qiáng)大,但不夠輕量、準(zhǔn)確。應(yīng)用復(fù)雜時(shí),由于這個(gè)工具需要跟設(shè)備通訊,會(huì)頻繁讀取設(shè)備狀態(tài),收集代碼堆棧信息,產(chǎn)生的數(shù)據(jù)量非常大.我們測試時(shí)幾分鐘有時(shí)產(chǎn)生上GB數(shù)據(jù)。如果只是獲取幀率,建議大家使用輕量的CADisplayLink,只有當(dāng)需要獲取更詳盡的信息時(shí),才考慮使用TimeProfile。
如何優(yōu)化
可以把這個(gè)階段分成兩階段: 定位主線程耗時(shí)代碼和針對渲染問題優(yōu)化。前者可通過TimeProfile統(tǒng)計(jì)到每個(gè)函數(shù)的耗時(shí)情況。先解決可能阻塞主線程的代碼,比如有無讀寫IO操作(將其放入非主線程執(zhí)行),有無耗時(shí)較為明顯的函數(shù),最后再通過模擬器定位離屏渲圖層混合的問題,尋找優(yōu)化方案。
階段一:下面介紹下我們優(yōu)化過程中統(tǒng)計(jì)出來的一些開銷較高的系統(tǒng)API (可能你也遇到過)
1.字符格式化操作
+(instancetype)stringWithFormat:(NSString *)format, …
當(dāng)代碼中調(diào)用該接口較少時(shí),你可以略過這個(gè)問題。但當(dāng)主線程中大量的使用該API時(shí),這個(gè)函數(shù)的耗時(shí)會(huì)變得明顯, 因?yàn)檫@個(gè)函數(shù)的執(zhí)行效率并不高。
解決方案:使用C函數(shù),比如asprintf,snprintf等創(chuàng)建char,然后用char構(gòu)建NSString。
2.圖片資源訪問
+(UIImage *)imageNamed:(NSString *)name
調(diào)用該接口加載圖片后,系統(tǒng)會(huì)緩存該圖片,以加快下次訪問。但在系統(tǒng)壓力較大的低端機(jī)型上,反復(fù)調(diào)用該接口獲取某張固定圖片,時(shí)間還是會(huì)很長。我們用TimeProfile也抓到了該函數(shù)取占位圖時(shí)耗時(shí)較長的情況。從原理上看,該接口要考慮不同擴(kuò)展名、不同機(jī)型下最佳適配資源(2x,3x分辨率圖片),根據(jù)傳入的文件名做模糊匹配。所以其效率也不是很高。
解決方案:1.使用分辨率更小的圖片,這有助于縮短第一次加載時(shí)間。2.如果該圖片屬于公共訪問非常頻繁的資源(比如占位圖),通過該接口獲取到圖片內(nèi)存地址后,用全局指針保存起來。再次訪問時(shí)可以直接使用保存好的指針,完全不會(huì)占用主線程時(shí)間。
3.文本繪制區(qū)域計(jì)算
-(CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context;
- (CGSize)sizeThatFits:(CGSize)size;
當(dāng)文本控件較多,滑動(dòng)過程中頻繁使用這類接口計(jì)算文本顯示區(qū)域,會(huì)占用較多主線程時(shí)間。
解決方案:
從UI設(shè)計(jì)上給定一個(gè)固定顯示區(qū)域,讓系統(tǒng)對過長的文本自動(dòng)截?cái)?,從而避免調(diào)用顯示接口。
2.如果方案1行不通,可以在調(diào)用一次后把計(jì)算結(jié)果緩存起來。用戶再次訪問時(shí),直接使用已計(jì)算好的數(shù)值。這個(gè)方案需要考慮影響計(jì)算結(jié)果的因素,比如字體、字號、限定的寬高、行數(shù)、行距、截?cái)喾绞降?。如果將這么多變動(dòng)的因素組合起來查詢,效率會(huì)比較低。 我們的方法是將這些數(shù)據(jù)打包成一個(gè)對象,相當(dāng)于計(jì)算結(jié)果跟原始的屬性綁定到同一個(gè)指針指向的空間里了,這樣使用時(shí)不需查詢,通過指針就可直接訪問。
4.UIView層級調(diào)整有關(guān)的代碼
- (void)insertSubview:(UIView*)view belowSubview:(UIView *)siblingSubview;
- (void)insertSubview:(UIView*)view aboveSubview:(UIView *)siblingSubview;
- (void)removeFromSuperview;
-(void)bringSubviewToFront:(UIView *)view;
-(void)sendSubviewToBack:(UIView *)view;
…
解決方案:如果你的程序運(yùn)行過程中有較多調(diào)用這類動(dòng)態(tài)插入或者調(diào)整View層級的代碼,可以在創(chuàng)建view時(shí)將層級固定下來,并對臨時(shí)用不到的view設(shè)置為隱藏,再在合適的時(shí)機(jī)顯示出來。
5.NSScan的使用
數(shù)字跟字母混合情況下,有很多人會(huì)選用這個(gè)API做數(shù)值轉(zhuǎn)換,用來分離出數(shù)值部分,但經(jīng)測試,該API性能并不好。
解決方案:1.大多簡單的數(shù)字跟字母混合字符串,直接轉(zhuǎn)換即可。比如要取出字符16px里的16出來,使用intvalue就可以。2.也可使用strtoul(const char *nptr,char **endptr,int base ),比如一個(gè)色值數(shù)據(jù)#6C6C6C,使用該接口配合位運(yùn)算,能很高效的分離出RGB3個(gè)10進(jìn)制數(shù)值來。
階段二:渲染優(yōu)化
渲染層面,影響流暢性的因素主要有離屏渲染和圖層混合。系統(tǒng)也提供了一些優(yōu)化開關(guān),默認(rèn)的優(yōu)化開關(guān)是關(guān)閉的,需要根據(jù)UI特點(diǎn),驗(yàn)證后再?zèng)Q定是否開啟。下面介紹這方面的知識。
1.關(guān)于離屏渲染Off-Screen Rendering vs On-Screen Rendering
Off-Screen Rendering(離屏渲染)需要先創(chuàng)建屏幕外緩沖區(qū)做渲染,然后將渲染結(jié)果寫入存儲(chǔ)像素信息的幀緩沖區(qū)中。這個(gè)過程除了要?jiǎng)?chuàng)建額外的緩存,還涉及兩次較為耗時(shí)的上下文切換:從當(dāng)前屏切到屏幕外緩沖區(qū),再切換回當(dāng)前屏緩沖區(qū),所以如果有大量離屏渲染會(huì)影響幀率。
引起離屏渲染的常見原因有:
重寫drawRect,并調(diào)用Core Graphics接口,會(huì)在CPU上執(zhí)行離屏渲染
UI中有圓角且masksToBounds=Y(jié)ES時(shí),陰影,組透明allowsGroupOpacity=true,光柵化shouldRasterize=true等情況時(shí),會(huì)在GPU上進(jìn)行離屏渲染
我們對常見的情況做了下總結(jié):
a.圓角問題的處理,總結(jié)了五種方案
1.通過CALayer的masksToBounds = true組合cornerRadius來實(shí)現(xiàn)圓角效果。這種方案雖然會(huì)產(chǎn)生離屏渲染,但在圓角圖層上覆蓋新的圖層不會(huì)出現(xiàn)圓角被新圖層覆蓋的問題,較為通用
2.只設(shè)置cornerRadius,可以避免離屏渲染,但可能被新加的圖層覆蓋,導(dǎo)致圓角出不來,應(yīng)用場景有限
3.通過后臺線程自繪,生成圖片,方案較為通用,而且可以解決系統(tǒng)圓角的某些顯示問題(下面會(huì)講),但繪制函數(shù)較為耗時(shí)。
4.用圖片遮罩來處理圓角:制作一張四周圓角外帶顏色中間透明的圖片,遮到需要圓角的VIEW上。渲染時(shí)只需要進(jìn)行圖層混合,相比離屏渲染性能好的多。但由于各處圓角尺寸不固定,而且要求透明色區(qū)域外的顏色跟圓角的superview背景色一致,很難做成通用方案。
- 后端提供帶圓角的圖片,這個(gè)方案性能最好,前端無工作量,但比較依賴后端服務(wù)能力。
當(dāng)圓角是一個(gè)整圓,并且指定了寬線條外框時(shí)(比如頭像處理成一個(gè)圓形的),系統(tǒng)繪制的圓圈(上文提到的方案1)周邊可能會(huì)顯示出不太明顯的雜色點(diǎn),用自繪(上文提到的方案3)就沒有這個(gè)問題.我們基礎(chǔ)庫需要考慮通用性,所以組合了1、3兩種方案,優(yōu)先使用系統(tǒng)實(shí)現(xiàn),有顯示雜色情況時(shí)使用方案3.
b.陰影:陰影會(huì)降低流暢性。解決方案:1.跟UED要一張不帶中間內(nèi)容的陰影外框圖貼到最底層。 2.如果layer尺寸是固定的,不需要頻繁更改其尺寸,可以使用shadowpath代替shadowoffset。
c.組透明度allowsGroupOpacity:IOS7之后默認(rèn)是開啟的。開啟后會(huì)使子Layer繼承其父layer的透明度。如果不用處理透明,可以關(guān)閉它,以提高性能。
d.光柵化shouldRasterize:光柵化即將渲染過的layer臨時(shí)緩存為位圖,以供將來渲染使用。這個(gè)選項(xiàng)會(huì)增加內(nèi)存的使用,導(dǎo)致渲染時(shí)間變長。但如果VIEW層級較多效果復(fù)雜,且內(nèi)容不變,開啟后有利于增強(qiáng)性能。
2.關(guān)于Blending圖層像素混合
Blending概念:可以想象你手里拿著幾張塑料卡片。當(dāng)前面的塑料片不透明時(shí),我們看到的只是離你最近那張的顏色(系統(tǒng)只繪制最頂層VIEW顏色);但當(dāng)卡片是半透明時(shí),我們看到的可能是多張卡片的混合色(系統(tǒng)對多個(gè)layer內(nèi)的像素值做疊加合成處理)。所以設(shè)計(jì)時(shí)建議優(yōu)先考慮用不透明的圖層。
3.如果你重寫了UIView的drawRect方法,考慮是否打開以下兩個(gè)開關(guān)
a. clearsContextBeforeDrawing
這個(gè)值可以決定在drawRect調(diào)用時(shí)是否清理之前顯示的內(nèi)容。系統(tǒng)默認(rèn)開啟,以保證你在重繪時(shí)渲染區(qū)是“干凈”的,即被刷新為(R:0,G:0,B:0,A:0)黑透明色。有時(shí)我們只需更新一小部分區(qū)域,此時(shí)這個(gè)清理步驟并不是必須的,我們可以設(shè)置屬性=NO來提高繪制性能。
b. drawsAsynchronously異步繪制開關(guān)
開啟后drawRect,drawInContext雖然仍在主線程調(diào)用,但這里的代碼不會(huì)做任何事情,真正的繪制會(huì)異步化到后臺線程。由于異步化系統(tǒng)需要做更多的處理,需要測試對比開啟關(guān)閉的效果后再?zèng)Q定是否開啟。
4.CGRect
這是個(gè)容易被忽略的優(yōu)化點(diǎn)。由于我們card化大多UI元素frame是經(jīng)計(jì)算得出的,很多CGRect存儲(chǔ)的浮點(diǎn)型轉(zhuǎn)化到屏幕像素點(diǎn)后也不是整數(shù)。這個(gè)問題可能導(dǎo)致圖形邊緣模糊,還會(huì)導(dǎo)致GPU做更多的抗鋸齒運(yùn)算。應(yīng)盡量保證其映射為屏幕像素點(diǎn)后還為整數(shù)值。
5.檢查后臺返回的圖片
顯示區(qū)域和后臺給的圖片尺寸應(yīng)基本一致。圖片分辨率過高不僅解碼慢,內(nèi)存占用高(比如一張3x3圖片解碼成位圖后將會(huì)是2x2分辨率圖片的2.25倍),渲染時(shí)對圖像放縮也會(huì)耗費(fèi)性能。
6.其他
如果使用SDWebimage下載圖片,并且下載完成后需要對圖片重繪(比如圓角化,模糊化后再展示,可以在請求圖片的時(shí)候,設(shè)置SDWebImageAvoidAutoSetImage,防止sd設(shè)置一張并不需要展示的圖片。
對于重用的cell,設(shè)置數(shù)據(jù)前,判斷使用的model與重用前的是否相同,再?zèng)Q定是否需要再執(zhí)行UI重新布局。
檢查下有無過于復(fù)雜的VIEW層級,盡量減少或合并一些VIEW層級;如果不需處理觸摸事件,可以用layer代替UIView。
針對特定低端機(jī)型做優(yōu)化, 比如降低動(dòng)畫效果,減少陰影, 關(guān)閉圓角。
檢查有無線程鎖操作,避免主線程對鎖的訪
總結(jié)
隨著APP代碼的復(fù)雜,流暢問題逐步演化為多因素疊加一起相互影響的問題.比如剩余內(nèi)存量,APP線程數(shù)量,CPU頻率,操作系統(tǒng)版本(即使不降頻,這幾年IOS每個(gè)版本新系統(tǒng)整體性能比舊版本要差)。業(yè)內(nèi)也有不少探索,通過將UI相關(guān)的計(jì)算并行化,提供線程調(diào)度管理及預(yù)加載等機(jī)制來保證流暢性,歡迎就此多做交流。此文拋磚引玉,以供參考。
更多文章
CocoaPods開源庫的搭建
CocoaPods搭建私有庫
CocoaPods搭建私有庫遇到問題
CocoaPods私有庫的升級維護(hù)
SKStoreReviewController之程序內(nèi)評價(jià)
App應(yīng)用程序圖標(biāo)的動(dòng)態(tài)更換
開源框架 MGJRouter_Swift
iOS的MVP設(shè)計(jì)模式
iOS插件化
iOS FMDB的使用
Swift之ReactiveSwift
OC之ReactiveCocoa
OC之ReactiveCocoa進(jìn)階
iOS 性能考慮