序言
開始之前, 簡要介紹一下移動客戶端的動態(tài)化排版方案.為滿足UI布局的靈活和后端可控性, 移動端開發(fā)了基于Card的動態(tài)排版渲染引擎:前后端制定好協(xié)議, 客戶端解析后端下發(fā)的描述信息,構(gòu)建和拼接不同UI元素慢洋。 相較于Native客戶端固化布局, 動態(tài)化方案由于事先不知道UI屬性和確切尺寸备禀,需要動態(tài)創(chuàng)建并計算UI元素顯示區(qū)域斤寇。 這對代碼性能優(yōu)化提出了更高的要求. 本文就幀率測試方法和優(yōu)化經(jīng)驗做下總結(jié).
工具選擇
檢測幀率,使用CADisplayLink API
檢測函數(shù)執(zhí)行耗時情況表箭,使用XCode自帶的TimeProfiler工具
檢測渲染問題,使用模擬器Debug菜單下自帶的離屏及圖層顏色混合檢測工具
大家平時檢測幀率可能常用TimeProfile钮莲。該工具雖然功能強大免钻,但不夠輕量、準(zhǔn)確崔拥。應(yīng)用復(fù)雜時极舔,由于這個工具需要跟設(shè)備通訊,會頻繁讀取設(shè)備狀態(tài)链瓦,收集代碼堆棧信息拆魏,產(chǎn)生的數(shù)據(jù)量非常大.我們測試時幾分鐘有時產(chǎn)生上GB數(shù)據(jù)。如果只是獲取幀率,建議大家使用輕量的CADisplayLink稽揭,只有當(dāng)需要獲取更詳盡的信息時俺附,才考慮使用TimeProfile。
如何優(yōu)化
可以把這個階段分成兩階段: 定位主線程耗時代碼和針對渲染問題優(yōu)化溪掀。前者可通過TimeProfile統(tǒng)計到每個函數(shù)的耗時情況事镣。先解決可能阻塞主線程的代碼,比如有無讀寫IO操作(將其放入非主線程執(zhí)行)揪胃,有無耗時較為明顯的函數(shù)璃哟,最后再通過模擬器定位離屏渲圖層混合的問題,尋找優(yōu)化方案喊递。
階段一:下面介紹下我們優(yōu)化過程中統(tǒng)計出來的一些開銷較高的系統(tǒng)API (可能你也遇到過)
1.字符格式化操作
+(instancetype)stringWithFormat:(NSString *)format, …
當(dāng)代碼中調(diào)用該接口較少時随闪,你可以略過這個問題。但當(dāng)主線程中大量的使用該API時骚勘,這個函數(shù)的耗時會變得明顯铐伴, 因為這個函數(shù)的執(zhí)行效率并不高。
解決方案:使用C函數(shù)俏讹,比如asprintf,snprintf等創(chuàng)建char当宴,然后用char構(gòu)建NSString。
2.圖片資源訪問
+(UIImage *)imageNamed:(NSString *)name
調(diào)用該接口加載圖片后泽疆,系統(tǒng)會緩存該圖片户矢,以加快下次訪問。但在系統(tǒng)壓力較大的低端機型上殉疼,反復(fù)調(diào)用該接口獲取某張固定圖片梯浪,時間還是會很長。我們用TimeProfile也抓到了該函數(shù)取占位圖時耗時較長的情況瓢娜。從原理上看挂洛,該接口要考慮不同擴展名、不同機型下最佳適配資源(2x,3x分辨率圖片)恋腕,根據(jù)傳入的文件名做模糊匹配抹锄。所以其效率也不是很高。
解決方案:1.使用分辨率更小的圖片荠藤,這有助于縮短第一次加載時間伙单。2.如果該圖片屬于公共訪問非常頻繁的資源(比如占位圖),通過該接口獲取到圖片內(nèi)存地址后哈肖,用全局指針保存起來吻育。再次訪問時可以直接使用保存好的指針,完全不會占用主線程時間淤井。
3.文本繪制區(qū)域計算
-(CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context布疼;
- (CGSize)sizeThatFits:(CGSize)size;
當(dāng)文本控件較多摊趾,滑動過程中頻繁使用這類接口計算文本顯示區(qū)域,會占用較多主線程時間游两。
解決方案:
從UI設(shè)計上給定一個固定顯示區(qū)域砾层,讓系統(tǒng)對過長的文本自動截斷,從而避免調(diào)用顯示接口贱案。
2.如果方案1行不通肛炮,可以在調(diào)用一次后把計算結(jié)果緩存起來。用戶再次訪問時宝踪,直接使用已計算好的數(shù)值侨糟。這個方案需要考慮影響計算結(jié)果的因素,比如字體瘩燥、字號秕重、限定的寬高、行數(shù)厉膀、行距溶耘、截斷方式等。如果將這么多變動的因素組合起來查詢站蝠,效率會比較低汰具。 我們的方法是將這些數(shù)據(jù)打包成一個對象,相當(dāng)于計算結(jié)果跟原始的屬性綁定到同一個指針指向的空間里了菱魔,這樣使用時不需查詢,通過指針就可直接訪問吟孙。
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;
…
解決方案:如果你的程序運行過程中有較多調(diào)用這類動態(tài)插入或者調(diào)整View層級的代碼澜倦,可以在創(chuàng)建view時將層級固定下來,并對臨時用不到的view設(shè)置為隱藏杰妓,再在合適的時機顯示出來藻治。
5.NSScan的使用
數(shù)字跟字母混合情況下,有很多人會選用這個API做數(shù)值轉(zhuǎn)換巷挥,用來分離出數(shù)值部分桩卵,但經(jīng)測試,該API性能并不好倍宾。
解決方案:1.大多簡單的數(shù)字跟字母混合字符串雏节,直接轉(zhuǎn)換即可。比如要取出字符16px里的16出來高职,使用intvalue就可以钩乍。2.也可使用strtoul(const char *nptr,char **endptr,int base ),比如一個色值數(shù)據(jù)#6C6C6C怔锌,使用該接口配合位運算寥粹,能很高效的分離出RGB3個10進制數(shù)值來变过。
階段二:渲染優(yōu)化
渲染層面,影響流暢性的因素主要有離屏渲染和圖層混合涝涤。系統(tǒng)也提供了一些優(yōu)化開關(guān)媚狰,默認(rèn)的優(yōu)化開關(guān)是關(guān)閉的,需要根據(jù)UI特點阔拳,驗證后再決定是否開啟崭孤。下面介紹這方面的知識。
1.關(guān)于離屏渲染Off-Screen Rendering vs On-Screen Rendering
Off-Screen Rendering(離屏渲染)需要先創(chuàng)建屏幕外緩沖區(qū)做渲染衫生,然后將渲染結(jié)果寫入存儲像素信息的幀緩沖區(qū)中裳瘪。這個過程除了要創(chuàng)建額外的緩存,還涉及兩次較為耗時的上下文切換:從當(dāng)前屏切到屏幕外緩沖區(qū)罪针,再切換回當(dāng)前屏緩沖區(qū)彭羹,所以如果有大量離屏渲染會影響幀率。
引起離屏渲染的常見原因有:
重寫drawRect,并調(diào)用Core Graphics接口泪酱,會在CPU上執(zhí)行離屏渲染
UI中有圓角且masksToBounds=Y(jié)ES時派殷,陰影,組透明allowsGroupOpacity=true墓阀,光柵化shouldRasterize=true等情況時,會在GPU上進行離屏渲染
我們對常見的情況做了下總結(jié):
a.圓角問題的處理毡惜,總結(jié)了五種方案
1.通過CALayer的masksToBounds = true組合cornerRadius來實現(xiàn)圓角效果。這種方案雖然會產(chǎn)生離屏渲染斯撮,但在圓角圖層上覆蓋新的圖層不會出現(xiàn)圓角被新圖層覆蓋的問題经伙,較為通用
2.只設(shè)置cornerRadius,可以避免離屏渲染勿锅,但可能被新加的圖層覆蓋帕膜,導(dǎo)致圓角出不來,應(yīng)用場景有限
3.通過后臺線程自繪溢十,生成圖片垮刹,方案較為通用,而且可以解決系統(tǒng)圓角的某些顯示問題(下面會講)张弛,但繪制函數(shù)較為耗時荒典。
4.用圖片遮罩來處理圓角:制作一張四周圓角外帶顏色中間透明的圖片,遮到需要圓角的VIEW上吞鸭。渲染時只需要進行圖層混合寺董,相比離屏渲染性能好的多。但由于各處圓角尺寸不固定瞒大,而且要求透明色區(qū)域外的顏色跟圓角的superview背景色一致螃征,很難做成通用方案。
- 后端提供帶圓角的圖片透敌,這個方案性能最好盯滚,前端無工作量踢械,但比較依賴后端服務(wù)能力。
當(dāng)圓角是一個整圓魄藕,并且指定了寬線條外框時(比如頭像處理成一個圓形的)内列,系統(tǒng)繪制的圓圈(上文提到的方案1)周邊可能會顯示出不太明顯的雜色點,用自繪(上文提到的方案3)就沒有這個問題.我們基礎(chǔ)庫需要考慮通用性背率,所以組合了1话瞧、3兩種方案,優(yōu)先使用系統(tǒng)實現(xiàn)寝姿,有顯示雜色情況時使用方案3.
b.陰影:陰影會降低流暢性交排。解決方案:1.跟UED要一張不帶中間內(nèi)容的陰影外框圖貼到最底層。 2.如果layer尺寸是固定的,不需要頻繁更改其尺寸饵筑,可以使用shadowpath代替shadowoffset埃篓。
c.組透明度allowsGroupOpacity:IOS7之后默認(rèn)是開啟的。開啟后會使子Layer繼承其父layer的透明度根资。如果不用處理透明架专,可以關(guān)閉它,以提高性能玄帕。
d.光柵化shouldRasterize:光柵化即將渲染過的layer臨時緩存為位圖部脚,以供將來渲染使用。這個選項會增加內(nèi)存的使用裤纹,導(dǎo)致渲染時間變長委刘。但如果VIEW層級較多效果復(fù)雜,且內(nèi)容不變鹰椒,開啟后有利于增強性能钱雷。
2.關(guān)于Blending圖層像素混合
Blending概念:可以想象你手里拿著幾張塑料卡片。當(dāng)前面的塑料片不透明時吹零,我們看到的只是離你最近那張的顏色(系統(tǒng)只繪制最頂層VIEW顏色);但當(dāng)卡片是半透明時拉庵,我們看到的可能是多張卡片的混合色(系統(tǒng)對多個layer內(nèi)的像素值做疊加合成處理)灿椅。所以設(shè)計時建議優(yōu)先考慮用不透明的圖層。
3.如果你重寫了UIView的drawRect方法钞支,考慮是否打開以下兩個開關(guān)
a. clearsContextBeforeDrawing
這個值可以決定在drawRect調(diào)用時是否清理之前顯示的內(nèi)容茫蛹。系統(tǒng)默認(rèn)開啟,以保證你在重繪時渲染區(qū)是“干凈”的烁挟,即被刷新為(R:0,G:0,B:0,A:0)黑透明色婴洼。有時我們只需更新一小部分區(qū)域,此時這個清理步驟并不是必須的撼嗓,我們可以設(shè)置屬性=NO來提高繪制性能柬采。
b. drawsAsynchronously異步繪制開關(guān)
開啟后drawRect欢唾,drawInContext雖然仍在主線程調(diào)用,但這里的代碼不會做任何事情粉捻,真正的繪制會異步化到后臺線程礁遣。由于異步化系統(tǒng)需要做更多的處理,需要測試對比開啟關(guān)閉的效果后再決定是否開啟肩刃。
4.CGRect
這是個容易被忽略的優(yōu)化點祟霍。由于我們card化大多UI元素frame是經(jīng)計算得出的,很多CGRect存儲的浮點型轉(zhuǎn)化到屏幕像素點后也不是整數(shù)盈包。這個問題可能導(dǎo)致圖形邊緣模糊沸呐,還會導(dǎo)致GPU做更多的抗鋸齒運算。應(yīng)盡量保證其映射為屏幕像素點后還為整數(shù)值呢燥。
5.檢查后臺返回的圖片
顯示區(qū)域和后臺給的圖片尺寸應(yīng)基本一致崭添。圖片分辨率過高不僅解碼慢,內(nèi)存占用高(比如一張3x3圖片解碼成位圖后將會是2x2分辨率圖片的2.25倍)疮茄,渲染時對圖像放縮也會耗費性能滥朱。
6.其他
如果使用SDWebimage下載圖片,并且下載完成后需要對圖片重繪(比如圓角化力试,模糊化后再展示徙邻,可以在請求圖片的時候,設(shè)置SDWebImageAvoidAutoSetImage畸裳,防止sd設(shè)置一張并不需要展示的圖片缰犁。
對于重用的cell,設(shè)置數(shù)據(jù)前怖糊,判斷使用的model與重用前的是否相同帅容,再決定是否需要再執(zhí)行UI重新布局。
檢查下有無過于復(fù)雜的VIEW層級伍伤,盡量減少或合并一些VIEW層級并徘;如果不需處理觸摸事件,可以用layer代替UIView扰魂。
針對特定低端機型做優(yōu)化麦乞, 比如降低動畫效果,減少陰影, 關(guān)閉圓角劝评。
檢查有無線程鎖操作姐直,避免主線程對鎖的訪
總結(jié)
隨著APP代碼的復(fù)雜,流暢問題逐步演化為多因素疊加一起相互影響的問題.比如剩余內(nèi)存量,APP線程數(shù)量,CPU頻率,操作系統(tǒng)版本(即使不降頻,這幾年IOS每個版本新系統(tǒng)整體性能比舊版本要差)。業(yè)內(nèi)也有不少探索蒋畜,通過將UI相關(guān)的計算并行化声畏,提供線程調(diào)度管理及預(yù)加載等機制來保證流暢性,歡迎就此多做交流姻成。此文拋磚引玉插龄,以供參考愿棋。
更多文章
CocoaPods開源庫的搭建
CocoaPods搭建私有庫
CocoaPods搭建私有庫遇到問題
CocoaPods私有庫的升級維護
SKStoreReviewController之程序內(nèi)評價
App應(yīng)用程序圖標(biāo)的動態(tài)更換
開源框架 MGJRouter_Swift
iOS的MVP設(shè)計模式
iOS插件化
iOS FMDB的使用
Swift之ReactiveSwift
OC之ReactiveCocoa
OC之ReactiveCocoa進階
iOS 性能考慮