UITableView的復(fù)用機(jī)制
UITableView首先加載一屏幕(假設(shè)UITableView的大小是整個(gè)屏幕的大小)所需要的UITableViewCell,具體個(gè)數(shù)要根據(jù)每個(gè)cell的高度而定,總之肯定要鋪滿整個(gè)屏幕,更準(zhǔn)確說當(dāng)前加載的cell的高度要大于屏幕高度。然后你往上滑動,想要查看更多的內(nèi)容,那么肯定需要一個(gè)新的cell放在已經(jīng)存在內(nèi)容的下邊。這時(shí)候先不去生成,而是先去UITableView自己的一個(gè)資源池里去獲取。這個(gè)資源池里放了已經(jīng)生成的而且能用的cell。如果資源池是空的話才會主動生成一個(gè)新的cell。那么這個(gè)資源池里的cell又來自哪里呢?當(dāng)你滑動時(shí)視圖是,位于最頂部的cell會相應(yīng)的往上滑動,直到它徹底消失在屏幕上,消失的cell去了哪里呢?你肯定想到了,是的,它被UITableView放到資源池里了。其他cell也是這樣,只要一滑出屏幕就放入資源池。這樣,有進(jìn)有出,總共需要大約一屏幕多一兩個(gè)的cell就夠了。相對于1000來說節(jié)省的資源就是指數(shù)級啊,完美解決了性能問題。
常用的代碼
//方法1
- (void)viewDidLoad {
[super viewDidLoad];
// Setup table view.
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
[self.myTableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:@"MyTableViewCell"];
//或者[UITableView registerNib:forCellReuseIdentifier:]方法
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"MyTableViewCell";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//do something
return cell;
}
//方法2
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"UITableViewCell";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
//不要在這里設(shè)置cell的屬性
}
//do something
return cell;
}
注意這里- [UITableView registerClass:forCellReuseIdentifier:]和- [UITableView registerNib:forCellReuseIdentifier:]這兩個(gè)方法一定不能用錯(cuò),否則就會報(bào)錯(cuò)。
還有就是我注釋中說的不要在if里面設(shè)置cell的屬性,這就是因?yàn)樗膹?fù)用機(jī)制,如果在那里設(shè)置了,你后面的cell因?yàn)槭菑?fù)用的前面的cell,所以不會執(zhí)行if里面的代碼,就會導(dǎo)致你設(shè)置的屬性失效。
UITableView的優(yōu)化
1、cell高度的計(jì)算
如果是固定高度,則直接設(shè)置self.tableView.rowHeight = 88;,不要重寫-(CGFloat)tableView:(UITableView *)tableViewheightForRowAtIndexPath:(NSIndexPath *)indexPath這個(gè)方法,重寫了這個(gè)方法后前面設(shè)置的rowHeight將會失效,并且每次顯示一個(gè)cell都會調(diào)用一次這個(gè)方法,所以重寫這個(gè)方法肯定沒有直接設(shè)置rowHeight效率高。
iOS8之后有了self-sizing cell的概念,cell可以自己算出高度,使用self-sizing cell需要滿足以下三個(gè)條件:
- 使用Autolayout進(jìn)行UI布局約束(要求cell.contentView的四條邊都與內(nèi)部元素有約束關(guān)系)。
- 指定TableView的estimatedRowHeight屬性的默認(rèn)值,就是初始化的一個(gè)默認(rèn)高度。
- 指定TableView的rowHeight屬性為UITableViewAutomaticDimension。
- (void)viewDidload {
self.myTableView.estimatedRowHeight = 44.0;
self.myTableView.rowHeight = UITableViewAutomaticDimension;
}
除了提高cell高度的計(jì)算效率之外,對于已經(jīng)計(jì)算出的高度,也可以進(jìn)行緩存,對于已經(jīng)計(jì)算過的高度,沒有必要進(jìn)行計(jì)算第二次。
2、渲染
GPU渲染機(jī)制:
CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
GPU屏幕渲染有以下兩種方式:
- On-Screen Rendering
意為當(dāng)前屏幕渲染,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行。 - Off-Screen Rendering
意為離屏渲染,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。
離屏渲染的代價(jià)
相比于當(dāng)前屏幕渲染,離屏渲染的代價(jià)是很高的,主要體現(xiàn)在兩個(gè)方面:
- 創(chuàng)建新緩沖區(qū)
要想進(jìn)行離屏渲染,首先要創(chuàng)建一個(gè)新的緩沖區(qū)。
- 上下文切換
離屏渲染的整個(gè)過程,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕。而上下文環(huán)境的切換是要付出很大代價(jià)的。
總之:離屏渲染會付出很大的開銷,能避免離屏渲染盡量就不要離屏渲染
下面的情況或操作會引發(fā)離屏渲染:
- 設(shè)置透明(alpha)屬性
- 為圖層設(shè)置遮罩(layer.mask)
- 將圖層的layer.masksToBounds / view.clipsToBounds屬性設(shè)置為true
- 將圖層layer.allowsGroupOpacity屬性設(shè)置為YES和layer.opacity小于1.0
- 為圖層設(shè)置陰影(layer.shadow *)。
- 為圖層設(shè)置layer.shouldRasterize=true
- 具有l(wèi)ayer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的圖層
- 文本(任何種類,包括UILabel,CATextLayer,Core Text等)使用CGContext在drawRect :方法中繪制大部分情況下會導(dǎo)致離屏渲染,甚至僅僅是一個(gè)空的實(shí)現(xiàn)
iOS 9.0 之前UIimageView跟UIButton設(shè)置圓角都會觸發(fā)離屏渲染。
iOS 9.0 之后UIButton設(shè)置圓角會觸發(fā)離屏渲染,而UIImageView里png圖片設(shè)置圓角不會觸發(fā)離屏渲染了,如果設(shè)置其他陰影效果之類的還是會觸發(fā)離屏渲染的。
常用的幾個(gè)優(yōu)化
1、圓角優(yōu)化
在APP開發(fā)中,圓角圖片還是經(jīng)常出現(xiàn)的。如果一個(gè)界面中只有少量圓角圖片或許對性能沒有非常大的影響,但是當(dāng)圓角圖片比較多的時(shí)候就會APP性能產(chǎn)生明顯的影響。
我們設(shè)置圓角一般通過如下方式:
imageView.layer.cornerRadius=CGFloat(10);
imageView.layer.masksToBounds=YES;
優(yōu)化方案1:使用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個(gè)圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//開始對imageView進(jìn)行畫圖
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
//使用貝塞爾曲線畫出一個(gè)圓形圖
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width/2.0] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//結(jié)束畫圖
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
優(yōu)化方案2:使用CAShapeLayer和UIBezierPath設(shè)置圓角
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(100,100,100,100)];
imageView.image=[UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath=[UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer=[[CAShapeLayer alloc]init];
//設(shè)置大小
maskLayer.frame=imageView.bounds;
//設(shè)置圖形樣子
maskLayer.path=maskPath.CGPath;
imageView.layer.mask=maskLayer;
[self.view addSubview:imageView];
對于方案2的解釋:
- 使用CAShapeLayer(屬于CoreAnimation)與貝塞爾曲線可以實(shí)現(xiàn)不在view的drawRect(繼承于CoreGraphics走的是CPU,消耗的性能較大)方法中畫出一些想要的圖形
- CAShapeLayer動畫渲染直接提交到手機(jī)的GPU當(dāng)中,相較于view的drawRect方法使用CPU渲染而言,其效率極高,能大大優(yōu)化內(nèi)存使用情況。
總的來說就是用CAShapeLayer的內(nèi)存消耗少,渲染速度快,建議使用優(yōu)化方案2。
2、shadow優(yōu)化
對于shadow,如果圖層是個(gè)簡單的幾何圖形或者圓角圖形,我們可以通過設(shè)置shadowPath來優(yōu)化性能,能大幅提高性能。示例如下:
imageView.layer.shadowColor=[UIColor grayColor].CGColor;
imageView.layer.shadowOpacity=1.0;
imageView.layer.shadowRadius=2.0;
UIBezierPath *path=[UIBezierPath bezierPathWithRect:imageView.bounds];
imageView.layer.shadowPath=path.CGPath;
我們還可以通過設(shè)置shouldRasterize屬性值為YES來強(qiáng)制開啟離屏渲染。其實(shí)就是光柵化(Rasterization)。既然離屏渲染這么不好,為什么我們還要強(qiáng)制開啟呢?當(dāng)一個(gè)圖像混合了多個(gè)圖層,每次移動時(shí),每一幀都要重新合成這些圖層,十分消耗性能。當(dāng)我們開啟光柵化后,會在首次渲染的時(shí)候產(chǎn)生一個(gè)位圖緩存,當(dāng)再次使用時(shí)候就會復(fù)用這個(gè)緩存。但是如果圖層發(fā)生改變的時(shí)候就會重新產(chǎn)生位圖緩存。所以這個(gè)功能一般不能用于UITableViewCell中,cell的復(fù)用反而降低了性能。最好用于圖層較多的靜態(tài)內(nèi)容的圖形。而且產(chǎn)生的位圖緩存的大小是有限制的,一般是2.5個(gè)屏幕尺寸。在100ms之內(nèi)不使用這個(gè)緩存,緩存也會被刪除。所以我們要根據(jù)使用場景而定。
3、其他的一些優(yōu)化建議
- 當(dāng)我們需要圓角效果時(shí),可以使用一張中間透明圖片蒙上去
- 使用ShadowPath指定layer陰影效果路徑
- 使用異步進(jìn)行l(wèi)ayer渲染(Facebook開源的異步繪制框架AsyncDisplayKit)
- 設(shè)置layer的opaque值為YES,減少復(fù)雜圖層合成(如果opaque設(shè)置NO,那么Alpha應(yīng)該小于1)
- 盡量使用不包含透明(alpha)通道的圖片資源
- 盡量設(shè)置layer的大小值為整形值
- 直接讓美工把圖片切成圓角進(jìn)行顯示,這是效率最高的一種方案
- 很多情況下用戶上傳圖片進(jìn)行顯示,可以讓服務(wù)端處理圓角
- 使用代碼手動生成圓角Image設(shè)置到要顯示的View上,利用UIBezierPath(CoreGraphics框架)畫出來圓角圖片
3、其它
- 1) 減少視圖的數(shù)目:
我們在cell上添加系統(tǒng)控件的時(shí)候,實(shí)際上系統(tǒng)都會調(diào)用底層的接口進(jìn)行繪制,大量添加控件時(shí),會消耗很大的資源并且也會影響渲染的性能。當(dāng)使用默認(rèn)的UITableViewCell并且在它的ContentView上面添加控件時(shí)會相當(dāng)消耗性能。所以目前最佳的方法還是繼承UITableViewCell,并重寫drawRect方法,并且這里的繪制過程可以通過多線程異步繪制。 - 2)減少多余的繪制操作:
在實(shí)現(xiàn)drawRect方法的時(shí)候,它的參數(shù)rect就是我們需要繪制的區(qū)域,在rect范圍之外的區(qū)域我們不需要進(jìn)行繪制,否則會消耗相當(dāng)大的資源。 - 3)不要給cell動態(tài)添加subView:
在初始化cell的時(shí)候就將所有需要展示的添加完畢,然后根據(jù)需要來設(shè)置hide屬性顯示和隱藏。 - 4)滑動時(shí)按需加載對應(yīng)的內(nèi)容:
滑動很快時(shí),只加載目標(biāo)范圍內(nèi)的cell,這樣按需加載(配合SDWebImage),極大提高流暢度,但是這樣在滑動過程中就會暫時(shí)顯示空白,這就需要在性能和用戶體驗(yàn)中權(quán)衡了。
最后安利一篇YY大神的博客
YY大神:iOS 保持界面流暢的技巧.