- UITableView的簡單認(rèn)識
UITableView最核心的思想就是UITableViewCell的重用機制。簡單的理解就是:UITableView只會創(chuàng)建一屏幕(或一屏幕多一點)的UITableViewCell,其他都是從中取出來重用的。每當(dāng)Cell滑出屏幕時,就會放入到一個集合(或數(shù)組)中(這里就相當(dāng)于一個重用池),當(dāng)要顯示某一位置的Cell時,會先去集合(或數(shù)組)中取,如果有,就直接拿來顯示;如果沒有,才會創(chuàng)建。這樣做的好處可想而知,極大的減少了內(nèi)存的開銷。
知道UITableViewCell的重用原理后,我們來看看UITableView的回調(diào)方法。UITableView最主要的兩個回調(diào)方法是tableView:cellForRowAtIndexPath:和tableView:heightForRowAtIndexPath:。理想上我們是會認(rèn)為UITableView會先調(diào)用前者,再調(diào)用后者,因為這和我們創(chuàng)建控件的思路是一樣的,先創(chuàng)建它,再設(shè)置它的布局。但實際上卻并非如此,我們都知道,UITableView是繼承自UIScrollView的,需要先確定它的contentSize及每個Cell的位置,然后才會把重用的Cell放置到對應(yīng)的位置。所以事實上,UITableView的回調(diào)順序是先多次調(diào)用tableView:heightForRowAtIndexPath:以確定contentSize及Cell的位置,然后才會調(diào)用tableView:cellForRowAtIndexPath:,從而來顯示在當(dāng)前屏幕的Cell。
舉個例子來說:如果現(xiàn)在要顯示100個Cell,當(dāng)前屏幕顯示5個。那么刷新(reload)UITableView時,UITableView會先調(diào)用100次tableView:heightForRowAtIndexPath:方法,然后調(diào)用5次tableView:cellForRowAtIndexPath:方法;滾動屏幕時,每當(dāng)Cell滾入屏幕,都會調(diào)用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法。
看到這里,想必大伙也都能隱約察覺到,UITableView優(yōu)化的首要任務(wù)是要優(yōu)化上面兩個回調(diào)方法。事實也確實如此,下面按照我探討進(jìn)階的過程,來研究如何優(yōu)化:
優(yōu)化探索,項目拿到手時代碼是這樣:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
if (!cell) {
cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
}
NSDictionary *dict = self.dataList[indexPath.row];
[cell setContentInfo:dict];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
return cell.frame.size.height;
} ```
看到這段代碼,對于剛畢業(yè)的我來說,覺得還是蠻巧妙的,但巧歸巧,當(dāng)Cell非常復(fù)雜的時候,直接卡出翔了。。。特別是在我的Touch4上,這我能忍?!好吧,依據(jù)上面UITableView原理的分析,我們先來分析它為什么卡?
這樣寫,在Cell賦值內(nèi)容的時候,會根據(jù)內(nèi)容設(shè)置布局,當(dāng)然也就可以知道Cell的高度,想想如果1000行,那就會調(diào)用1000+頁面Cell個數(shù)次tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法,而我們對Cell的處理操作,都是在這個方法里的!什么賦值、布局等等。開銷自然很大,這種方案Pass。。。改進(jìn)代碼。
改進(jìn)代碼后:
-
(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
return [ContacterTableCell cellHeightOfInfo:dict];
} ```
思路是把賦值和計算布局分離。這樣讓tableView:cellForRowAtIndexPath:方法只負(fù)責(zé)賦值,tableView:heightForRowAtIndexPath:方法只負(fù)責(zé)計算高度。注意:兩個方法盡可能的各司其職,不要重疊代碼!兩者都需要盡可能的簡單易算。Run一下,會發(fā)現(xiàn)UITableView滾動流暢了很多。。。
基于上面的實現(xiàn)思路,我們可以在獲得數(shù)據(jù)后,直接先根據(jù)數(shù)據(jù)源計算出對應(yīng)的布局,并緩存到數(shù)據(jù)源中,這樣在tableView:heightForRowAtIndexPath:方法中就直接返回高度,而不需要每次都計算了。
用 “空間換時間”
將計算行高的時間提前到從服務(wù)器摟回數(shù)據(jù)的時候,計算完了高度一并寫回數(shù)據(jù)庫,
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
CGRect rect = [dict[@"frame"] CGRectValue];
return rect.frame.height;
} ```
其實上面的改進(jìn)方法并不是最佳方案,但基本能滿足簡單的界面!記得開頭我的任務(wù)嗎?像朋友圈那樣的圖文混排,這種方案還是扛不住的!我們需要進(jìn)入更深層次的探究:自定義Cell的繪制。
我們在Cell上添加系統(tǒng)控件的時候,實質(zhì)上系統(tǒng)都需要調(diào)用底層的接口進(jìn)行繪制,當(dāng)我們大量添加控件時,對資源的開銷也會很大,所以我們可以索性直接繪制,提高效率。是不是說的很抽象?廢話不多說,直接上代碼:
首先需要給自定義的Cell添加draw方法,(當(dāng)然也可以重寫drawRect)然后在方法體中實現(xiàn):
//異步繪制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = [_data[@"frame"] CGRectValue];
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
//整個內(nèi)容的背景
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
CGContextFillRect(context, rect);
//轉(zhuǎn)發(fā)內(nèi)容的背景
if ([_data valueForKey:@"subData"]) {
[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
CGContextFillRect(context, subFrame);
[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
}
{
//名字
float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
float x = leftX;
float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
[_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
andHeight:rect.size.height];
//時間+設(shè)備
y += SIZE_FONT_NAME+5;
float fromX = leftX;
float size = [UIScreen screenWidth]-leftX;
NSString *from = [NSString stringWithFormat:@"%@ %@", _data[@"time"], _data[@"from"]];
[from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
andHeight:rect.size.height andWidth:size];
}
//將繪制的內(nèi)容以圖片的形式返回,并調(diào)主線程顯示
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (flag==drawColorFlag) {
postBGView.frame = rect;
postBGView.image = nil;
postBGView.image = temp;
}
}
//內(nèi)容如果是圖文混排,就添加View,用CoreText繪制
[self drawText];
}}
上述代碼只貼出來部分功能,但大體的思路都是一樣的,各個信息都是根據(jù)之前算好的布局進(jìn)行繪制的。這里是需要異步繪制,但如果在重寫drawRect方法就不需要用GCD異步線程了,因為drawRect本來就是異步繪制的。對于圖文混排的繪制,可以移步Google,研究下CoreText,這塊內(nèi)容太多了,不便展開。
好了,至此,我們又讓UITableView的效率提高了一個等級!但我們的步伐還遠(yuǎn)遠(yuǎn)不止這些,下面我們還可以從UIScrollView的角度出發(fā),再次找到突破口。
滑動UITableView時,按需加載對應(yīng)的內(nèi)容
直接上代碼:
//按需加載 - 如果目標(biāo)行與當(dāng)前行相差超過指定行數(shù),只在目標(biāo)滾動范圍的前后指定3行加載。
-
(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if (labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)]; NSMutableArray *arr = [NSMutableArray arrayWithArray:temp]; if (velocity.y<0) { NSIndexPath *indexPath = [temp lastObject]; if (indexPath.row+33) { [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]]; [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]]; } } [needLoadArr addObjectsFromArray:arr];
}
}
記得在tableView:cellForRowAtIndexPath:方法中加入判斷:
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
} ```
滾動很快時,只加載目標(biāo)范圍內(nèi)的Cell,這樣按需加載,極大的提高流暢度。
寫了這么多,也差不多該來個總結(jié)了!UITableView的優(yōu)化主要從三個方面入手:
提前計算并緩存好高度(布局),因為heightForRowAtIndexPath:是調(diào)用最頻繁的方法;
異步繪制,遇到復(fù)雜界面,遇到性能瓶頸時,可能就是突破口;
滑動時按需加載,這個在大量圖片展示,網(wǎng)絡(luò)加載的時候很管用!(SDWebImage已經(jīng)實現(xiàn)異步加載,配合這條性能杠杠的)。
除了上面最主要的三個方面外,還有很多幾乎大伙都很熟知的優(yōu)化點:
正確使用reuseIdentifier來重用Cells
盡量使所有的view opaque,包括Cell自身
盡量少用或不用透明圖層
如果Cell內(nèi)現(xiàn)實的內(nèi)容來自web,使用異步加載,緩存請求結(jié)果
減少subviews的數(shù)量
在heightForRowAtIndexPath:中盡量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后緩存結(jié)果
盡量少用addView給Cell動態(tài)添加View,可以初始化時就添加,然后通過hide來控制是否顯示
- 那么在cell里面異步加載圖片是個程序員都會想到,但是如果你給每個循環(huán)對象都加上異步加載,并且下滑的時候,這一操作將會被執(zhí)行,雖然是異步,但是一個app里面的線程過多也會卡頓的,特別是在下滑操作的時候給每個圖片進(jìn)行異步加載
那么這里可以利用UIScrollViewDelegate代理很好的解決這問題
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
可以識別tableview禁止或者減速滑動結(jié)束的時候進(jìn)行異步加載圖片
以下方法來執(zhí)行異步加載操作
//獲取可見部分的對象
NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *indexPath in visiblePaths)
{
//獲取的dataSource里面的對象,并且判斷加載完成的不需要再次異步加載
<code>
}
同時在cell繪制中也做限制 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
if (self.tableView.dragging == NO && self.tableView.decelerating == NO)
{
//開始異步加載圖片
<code>
}
如果tableview 停止滑動的時候開始異步加載圖片
最后也別忘記在內(nèi)存緊張的情況下釋放調(diào)所有的異步線程,以保證的你的app不會被系統(tǒng)強制關(guān)閉 - (void)didReceiveMemoryWarning{
// 釋放調(diào)異步加載圖片的線程以及所有圖片資源對象
<code>
}
- 自定義view
UITableViewCell包含了textLabel、detailTextLabel和imageView等view,而你還可以自定義一些視圖放在它的contentView里。然而view是很大的對象,創(chuàng)建它會消耗較多資源,并且也影響渲染的性能。
如果你的table cell包含圖片,且數(shù)目較多,使用默認(rèn)的UITableViewCell會非常影響性能。奇怪的是,使用自定義的view,而非預(yù)定義的view,明顯會快些。
當(dāng)然,最佳的解決辦法還是繼承UITableViewCell,并在其drawRect:中自行繪制:
1 - (void)drawRect:(CGRect)rect
2 {
3 if (image)
4 {
5 [image drawAtPoint:imagePoint];
6 self.image = nil;
7 } else {
8 [placeHolder drawAtPoint:imagePoint];
9 }
10 [text drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];
11 } ```
不過這樣一來,你會發(fā)現(xiàn)選中一行后,這個cell就變藍(lán)了,其中的內(nèi)容就被擋住了。最簡單的方法就是將cell的selectionStyle屬性設(shè)為UITableViewCellSelectionStyleNone,這樣就不會被高亮了。
此 外還可以創(chuàng)建CALayer,將內(nèi)容繪制到layer上,然后對cell的contentView.layer調(diào)用addSublayer:方法。這個例 子中,layer并不會顯著影響性能,但如果layer透明,或者有圓角、變形等效果,就會影響到繪制速度了。解決辦法可參見后面的預(yù)渲染圖像。
不要做多余的繪制工作。
在實現(xiàn)drawRect:的時候,它的rect參數(shù)就是需要繪制的區(qū)域,這個區(qū)域之外的不需要進(jìn)行繪制。
例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判斷是否需要繪制image和text,然后再調(diào)用繪制方法。
預(yù)渲染圖像。
你會發(fā)現(xiàn)即使做到了上述幾點,當(dāng)新的圖像出現(xiàn)時,仍然會有短暫的停頓現(xiàn)象。解決的辦法就是在bitmap context里先將其畫一遍,導(dǎo)出成UIImage對象,然后再繪制到屏幕,詳細(xì)做法可見《利用預(yù)渲染加速iOS設(shè)備的圖像顯示》。可以節(jié)省每次圖片的解壓縮和重采樣的時間。
參考url:https://www.keakon.NET/2011/07/26/利用預(yù)渲染加速iOS設(shè)備的圖像顯示。
此外,自動載入更新數(shù)據(jù)對用戶來說也很友好,這減少了用戶等待下載的時間。例如每次載入50條信息,那就可以在滾動到倒數(shù)第10條以內(nèi)時,加載更多信息:
``` - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{ if (count - indexPath.row < 10 && !updating) { updating = YES; [self update]; }
}// update方法獲取到結(jié)果后,設(shè)置updating為NO ```
4. 圖片尺寸處理
圖片異步加載無非就是在這個方法里發(fā)起異步請求,圖片加載完后根據(jù) UIImageView 的引用設(shè)置圖片。有經(jīng)驗的程序員可能會使用 懶加載 的方式減少快速滑動時因為網(wǎng)絡(luò)請求過于頻繁與切換線程顯示圖片造成的卡頓。這里還有個問題,拿回來的圖片一定和最后顯示的大小不一樣,有時候偷懶,直接設(shè)置 image view 的 contentMode 屬性要 image view 自己 壓縮。這是一個很取巧的方法,但是對 table view 的滾動速度也會造成 不容忽視 的影響。對圖片變形需要對圖片做 transform ,每次壓縮圖片都要對圖片乘以一個變換矩陣,如果你的圖片很多,這個計算量是不同忽視的。
優(yōu)化建議:從網(wǎng)絡(luò)摟回來圖片后先根據(jù)需要顯示的圖片大小切成合適大小的圖,每次只顯示處理過大小的圖片,當(dāng)查看大圖時在顯示大圖。如果服務(wù)器能直接返回預(yù)處理好的小圖和圖片的大小更好。
洋紅色是因為像素沒對齊,比如上面的 label,一般情況下因為像素沒對齊,需要抗鋸齒,圖像會出現(xiàn)模糊的現(xiàn)象。
解決辦法:在設(shè)置 view 的 frame 時,在高分屏避免出現(xiàn) 21.3,6.7這樣的小數(shù),尤其是 x,y坐標(biāo),用 ceil 或 floor 或 round 取整。每 0.5 個點對應(yīng)一個 pixel,0.3,0.7這樣的就難為 iPhone 了,低分屏不要出現(xiàn)小數(shù)。
黃色是因為顯示的圖片實際大小與顯示大小不同,對圖片進(jìn)行了拉伸,測試顯示使用 image view 顯示實際大小的圖也會變黃。
減少洋紅色和黃色可以提升滾動的流暢性
手動 Drawing 視圖提升流暢性
如果通過上面的方法,滾動速度還不能達(dá)到可以容忍的速度,那就只剩下最后一個辦法了,手動繪制視圖。
手動繪制方法,不是直接子類化 UITableViewCell,然后覆蓋 drawRect: 方法,這樣你會得到一個大黑塊!因為 cell 中不是只有一個 content view。如果不了解 cell 的層次結(jié)構(gòu),可以用 Reveal 去看下。
繪制 cell 不建議使用 UIView,建議使用 CALayer。 UIView 的繪制是建立在 CoreGraphic 上的,使用的是 CPU。CALayer 使用的是 Core Animation,CPU,GPU 通吃,由系統(tǒng)決定使用哪個。View的繪制使用的是自下向上的一層一層的繪制,然后渲染。Layer處理的是 Texure,利用 GPU 的 Texture Cache 和獨立的浮點數(shù)計算單元加速 紋理 的處理。
GPU 不喜歡 透明,所以所有的繪圖一定要弄成不透明,對于圓角和陰影這些的可以截個偽透明的小圖然后繪制上去。在layer的回調(diào)里一定也只做繪圖,不做計算!
5. 預(yù)加載數(shù)據(jù)
此外,自動載入更新數(shù)據(jù)對用戶來說也很友好,這減少了用戶等待下載的時間。例如每次載入50條信息,那就可以在滾動到倒數(shù)第10條以內(nèi)時,加載更多信息:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (count - indexPath.row <10 && !updating) {
updating = YES;
[self update];
}
}
// update方法獲取到結(jié)果后,設(shè)置updating為NO
還有一點要注意的就是當(dāng)圖片下載完成后,如果cell是可見的,還需要更新圖像:
NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *visibleIndexPathin indexPaths) {
if (indexPath == visibleIndexPath) {
MyTableViewCell *cell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
cell.image = image;
[cell setNeedsDisplayInRect:imageRect];
break;
}
}
源url:
http://www.cocoachina.com/ios/20150602/11968.html
http://bbs.51cto.com/thread-1123666-1-1.html
http://www.mamicode.com/info-detail-1125512.html
http://blog.csdn.Net/chengwuli125/article/details/9926959