iOS系列開發(fā)-UITableView性能優(yōu)化
在我們的日常開發(fā)中,很多開發(fā)人員最常接觸的就是UITableView或者UICollectionView來布局某些列表等界面.
這里我們就拿UITableView來作為說明內(nèi)容
絕大部分的時(shí)候,一個(gè)UITableView的內(nèi)容不會很多,cell的樣式\高度也不會很多元化,其僅僅作為一個(gè)展示用的UITableView來說,很多時(shí)候其性能都是很不錯(cuò)的.
但是也會有小眾的時(shí)候,一個(gè)作為列表展示的界面會有很多很多數(shù)據(jù),而且是實(shí)時(shí)的會加載很多新的內(nèi)容,表格的樣式也不唯一,有的僅有文字,有的僅有圖片,有的高度很長,在復(fù)雜點(diǎn)的很多cell雖然總體上差不多,但是卻會有很多或多或少的布局上的不一樣或者組件上的差距
當(dāng)然,并不是說有了這些復(fù)雜的內(nèi)容,我們的UITableView就會性能變差,但是我們卻可以說性能上比較差的UITableView,很多原因都是因?yàn)檫@些不定的因素,復(fù)雜的邏輯判斷,復(fù)雜的數(shù)據(jù)處理,復(fù)雜的圖形渲染,復(fù)雜的高度計(jì)算等等導(dǎo)致的.
而且如果表格的性能真的已經(jīng)變差了,那么其調(diào)優(yōu)的步驟是一定需要進(jìn)行的,否則作為開發(fā)人員,我們過不了自己的關(guān),過不了測試的關(guān),過不了產(chǎn)品的關(guān)...
但是說白了UITableView也就那么些方法
我們能夠利用的有哪些?
@property (nonatomic) CGFloat rowHeight;
是的,你會發(fā)現(xiàn)當(dāng)rowHeight時(shí)唯一固定的時(shí)候,往往性能不會太差
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
我們都知道,當(dāng)我們使用了代理的方法之后,上面的行高屬性就會失效了,但是相對應(yīng)的,我們需要給每一行都返回一個(gè)行高,這個(gè)是無法避免的,那么我們能否在這里做些優(yōu)化呢?
答案是肯定的,而且是顯著的.
為什么這么說呢?
當(dāng)我們的UITableView是一個(gè)動(dòng)態(tài)行高的表格的話,我們以往的算法就是動(dòng)態(tài)計(jì)算,什么意思呢?
大致是這樣的
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
DetaileModel *model = _listArray[indexPath.row];//取出模型
return [DetailCell heigthForModel:model];//計(jì)算所需高度并返回
}
但是響應(yīng)的,因?yàn)閮?nèi)容的多樣化,可能有,這部分的計(jì)算本事雖然不會很耗時(shí),但是因?yàn)槲覀價(jià)eloadData的時(shí)候或者滾動(dòng)的時(shí)候,其都需要不停的計(jì)算高度并返回,這樣就會造成很多的展示表格或者創(chuàng)建或者重用表格cell的時(shí)候都需要耗時(shí)在計(jì)算高度的這部分上,那么我們?nèi)绾握{(diào)節(jié)呢?這部分計(jì)算肯定是需要的,此時(shí)你可能會說,iOS現(xiàn)在支持自適應(yīng)高度,
@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is 0, which means there is no estimate
是的,UITableView中,我們可以寫一個(gè)預(yù)估行高,然后使用xib或者storyboard或者手動(dòng)layout的時(shí)候編寫好約束的時(shí)候,即可動(dòng)態(tài)自動(dòng)的返回行高,但這樣的性能其實(shí)更差,時(shí)間全部都在調(diào)節(jié)約束上面了,然后硬生生的依靠約束來調(diào)節(jié)行高,這樣的性能只會更差,不推薦.
所以我們可以采取的是
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
DetaileModel *model = _listArray[indexPath.row];//取出模型
return model.cellHeight;//獲得所需高度并返回
}
如果我們把cellHeight當(dāng)做一個(gè)屬性,提前緩存好,之后直接獲取怎么樣呢?
此時(shí)你就會說,這樣有什么用嗎?
簡單的說幾點(diǎn)
1.使用內(nèi)存換性能
2.不需要重復(fù)計(jì)算
為什么這么說呢?
首先我們會發(fā)現(xiàn),往往我們計(jì)算行高和設(shè)置cell的時(shí)候都是根據(jù)模型數(shù)據(jù)來判斷哪些需要顯示哪些不需要顯示,顯示的話顯示的位置和大小分別是多少,我們都是根據(jù)數(shù)據(jù)模型的內(nèi)容來判斷的.那么我們?nèi)绻@設(shè)置這個(gè)模型數(shù)據(jù)的時(shí)候(網(wǎng)絡(luò)請求回來json轉(zhuǎn)模型的時(shí)候),我們就根據(jù)這個(gè)模型的數(shù)據(jù)添加一個(gè)cellHeight的屬性,并且直接計(jì)算好保存在模型中,當(dāng)做模型的一個(gè)字段,那么我們在reloadData的時(shí)候我們就會發(fā)現(xiàn)我們把計(jì)算高度的事情提前一步做好了,我們只需要返回model的cellHeight即可.而且因?yàn)槠浯鎯υ谠撃P蛿?shù)據(jù)中,那么其跟模型一樣,一直是存在的,我們即可以在重新刷新表格或者滾動(dòng)視圖的時(shí)候都不需要再一次計(jì)算了(只要改模型存在,那么該cellHeight屬性就一直存在).
我們會發(fā)現(xiàn)僅僅是一個(gè)提前計(jì)算并存儲起來,一個(gè)表格的性能就會大大提高,一些稍微不是特別復(fù)雜的表格此時(shí)已經(jīng)就能夠很流暢了.
至此我們會發(fā)現(xiàn)我們使用了類似設(shè)置cell的方式
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellid = @"cellid";
DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:cellid];
if (!cell) {
cell = [[DetailCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellid];
}
cell.model = _listArray[indexPath.row];
return cell;
}
很多的開發(fā)者都使用類似的方式,在cell中有一個(gè)model屬性,或者設(shè)置model的方法,以此來通過傳遞一個(gè)model來設(shè)置數(shù)據(jù),可能在自定義的設(shè)置方法中,可能在model的setter方法里面,此時(shí)我們又會一到一個(gè)新的問題,某些model本身并不深很復(fù)雜,但是我們需要對這些數(shù)據(jù)做一些處理之后在做展示,比如圖文混排,比如拼接分割轉(zhuǎn)換等處理字符的顯示,這些在以往的時(shí)候,我們都是放在設(shè)置cell的數(shù)據(jù)源方法里面,通過模型的傳遞過來,然后通過進(jìn)一步的轉(zhuǎn)化或者修改來決定如何顯示數(shù)據(jù). 但是同樣的,此時(shí),我們就需要不停的在表格更新顯示的時(shí)候來計(jì)算.
此時(shí)你肯定會想到,處理方法和上面一樣,提前計(jì)算好! 是的,我們在設(shè)置模型的時(shí)候就提前計(jì)算好,在模型中添加新的字段,存儲這些需要計(jì)算的數(shù)據(jù),在cell展示的時(shí)候直接拿過來用即可.
但是問題來了,新的數(shù)據(jù)模型仿佛不是數(shù)據(jù)模型了,參與了很多的計(jì)算(內(nèi)容計(jì)算,行高計(jì)算...)
那么我們能不能優(yōu)化一下呢?
這是我們之前的邏輯
這是我們現(xiàn)在的邏輯
我們能夠輕松的優(yōu)化了紅色部分的性能了
原因是我們把所有的需要提前計(jì)算的東西都提前了,將原來的需要計(jì)算的部分全部轉(zhuǎn)化成了一個(gè)內(nèi)存,使用內(nèi)存來代替之前的計(jì)算,在需要的時(shí)候只需要從內(nèi)存中直接拿出來使用即可.優(yōu)點(diǎn)上面簡單的說明了,只需要在賦值的時(shí)候計(jì)算好即可,之后就不需要計(jì)算了,以后的刷新都是直接從內(nèi)存中取出所需要的內(nèi)容或者行高即可
但是,相對應(yīng)的,模型就變的不在像模型了,(此時(shí)的模型中有了計(jì)算行高的部分,有了計(jì)算新的內(nèi)容的部分),并且跟網(wǎng)絡(luò)返回回來的模型已經(jīng)不是同樣的一個(gè)模型數(shù)據(jù)了.比原來有了更多的字段,或許在以后的時(shí)候中會有或多或少的問題,比如字段重復(fù)卻不知道,比如模型傳遞,傳遞多次之后,已經(jīng)不知道哪部分是后來自己添加的屬性了...
那么我們使用新的數(shù)組來替代呢?我們使用一個(gè)新的數(shù)組來替代原來的模型數(shù)組
使用一個(gè)自建的模型類來代替原來的模型類,里面只有需要的內(nèi)容,比如行高,比如所有需要顯示的內(nèi)容,這樣的一個(gè)模型類來組成新的模型數(shù)組.
然后UITableView只需要處理新的模型數(shù)組來呈現(xiàn)即可.
但是這樣的的數(shù)組你會發(fā)現(xiàn),丟失了原來的模型.比如我們點(diǎn)擊某一個(gè)cell的時(shí)候我們需要將模型傳遞到下一個(gè)界面,此時(shí)拿到的數(shù)組就不是該模型了,而是我們自己定義的一個(gè)新的模型,這樣會不會就不太方便呢?
那么我們進(jìn)一步擴(kuò)展,我們創(chuàng)建一個(gè)DetailModelManager的類,這個(gè)類里面有這樣的一個(gè)屬性,model, 我們在其set方法中做幾件事,第一件,就是普通的set,第二件是計(jì)算好一個(gè)cellHeight并保存為DetailModelManager的屬性,而不是model的一個(gè)屬性,計(jì)算好(或者賦值)所有需要顯示的內(nèi)容的字段,并保存為一個(gè)個(gè)的屬性
現(xiàn)在的邏輯變了
其實(shí)熟悉MVVM框架的同學(xué)此時(shí)就肯定笑話我了,這個(gè)不就是MVVM的VM嘛
是的,這其實(shí)就是MVVM的VM部分的一塊優(yōu)勢了,現(xiàn)在的列表數(shù)組已經(jīng)變成了DetailModelManager(ViewModel)數(shù)組了,所有的需要復(fù)雜計(jì)算的部分我們都可以以類似的方式來處理,比如我項(xiàng)目的一個(gè)列表
此時(shí)的列表不是單純的模型數(shù)組
tableView中寫法稍變
原來所有的model部分轉(zhuǎn)化成viewModel部分
那么viewModel部分做了什么?
是的在之前的setModel的方法中我就做了很多事情,把后續(xù)需要計(jì)算的很多東西都計(jì)算結(jié)束并保存為屬性了
至于cell就簡單了,原來的model換成viewModel,原來的model的屬性,現(xiàn)在變成了viewModel的屬性了,而且完全不需要任何計(jì)算.直接都是拿過來使用即可,
后期如果顯示的內(nèi)容不符合需求,只需要修改viewModel的那個(gè)字段的計(jì)算即可.其他任何地方都不需要修改.
呃,不知不覺說了部分的MVVM的內(nèi)容,雖然和UITableView的性能優(yōu)化沒有關(guān)系,但是畢竟是性能優(yōu)化的一個(gè)實(shí)現(xiàn)方式,所以就一并說了一點(diǎn)
- 總之就是行高一定要緩存
另外,我們肯能會遇到這樣的需求,一個(gè)稍微復(fù)雜的cell中可能有九宮格展示圖片(如新浪微博),可能有滾動(dòng)視圖展示圖片(如很多視頻類app),這個(gè)時(shí)候,我們會發(fā)現(xiàn),用來展示圖片的cell展示的內(nèi)容是動(dòng)態(tài)的,即數(shù)量是不定的,于是我們很多人就都會想到,九宮格啥的或者滾動(dòng)視圖啥的,我們都可以用coolectionView來實(shí)現(xiàn)嘛,完全動(dòng)態(tài)保證,多好...
但是其實(shí)這樣做仍然是有很多麻煩的.
首先是層級關(guān)系,一個(gè)cell中包含collectionView等雖然不會有很多影響,但是這個(gè)cell的代碼就會變的很龐大,很麻煩.而且動(dòng)態(tài)添加的方式會很消耗性能,原因上面也差不多說了,我們上面一直就是在做表格的性能優(yōu)化,你在cell中又加一個(gè)表格,雖然不復(fù)雜,但是該遇到的性能問題一個(gè)都不會丟的全部觸發(fā),而且或許是double.
如何做一點(diǎn)優(yōu)化呢?首先我們會發(fā)現(xiàn)諸如新浪微博的九宮格圖片展示樣式其實(shí)也是有規(guī)律的,其最多展示9張圖片.所以我們其實(shí)沒有必要使用動(dòng)態(tài)創(chuàng)建的方式,我們在創(chuàng)建cell的時(shí)候就創(chuàng)建9個(gè)imageView即可.在使用或者復(fù)用的時(shí)候,根據(jù)圖片個(gè)數(shù)或者展示需求來保證其顯示或者隱藏即可.這樣的cell你會發(fā)現(xiàn)在層次上和你創(chuàng)建collectionview是一樣的,但是在展示上,其非動(dòng)態(tài)獲取數(shù)據(jù)源展示和創(chuàng)建cell,其就是簡單的設(shè)置圖片的顯示和隱藏即可,然后根據(jù)圖片的格式,調(diào)整好約束就好了.一個(gè)簡單的處理我們會發(fā)現(xiàn)效果是很明顯的,這樣能盡可能的減少cell創(chuàng)建或從緩存池取時(shí)因?yàn)椴季肿涌丶牡臅r(shí)間.
所以說
所有的子視圖都要預(yù)先創(chuàng)建
如果不需要顯示可以設(shè)置hidden
另外我們是否會發(fā)現(xiàn)我們在開發(fā)app的時(shí)候,A做push動(dòng)作到B.如果B沒有設(shè)置背景色的話,會出現(xiàn)一個(gè)明顯的卡頓?所以避免顏色導(dǎo)致問題,我們最好都給子視圖設(shè)置好背景色.雖然不會有太大的影響,但是還是有點(diǎn)效果的
此外,柵格化也是我們可以關(guān)注的一點(diǎn),首先柵格化是設(shè)計(jì)中的術(shù)語,但是用在我們的開發(fā)同樣適用,當(dāng)我們遇到表格中的cell層級很多的時(shí)候,我們是可以做個(gè)柵格化的操作,就是將 cell 中的所有內(nèi)容,生成一張獨(dú)立的圖像,在屏幕滾動(dòng)時(shí)否纬,只顯示圖像 設(shè)置屬性 self.layer.shouldRasterize = YES;雖然代碼很簡單,但是你會發(fā)現(xiàn)滾動(dòng)的效果會很明顯,當(dāng)然柵格化的同時(shí)必須指定分辨率希痴,否則默認(rèn)使用 1倍的scale 生成圖像! 需要設(shè)置 self.layer.rasterizationScale = [UIScreen mainScreen].scale;
當(dāng)然既然有柵格化優(yōu)化滾動(dòng),那么還有操作來優(yōu)化其他的,比如繪制.
我們在開發(fā)中都會用到SDWebImage等類似的圖片異步加載的.來保證每個(gè)cell中的網(wǎng)絡(luò)圖片在加載數(shù)據(jù)的時(shí)候不會占用主線程,從而來保證cell的流暢性,當(dāng)然,其實(shí)我們不僅僅能做到這點(diǎn),我們還可以設(shè)置cell的異步繪制,如果 cell 比較復(fù)雜忘闻,可以設(shè)置cell圖層的屬性 self.layer.drawsAsynchronously = YES;代碼同樣簡單的可以忽略,但是效果也是顯著的.
所以
- 異步加載是很需要的
所以總的來說我們在開發(fā)表格的時(shí)候,我們需要關(guān)注的其實(shí)無非也就是簡單的幾點(diǎn)
- 對行高的緩存------不要頻繁的動(dòng)態(tài)計(jì)算行高,少使用系統(tǒng)自動(dòng)預(yù)估和動(dòng)態(tài)行高,主動(dòng)負(fù)擔(dān)cell的布局,而不是依靠系統(tǒng)自動(dòng)'撐開',在這塊處理上我們可以使用內(nèi)存換計(jì)算的方式,把內(nèi)存在模型構(gòu)建完成的時(shí)候就算出來,提前保存好.之后直接使用即可.比如把行高計(jì)算放入model、viewmodel的初始化中或者設(shè)置數(shù)據(jù)的方法中,優(yōu)勢是不需要重復(fù)計(jì)算,減少計(jì)算行高時(shí)間花銷
- 子視圖提前創(chuàng)建,不要?jiǎng)討B(tài)創(chuàng)建------對于視圖的創(chuàng)建,每個(gè)人都有自己的方式,但是對于cell中來說,因?yàn)閏ell本身就是一個(gè)動(dòng)態(tài)的,所以在對其復(fù)用或者創(chuàng)建的時(shí)候我們就要盡量避免使用動(dòng)態(tài)的方式創(chuàng)建子視圖,無論是先removeSubViews再addSubView 亦或者是使用collectionView等動(dòng)態(tài)視圖,這些都要在復(fù)用的時(shí)候避免,如果可以,提前創(chuàng)建好需要的子視圖.在需要顯示的時(shí)候顯示,在不需要顯示的時(shí)候隱藏,這樣其創(chuàng)建的耗時(shí)工作僅在第一次創(chuàng)建的時(shí)候,之后的復(fù)用都不會再次發(fā)生創(chuàng)建和刪除視圖等操作,一個(gè)簡單的隱藏就可以達(dá)到我們的需求,何樂不為?
- 圖片等展示異步加載------對于這一點(diǎn),大家都很有心得.或者說大家都會在開發(fā)的時(shí)候很直接的做到.如果講cell中的網(wǎng)絡(luò)圖片的加載使用主線程,那么何止是cell會卡頓,簡直都是太小白的操作了.
- 盡量減少cell的層級------子視圖的層級不易過多,(不僅僅是cell的層級,整個(gè)應(yīng)用的視圖中過多)層級如果太多,系統(tǒng)對視圖的渲染壓力就會相應(yīng)的變大,畢竟很明顯的道理是越少越好蠻.至于如何減少cell的層級,很多方式,比如我們使用drewRect的方式來繪制文字或者圖片,而不使用label等,比如我們使用UItextField來充當(dāng)label,這樣既可以在某些時(shí)候僅展示文字,某些時(shí)候用來編輯文字,做到一個(gè)控件兩種使用方式(在類似需要填寫或者選擇等信息的界面中我都會這么用),當(dāng)然如果你對性能要求很高,且技術(shù)足夠,你可以完全手工繪制cell,對于此你可以簡單的學(xué)習(xí)setNeedsDisplay
- 圖層顏色的選擇------透明圖層對渲染性能會有一定的影響,系統(tǒng)必須將透明圖層與下面的視圖混合起來計(jì)算顏色菩收,并繪制出來。減少透明圖層并使用不透明的圖層來替代它們鲸睛,可以極大地提高渲染速度娜饵。
- 為代理方法瘦身-----自從我們學(xué)會的block的時(shí)候我們會發(fā)現(xiàn)我們在任何地方都習(xí)慣使用block的方式來代替代理,在cell的創(chuàng)建中也不例外.比如cell中包含按鈕的點(diǎn)擊事件,我們現(xiàn)在都會使用block的方式來傳遞出來,但是不知不覺中cell的代理方法我們會發(fā)現(xiàn)很龐大,其實(shí)這樣做是否真的好呢?其實(shí)不是,首先龐大的代碼就會影響閱讀,其次,每一個(gè)block都會開辟一個(gè)空間,所以說你別看只寫了一個(gè)block,但是相對應(yīng)的你卻付出了block*個(gè)數(shù)的內(nèi)存,但其實(shí)他們的內(nèi)容完全一致,代理之所以存在是有其意義的,一味的使用block來替換代理其實(shí)是不對的,我們要在合適的時(shí)候用合適的方法.cell中的按鈕點(diǎn)擊,諸如此類的,仍然建議還是使用代理傳遞出來.你不會發(fā)現(xiàn)性能上的優(yōu)化,你卻能發(fā)現(xiàn)代碼上的優(yōu)化.
- 減少離屏渲染------離屏渲染,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作官辈。設(shè)置了以下屬性時(shí)箱舞,都會觸發(fā)離屏繪制:shouldRasterize(光柵化)masks(遮罩)shadows(陰影)edge antialiasing(抗鋸齒)group opacity(不透明)復(fù)雜形狀設(shè)置圓角等 漸變... ,我們的cell開發(fā)雖然不在,但是不會太過分,我們用到的比較多的就是幾種,如圓角圖片,其會造成很明顯的離屏渲染,從而造成性能上的變差.所以肯定是要對其優(yōu)化的,當(dāng)然網(wǎng)絡(luò)上有很多教你設(shè)置圓角圖片的貼文,不仿看看
- cell柵格化------當(dāng)shouldRasterize設(shè)成true時(shí),layer被渲染成一個(gè)bitmap拳亿,并緩存起來晴股,等下次使用時(shí)不會再重新去渲染了。實(shí)現(xiàn)圓角本身就是在做顏色混合(blending)肺魁,如果每次頁面出來時(shí)都blending电湘,消耗太大,這時(shí)shouldRasterize = yes鹅经,下次就只是簡單的從渲染引擎的cache里讀取那張bitmap胡桨,節(jié)約系統(tǒng)資源。如果在滾動(dòng)tableView時(shí)瞬雹,每次都執(zhí)行圓角設(shè)置昧谊,肯定會阻塞UI,設(shè)置這個(gè)將會使滑動(dòng)更加流暢酗捌。
- 異步加載------不僅僅是網(wǎng)絡(luò)圖片的異步加載.drawsAsynchronously屬性也是我們需要關(guān)注的.
- 性能優(yōu)化不是唯一的固定的,如果你的性能足夠,那么你不優(yōu)化也行,如果你的性能不夠,如何優(yōu)化,優(yōu)化哪些都是可選的.非固定的
性能上的優(yōu)化不僅僅如此,不僅僅是表格,
在這里推薦一部iOS開發(fā)必看書籍,如果融會貫通,你的開發(fā)經(jīng)驗(yàn)足以讓別人仰視對待,
ios核心動(dòng)畫高級技巧(https://www.gitbook.com/book/zsisme/ios-/details)