前言
剛開始學(xué)習(xí)iOS開發(fā)的時(shí)候就有前輩說,國內(nèi)的iOS程序員在國外被戲稱為UITableView開發(fā)者。不管是為了適配4S以下機(jī)型的屏幕比例,還是Cell重用機(jī)制對(duì)內(nèi)存負(fù)荷的降低等原因,都讓我們無數(shù)次在UIViewController新建完成后立刻添加一個(gè)UITableView上去。無論如何,UITableView的開發(fā)技巧是任何iOS程序員必須掌握的基本功,對(duì)它的優(yōu)化知識(shí)也大部分能間接推演到整個(gè)APP的優(yōu)化中去。在這里結(jié)合本人平時(shí)收集的一些資料,對(duì)這些UITableView的優(yōu)化手段做一下總結(jié)。
一、入門優(yōu)化
-
使用Cell重用機(jī)制
UITableViewCell的重用機(jī)制是UITableView的核心。UITableView只會(huì)創(chuàng)建控件可視范圍數(shù)量的UITableViewCell,每當(dāng)UITableViewCell滑出范圍時(shí),就會(huì)放入到一緩存池當(dāng)中,當(dāng)要顯示新的UITableViewCell時(shí),先去緩存池中取,若沒有可用的,才會(huì)重新創(chuàng)建。這樣可以極大的減少內(nèi)存的開銷。
//注冊(cè)cell
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
//獲取cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
推演至APP優(yōu)化:
盡量利用lazy initialize (延遲加載) 推遲對(duì)象創(chuàng)建的時(shí)間,并把對(duì)象的創(chuàng)建分散到多個(gè)任務(wù)中去。如果對(duì)象可以復(fù)用,并且復(fù)用的代價(jià)比釋放、創(chuàng)建新對(duì)象要小,那么這類對(duì)象應(yīng)當(dāng)盡量放到一個(gè)緩存池里復(fù)用。
-
異步加載并緩存Cell所需的網(wǎng)絡(luò)資源
之后再切換到回dispatch_get_main_queue進(jìn)行設(shè)置,將資源緩存到內(nèi)存和本地待復(fù)用(如圖片資源SDWebImage,視頻資源等),不可以阻塞主線程影響交互體驗(yàn)。
-
不要在
cellForRowAtIndexPath中執(zhí)行addSubview或addSublayer
當(dāng)列表中 cell 的樣式隨數(shù)據(jù)源產(chǎn)生變化時(shí),有時(shí)候我們會(huì)利用addSubview、addSublayer、viewWithTag、removeFromSuperview等手段進(jìn)行控制,這對(duì)內(nèi)存和CPU都會(huì)產(chǎn)生一點(diǎn)負(fù)荷。如果 Cell 樣式變化很大,直接創(chuàng)建新的自定義 Cell 來解決這個(gè)問題;如果樣式變化不大,使用hidden屬性進(jìn)行樣式控制。
Cell高度計(jì)算(階段一)
如果 rowHeight , sectionFooterHeight 和 sectionHeaderHeight 固定,則直接按參數(shù)設(shè)定行高,不要請(qǐng)求delegate。
遇到 cell 高度動(dòng)態(tài)變化的列表,使用 UITableView+FDTemplateLayoutCell 框架+AutoLayout進(jìn)行Cell的高度計(jì)算。
作者使用和每個(gè) Cell 重用標(biāo)識(shí)UITableViewCell ReuseID一一對(duì)應(yīng)的臨時(shí) Cell ,利用iOS6的APIsystemLayoutSizeFittingSize進(jìn)行Cell的高度計(jì)算并緩存起來,提升 cell 高度計(jì)算的效率,使用起來也非常簡單。
需要注意的是對(duì) cell 的約束要做到保證cell 的 contentView 內(nèi)部上下左右所有方向都有約束支撐,這是systemLayoutSizeFittingSize計(jì)算出內(nèi)容 size 的依托。作者在這篇博客詳細(xì)介紹了框架的實(shí)現(xiàn)原理和研發(fā)的歷程。** 盡可能讓 Cell 的所有 Subview 不透明**
透明視圖的存在讓GPU需要進(jìn)行更多混合圖層的計(jì)算(多層layer疊加時(shí)應(yīng)在像素點(diǎn)上顯示的顏色)以得出最終要顯示的效果。在UIScrollView上進(jìn)行滑動(dòng)時(shí),如果視圖上有透明 layer ,GPU需要不停地做更多的工作。
所以應(yīng)盡可能避免讓視圖的alpha值不等于 1 或 0 ,并設(shè)置非透明視圖的layer.opaque屬性為YES,避免無用的透明度通道合成。
推演至APP優(yōu)化:
同理如上。
以上都是優(yōu)化UITableView的一些初步手段,特點(diǎn)是實(shí)現(xiàn)過程的工作量不大、迭代更改源代碼效率高,對(duì)付一般的 Cell 可以說綽綽有余。
二、進(jìn)階優(yōu)化
棄用 AutoLayout
Autolayout是蘋果官方推薦使用提升開發(fā)效率的重要工具,但是Autolayout對(duì)于復(fù)雜視圖來說常常會(huì)產(chǎn)生嚴(yán)重的性能問題。隨著視圖數(shù)量的增長和UITableView的滑動(dòng),Autolayout對(duì)控件的重新布局帶來的 CPU 消耗會(huì)呈指數(shù)級(jí)上升。
所以如果你對(duì)UITableView的性能有很高要求,應(yīng)該盡量避免使用AutoLayout,用手工計(jì)算和改變frame的方式來控制視圖。Cell高度計(jì)算(階段二)
Cell高度計(jì)算階段一的方案雖然方便且減少了一定的損耗,但是當(dāng) Cell 內(nèi)容繁雜時(shí),你會(huì)明顯感覺到列表滑動(dòng)時(shí)的卡頓感(特別是未緩存 Cell 高度時(shí))。
這是因?yàn)?code>systemLayoutSizeFittingSize方案終究是AutoLayout的產(chǎn)物,在靜態(tài)頁面對(duì)所有視圖的重新布局只會(huì)在控制器初始化時(shí)阻塞主線程,用戶體驗(yàn)不會(huì)收到太多影響。但在UIScrollView的滑動(dòng)過程中阻塞主線程,就很蛋疼了。
所以最好的方案,還是在獲取到 Cell 對(duì)應(yīng)的 Model 之后,手工計(jì)算 Cell 的高度并緩存起來。
在這里提供一個(gè)思路:在 Model 里添加一個(gè)cellHeight屬性,計(jì)算 Cell 高度的方法作為 Cell 的類方法,在對(duì)應(yīng)需要設(shè)置或更新 Cell 高度的時(shí)候調(diào)用并對(duì)cellHeight賦值。
看過一篇文章把計(jì)算高度的方法放在 Model 中,這樣做不好的原因是,一方面不利于架構(gòu)分層,計(jì)算 Cell 高度并不是 Model 的任務(wù);另一方面一個(gè) Model 可能要對(duì)應(yīng)用來顯示多種 Cell ,總不能添加一種 Cell 就在 Model 里添加一個(gè)計(jì)算高度的方法。Cell中不需要交互的視圖,用
CALayer代替UIView來繪制
UIView很重,創(chuàng)建和銷毀都比CALayer要耗費(fèi)資源。我見過無數(shù)份新手的代碼,生成height為 1 的UIView來繪制分割線。對(duì)于沒有觸控響應(yīng)事件需求的視圖,盡量用CALayer代替UIView來節(jié)省損耗。
推演至APP優(yōu)化:
整個(gè)APP都應(yīng)該遵循這個(gè)原理,盡量用輕量級(jí)的對(duì)象來代替重量級(jí)的對(duì)象實(shí)現(xiàn)功能。
避免觸發(fā)圖片縮放
首先我們開啟模擬器->Debug->Color Misaligned Images 選項(xiàng),會(huì)看到項(xiàng)目里所有觸發(fā)了圖片縮放的視圖上都多了一層黃色的遮罩,這些視圖大部分都是圖片與視圖大小比例不同,而系統(tǒng)進(jìn)行這些調(diào)整相當(dāng)耗費(fèi)資源。
最好的解決方案當(dāng)然是讓UI切出各種尺寸和比例不同圖片,一步到位,但是這會(huì)讓項(xiàng)目包的大小劇增,而且大部分情況要顯示的圖片都是從服務(wù)器獲取的,大小比例根本不可控。
所以一般我們使用折中方案:后臺(tái)繪制對(duì)應(yīng)視圖大小和所需UIViewContentMode的圖片,再切換到主線程進(jìn)行設(shè)置。避免離屏渲染
盡量避免使用 CALayer 的 border、圓角、陰影、遮罩(mask)等屬性,對(duì)此我特地寫了這篇文章闡述大部分會(huì)導(dǎo)致離屏渲染的操作和對(duì)應(yīng)的解決方案,歡迎閱讀。重用時(shí)間處理類
NSDateFormatter和NSCalendar是常用的時(shí)間處理類,然而他們的初始化過程十分長久。將他們用單例模式緩存起來并在APP啟動(dòng)時(shí)進(jìn)行初始化,以優(yōu)化時(shí)間轉(zhuǎn)換過程的耗時(shí)。
進(jìn)階優(yōu)化的手段適用于比較復(fù)雜的 Cell 設(shè)計(jì),或是入門優(yōu)化后仍然感覺到卡頓和低幀數(shù)現(xiàn)象的情況。
三、錙銖必較優(yōu)化
以下的UITableView優(yōu)化手段,非到萬不得已或是對(duì)性能有極致要求的地步,不推薦使用,過早和過度的優(yōu)化是完全沒有必要的。做性能優(yōu)化時(shí),也最好是走修改代碼 -> Profile -> 修改代碼這樣一個(gè)流程,優(yōu)先解決最值得優(yōu)化的地方。
-
Cell高度計(jì)算(階段三)
前面階段二,我們提前計(jì)算了 Cell 的高度并緩存在 Model 中,提升了計(jì)算高度的效率。然而在cellForRowAtIndexPath回調(diào)中,對(duì) Cell 進(jìn)行繪制時(shí),我們?nèi)匀恍枰獙?duì) Cell 中的 Subviews 例如UILabel等進(jìn)行文字高度計(jì)算,如果這種需要?jiǎng)討B(tài)計(jì)算高度類型的視圖數(shù)量太多,仍然會(huì)阻塞線程對(duì)用戶體驗(yàn)有所影響。
所以我們可以把 Cell 內(nèi)部所有動(dòng)態(tài)變化的控件高度用一個(gè) CellSubViewsModel 緩存下來并動(dòng)態(tài)管理,就在階段二計(jì)算 Cell 高度時(shí),一定會(huì)涉及到其 Subviews 的尺寸計(jì)算,這個(gè)時(shí)候就可以初始化這個(gè) CellSubViewsModel 并將它賦值作為數(shù)據(jù)源 Model 的一部分,在cellForRowAtIndexPath回調(diào)中直接讀取Cell各 Subviews 的尺寸并進(jìn)行賦值,使數(shù)據(jù)源賦值和視圖繪制成為cellForRowAtIndexPath中唯二的工作。 - **使用 CoreText 異步繪制文本 **
屏幕上能看到的所有文本內(nèi)容控件,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView等),其排版和繪制都是在主線程進(jìn)行的,當(dāng)顯示大量文本時(shí),CPU 的壓力會(huì)非常大。對(duì)此解決方案只有一個(gè),那就是用最底層的 CoreText 對(duì)文本異步繪制。盡管這實(shí)現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢(shì)也非常大,CoreText 對(duì)象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整UILabel大小時(shí)算一遍、UILabel繪制時(shí)內(nèi)部再算一遍);CoreText 對(duì)象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染。 - **使用 CoreGraphic 異步解碼+繪制圖片 **
當(dāng)圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)會(huì)進(jìn)行數(shù)據(jù)解碼。這一步是發(fā)生在主線程的,并且不可避免。
所以我們可以走在后臺(tái)線程先把圖片繪制到CGBitmapContext中,然后從 Bitmap 直接創(chuàng)建圖片這樣的流程,把主線程的解碼和繪制工作利用 CoreGraphic 放到后臺(tái)來執(zhí)行。
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
- **異步繪制+及時(shí)取消繪制 **
當(dāng)列表滑動(dòng)過快時(shí),使用異步繪制有時(shí)候會(huì)出現(xiàn)異步繪制過程還沒觸發(fā),Cell 就已經(jīng)離開了屏幕顯示范圍的情況,此時(shí)應(yīng)及時(shí)取消繪制動(dòng)作。
參考鏈接
iOS應(yīng)用性能調(diào)優(yōu)的25個(gè)建議和技巧
ibireme--iOS 保持界面流暢的技巧