UITableView可以說(shuō)是使用頻率最高而且話題最多的UIKit組件了,他也是MVC的完美體現(xiàn)。
一. 原理篇
1.1 復(fù)用機(jī)制
UITableView是繼承自UIScrollView,其最核心的思想就是UITableViewCell的復(fù)用機(jī)制。初始化的時(shí)候他會(huì)先創(chuàng)建cell的緩存字典和section的緩存array,以及一個(gè)用于存放復(fù)用cell的mutableSet。并且它會(huì)去創(chuàng)建顯示的N+1個(gè)cell,其他都是從中取出來(lái)重用。
每當(dāng)Cell滑出屏幕時(shí),就會(huì)放入到set中(相當(dāng)于一個(gè)重用池),當(dāng)要顯示某一位置的Cell時(shí),會(huì)先去集合中取,如果有,就直接拿來(lái)顯示;如果沒(méi)有,才會(huì)創(chuàng)建。這樣做的好處可想而知,極大的減少了內(nèi)存的開(kāi)銷(xiāo)。
復(fù)用機(jī)制如下:
維護(hù)一個(gè)重用隊(duì)列
當(dāng)元素離開(kāi)可見(jiàn)范圍時(shí),
removeFromSuperview
并加入重用隊(duì)列(enqueue)當(dāng)需要加入新的元素時(shí),先嘗試從重用隊(duì)列獲取可重用元素(dequeue)并且從重用隊(duì)列移除
如果隊(duì)列為空,新建元素
-
這些一般都在
scrollViewDidScroll:
方法中完成?
1.2 調(diào)用流程
初始化的時(shí)候內(nèi)部會(huì)對(duì)_needsReload
進(jìn)行標(biāo)記,之后調(diào)用setNeedsLayout
方法。對(duì)于UIView的setNeedsLayout
方法,在調(diào)用后Runloop會(huì)在即將到來(lái)的周期調(diào)用displayIfNeeded
標(biāo)記,如果為YES則會(huì)進(jìn)行drawRect
視圖重繪。當(dāng)RunLoop到來(lái)時(shí),開(kāi)始重回過(guò)程即調(diào)用layoutSubViews
方法,且改方法是被重寫(xiě)過(guò)的。
它會(huì)在內(nèi)部調(diào)用reloadData
方法,在里面會(huì)對(duì)每個(gè)cell進(jìn)行removeFromSuperview
操作(為了指針懸掛的情況,有可能某個(gè)cell被其他視圖引用),以及清除cell緩存字典和復(fù)用set。
之后會(huì)再次去更新緩存:他會(huì)先移除每個(gè)section的header和footer視圖,然后根據(jù)DataSource中實(shí)現(xiàn)的delegate方法重新設(shè)置對(duì)應(yīng)的參數(shù),比如:titleForHeader,titleForFooter,heightForHeader,HeightForFooter,heightForRow,numberOfRows等。
重新緩存完成之后,那么就是進(jìn)行布局更新了。他會(huì)先去獲取容器視圖相對(duì)于父視圖的坐標(biāo)以及偏移量,然后依次將header,cell,footer取出來(lái)添加到self上。
我們都知道,UITableView是繼承自UIScrollView的,那么需要先確定它的contentSize
及每個(gè)Cell的位置,然后才會(huì)把重用的Cell放置到對(duì)應(yīng)的位置。所以UITableView的回調(diào)順序是先多次調(diào)用tableView:heightForRowAtIndexPath:
以確定contentSize
及Cell的位置,然后才會(huì)調(diào)用tableView:cellForRowAtIndexPath:
,從而來(lái)顯示在當(dāng)前屏幕的Cell。
?
比如:現(xiàn)在需要展示100個(gè)cell,當(dāng)前屏幕顯示10個(gè)。那么刷新tableview的時(shí)候,首先會(huì)調(diào)用100次tableView:heightForRowAtIndexPath:
方法,然后調(diào)用10次tableView:cellForRowAtIndexPath:
方法。滑動(dòng)屏幕時(shí),每當(dāng)Cell進(jìn)入屏幕,都會(huì)調(diào)用一次tableView:heightForRowAtIndexPath:
、tableView:cellForRowAtIndexPath:
方法。所以高度計(jì)算是一個(gè)很有必要優(yōu)化的地方。
二. 優(yōu)化篇
2.1 問(wèn)題分析
- CPU(主要是主線程)/GPU負(fù)擔(dān)過(guò)重或者不均衡(諸如mask/cornerRadius/drawRect/opaque帶來(lái)offscreen rendering/blending等等)。由于所有的UIView都是由CALayer來(lái)負(fù)責(zé)顯示,因此對(duì)Core Animation的了解就變得尤為重要。這里推薦Nick Lockwood的Core Animation: Advanced Techniques一書(shū),其中有對(duì)Core Animation的性能有著非常詳盡的梳理和剖析。
- Autolayout布局性能瓶頸,約束計(jì)算時(shí)間會(huì)隨著數(shù)量呈指數(shù)級(jí)增長(zhǎng),并且必須在主線程執(zhí)行。具體分析可以參考這篇文章:http://floriankugler.com/2013/04/22/auto-layout-performance-on-ios/。這也是為何ASDK拋棄了Autolayout而設(shè)計(jì)了自己的布局系統(tǒng)的重要原因之一(https://github.com/facebook/AsyncDisplayKit/issues/196)。Autolayout在單個(gè)View開(kāi)發(fā)時(shí)能帶來(lái)很多便利,而在一些需要高性能的場(chǎng)景下需要謹(jǐn)慎使用。
- 盡管從iPhone4S(A5)開(kāi)始CPU已經(jīng)采用多核,然而對(duì)于大多數(shù)app來(lái)說(shuō),并行效率仍然非常低下。也就是說(shuō),在app卡頓(主線程所占用的核心滿負(fù)荷)時(shí),往往CPU的其他核心幾乎無(wú)事可做。一般情況下,由于主線程承擔(dān)了絕大部分的工作,僅僅是把主線程的任務(wù)轉(zhuǎn)移一部分給其他線程進(jìn)行異步處理,就可以馬上享受到并發(fā)帶來(lái)的性能提升。這應(yīng)該也是AsyncDisplayKit得名的原因之一。
2.2 優(yōu)化思路
我們知道,當(dāng)用戶開(kāi)始滾動(dòng)或點(diǎn)擊一個(gè)View,所有的事件都會(huì)被送到主線程等待處理。此時(shí)主線程能否抽出足夠充裕的時(shí)間來(lái)處理變得極為重要,尤其是在連續(xù)操作(如UIGestureRecognizer)時(shí),每次touchMoved事件處理都會(huì)占用主線程一定的時(shí)間(如新的UIImageView進(jìn)入視圖,主線程開(kāi)始處理布局或者圖片解碼,而這些需要連續(xù)占用大量CPU時(shí)間)。如果一個(gè)操作耗時(shí)超過(guò)16ms(1000ms/60fps),那就意味著下一幀無(wú)法及時(shí)得到處理,引起丟幀。
2.3 可優(yōu)化點(diǎn)
提前計(jì)算并緩存好高度(布局),因?yàn)閔eightForRowAtIndexPath:是調(diào)用最頻繁的方法。
復(fù)雜界面可采用異步繪制。
在大量圖片展示時(shí),可以滑動(dòng)時(shí)按需加載。
盡量少用或不用透明圖層,多個(gè)透明元素重疊顯示可采用合并成一張圖片顯示。
減少subviews的數(shù)量,如果是不需要交互可以使用CALayer 替換掉 UIView。
在
heightForRowAtIndexPath:
中盡量不使用cellForRowAtIndexPath:
。根據(jù)場(chǎng)景合理使用imageWithContentsOfFile和imageNamed。
頁(yè)面元素多的時(shí)候,減少autolayout布局,采用frame。
緩存NSDateFormatter結(jié)果,不多次創(chuàng)建,及時(shí)釋放。
圖片解碼時(shí),CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會(huì)得到解碼,GPU執(zhí)行,卡主線程。常見(jiàn)的做法是在后臺(tái)線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。
CALayer 的 border、圓角、陰影、遮罩(mask)觸發(fā)的離屏渲染,可開(kāi)啟CALayer.shouldRasterize ,轉(zhuǎn)嫁到CPU上或是截圖或者采用圖片實(shí)現(xiàn)。
-
使用RunLoop和多線程在閑時(shí)處理一些繁重的計(jì)算工作。
?
具體可查看:UITableView+FDTemplateLayoutCell源碼解析
參考文獻(xiàn):
優(yōu)化UITableViewCell高度計(jì)算的那些事