1. http://www.cocoachina.com/ios/20150602/11968.htmlhtml
最近在微博上看到一個很好的開源項目VVeboTableViewDemo,是關於如何優化UITableView的。加上正好最近也在優化項目中的相似朋友圈功能這塊,思考了不少關於UITableView的優化技巧,相信這塊是難點也是痛點,因此決定詳細的整理下我對優化UITableView的理解。ios
UITableView做爲iOS開發中最重要的控件之一,其中的實現原理非常考究。Apple在這塊的優化水平直接決定了iOS的體驗能甩安卓幾條街,哈哈,扯淡扯多了。。。好了,廢話很少說,直接進入主題。首先來談談我對UITableView的認識:git
UITableView的簡單認識github
UITableView最核心的思想就是UITableViewCell的重用機制。簡單的理解就是:UITableView只會建立一屏幕(或一屏幕多一點)的UITableViewCell,其餘都是從中取出來重用的。每當Cell滑出屏幕時,就會放入到一個集合(或數組)中(這裏就至關於一個重用池),當要顯示某一位置的Cell時,會先去集合(或數組)中取,若是有,就直接拿來顯示;若是沒有,纔會建立。這樣作的好處可想而知,極大的減小了內存的開銷。web
知道UITableViewCell的重用原理後,咱們來看看UITableView的回調方法。UITableView最主要的兩個回調方法是tableView:cellForRowAtIndexPath:和tableView:heightForRowAtIndexPath:。理想上咱們是會認爲UITableView會先調用前者,再調用後者,由於這和咱們建立控件的思路是同樣的,先建立它,再設置它的佈局。但實際上卻並不是如此,咱們都知道,UITableView是繼承自UIScrollView的,須要先肯定它的contentSize及每一個Cell的位置,而後纔會把重用的Cell放置到對應的位置。因此事實上,UITableView的回調順序是先屢次調用tableView:heightForRowAtIndexPath:以肯定contentSize及Cell的位置,而後纔會調用tableView:cellForRowAtIndexPath:,從而來顯示在當前屏幕的Cell。面試
舉個例子來講:若是如今要顯示100個Cell,當前屏幕顯示5個。那麼刷新(reload)UITableView時,UITableView會先調用100次tableView:heightForRowAtIndexPath:方法,而後調用5次tableView:cellForRowAtIndexPath:方法;滾動屏幕時,每當Cell滾入屏幕,都會調用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法。segmentfault
看到這裏,想必大夥也都能隱約察覺到,UITableView優化的首要任務是要優化上面兩個回調方法。事實也確實如此,下面按照我探討進階的過程,來研究如何優化:數組
優化探索,項目拿到手時代碼是這樣:xcode
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- (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;
}
|
看到這段代碼,對於剛畢業的我來講,以爲仍是蠻巧妙的,但巧歸巧,當Cell很是複雜的時候,直接卡出翔了。。。特別是在個人Touch4上,這我能忍?!好吧,依據上面UITableView原理的分析,咱們先來分析它爲何卡?緩存
這樣寫,在Cell賦值內容的時候,會根據內容設置佈局,固然也就能夠知道Cell的高度,想一想若是1000行,那就會調用1000+頁面Cell個數次tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法,而咱們對Cell的處理操做,都是在這個方法裏的!什麼賦值、佈局等等。開銷天然很大,這種方案Pass。。。改進代碼。
改進代碼後:
1
2
3
4
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
return
[ContacterTableCell cellHeightOfInfo:dict];
}
|
思路是把賦值和計算佈局分離。這樣讓tableView:cellForRowAtIndexPath:方法只負責賦值,tableView:heightForRowAtIndexPath:方法只負責計算高度。注意:兩個方法儘量的各司其職,不要重疊代碼!二者都須要儘量的簡單易算。Run一下,會發現UITableView滾動流暢了不少。。。
基於上面的實現思路,咱們能夠在得到數據後,直接先根據數據源計算出對應的佈局,並緩存到數據源中,這樣在tableView:heightForRowAtIndexPath:方法中就直接返回高度,而不須要每次都計算了。
1
2
3
4
5
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
CGRect rect = [dict[@
"frame"
] CGRectValue];
return
rect.frame.height;
}
|
其實上面的改進方法並非最佳方案,但基本能知足簡單的界面!記得開頭個人任務嗎?像朋友圈那樣的圖文混排,這種方案仍是扛不住的!咱們須要進入更深層次的探究:自定義Cell的繪製。
咱們在Cell上添加系統控件的時候,實質上系統都須要調用底層的接口進行繪製,當咱們大量添加控件時,對資源的開銷也會很大,因此咱們能夠索性直接繪製,提升效率。是否是說的很抽象?廢話很少說,直接上代碼:
首先須要給自定義的Cell添加draw方法,(固然也能夠重寫drawRect)而後在方法體中實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = [_data[@
"frame"
] CGRectValue];
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
CGContextFillRect(context, rect);
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];
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];
}
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if
(flag==drawColorFlag) {
postBGView.frame = rect;
postBGView.image = nil;
postBGView.image = temp;
}
}
[self drawText];
}}
|
上述代碼只貼出來部分功能,但大致的思路都是同樣的,各個信息都是根據以前算好的佈局進行繪製的。這裏是須要異步繪製,但若是在重寫drawRect方法就不須要用GCD異步線程了,由於drawRect原本就是異步繪製的。對於圖文混排的繪製,能夠移步Google,研究下CoreText,這塊內容太多了,不便展開。
好了,至此,咱們又讓UITableView的效率提升了一個等級!但咱們的步伐還遠遠不止這些,下面咱們還能夠從UIScrollView的角度出發,再次找到突破口。
滑動UITableView時,按需加載對應的內容
直接上代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
- (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:方法中加入判斷:
1
2
3
4
|
if
(needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return
;
}
|
滾動很快時,只加載目標範圍內的Cell,這樣按需加載,極大的提升流暢度。
寫了這麼多,也差很少該來個總結了!UITableView的優化主要從三個方面入手:
-
提早計算並緩存好高度(佈局),由於heightForRowAtIndexPath:是調用最頻繁的方法;
-
異步繪製,遇到複雜界面,遇到性能瓶頸時,可能就是突破口;
-
滑動時按需加載,這個在大量圖片展現,網絡加載的時候很管用!(SDWebImage已經實現異步加載,配合這條性能槓槓的)。
除了上面最主要的三個方面外,還有不少幾乎大夥都很熟知的優化點:
-
正確使用reuseIdentifier來重用Cells
-
儘可能使全部的view opaque,包括Cell自身
-
儘可能少用或不用透明圖層
-
若是Cell內現實的內容來自web,使用異步加載,緩存請求結果
-
減小subviews的數量
-
在heightForRowAtIndexPath:中儘可能不使用cellForRowAtIndexPath:,若是你須要用到它,只用一次而後緩存結果
-
儘可能少用addView給Cell動態添加View,能夠初始化時就添加,而後經過hide來控制是否顯示
尾巴
確定不少人會很是好奇,爲何我都是手動用代碼建立Cell的?如今主流不都是Xib、Storyboard什麼的嘛?個人回答是:要想提升效率,仍是手動寫有用!拋開Xib、Storyboard須要系統自動轉碼,給系統多加了一層負擔不談,自定義Cell的繪製更是無從下手,因此,在我看來,複雜的須要高效的界面,仍是手動寫代碼吧!!!
最後若是大家的項目都是用的Xib、Storyboard,並須要優化UITableView的話,sunnyxx大神提出了好的方案:http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/ 大夥能夠自行研究研究。
知識是須要不斷學習的,做爲剛上路的我,若是有什麼理解不到位的,歡迎大夥留言指正,若是你有什麼更牛逼的想法,但願一塊兒交流交流。
註明:本篇的分析源碼來源於開源項目VVeboTableViewDemo
參考:https://github.com/johnil/VVeboTableViewDemo
2.
優化UITableViewCell高度計算的那些事
https://github.com/johnil/VVeboTableViewDemo
http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/
我是前言
這篇文章是我和咱們團隊最近對 UITableViewCell 利用 AutoLayout 自動高度計算和 UITableView 滑動優化的一個總結。
咱們也在維護一個開源的擴展,UITableView+FDTemplateLayoutCell
,讓高度計算這個事情變的史無前例的簡單,也受到了不少星星的支持,github連接請戳我
這篇總結你能夠讀到:
- UITableView高度計算和估算的機制
- 不一樣iOS系統在高度計算上的差別
- iOS8 self-sizing cell
- UITableView+FDTemplateLayoutCell如何用一句話解決高度問題
- UITableView+FDTemplateLayoutCell中對RunLoop的使用技巧
UITableViewCell高度計算
rowHeight
UITableView
是咱們再熟悉不過的視圖了,它的 delegate 和 data source 回調不知寫了多少次,也難免遇到 UITableViewCell 高度計算的事。UITableView 詢問 cell 高度有兩種方式。
一種是針對全部 Cell 具備固定高度的狀況,經過:
1
|
self.tableView.rowHeight = 88 |
上面的代碼指定了一個全部 cell 都是 88 高度的 UITableView,對於定高需求的表格,強烈建議使用這種(而非下面的)方式保證沒必要要的高度計算和調用。rowHeight
屬性的默認值是 44,因此一個空的 UITableView 顯示成那個樣子。
另外一種方式就是實現 UITableViewDelegate 中的:
1
2
3
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { |
須要注意的是,實現了這個方法後,rowHeight
的設置將無效。因此,這個方法適用於具備多種 cell 高度的 UITableView。
estimatedRowHeight
這個屬性 iOS7 就出現了, 文檔是這麼描述它的做用的:
If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
恩,聽上去蠻靠譜的。咱們知道,UITableView 是個 UIScrollView,就像平時使用 UIScrollView 同樣,加載時指定 contentSize
後它才能根據本身的 bounds、contentInset、contentOffset 等屬性共同決定是否能夠滑動以及滾動條的長度。而 UITableView 在一開始並不知道本身會被填充多少內容,因而詢問 data source 個數和建立 cell,同時詢問 delegate 這些 cell 應該顯示的高度,這就形成它在加載的時候浪費了多餘的計算在屏幕外邊的 cell 上。和上面的 rowHeight
很相似,設置這個估算高度有兩種方法:
1
2
3
4
5
|
self.tableView.estimatedRowHeight = 88; |
有所不一樣的是,即便面對種類不一樣的 cell,咱們依然可使用簡單的 estimatedRowHeight
屬性賦值,只要總體估算值接近就能夠,好比大概有一半 cell 高度是 44, 一半 cell 高度是 88, 那就能夠估算一個 66,基本符合預期。
說完了估算高度的基本使用,能夠開始吐槽了:
- 設置估算高度後,contentSize.height 根據「cell估算值 x cell個數」計算,這就致使滾動條的大小處於不穩定的狀態,contentSize 會隨着滾動從估算高度慢慢替換成真實高度,肉眼可見滾動條忽然變化甚至「跳躍」。
- 如果有設計很差的下拉刷新或上拉加載控件,或是 KVO 了 contentSize 或 contentOffset 屬性,有可能使表格滑動時跳動。
- 估算高度設計初衷是好的,讓加載速度更快,那憑啥要去侵害滑動的流暢性呢,用戶可能對進入頁面時多零點幾秒加載時間感受不大,可是滑動時實時計算高度帶來的卡頓是明顯能體驗到的,我的以爲還不如一開始都算好了呢(iOS8更過度,即便都算好了也會邊劃邊計算)
iOS8 self-sizing cell
具備動態高度內容的 cell 一直是個頭疼的問題,好比聊天氣泡的 cell, frame 佈局時代一般是用數據內容反算高度:
1
|
CGFloat height = textHeightWithFont() + imageHeight + topMargin + bottomMargin + ...;
|
供 UITableViewDelegate 調用時極可能是個 cell 的類方法:
1
2
3
|
@interface BubbleCell : UITableViewCell + (CGFloat)heightWithEntity:(id)entity; @end |
各類魔法 margin 加上耦合了屏幕寬度。
AutoLayout 時代好了很多,提供了-systemLayoutSizeFittingSize:
的 API,在 contentView 中設置約束後,就能計算出準確的值;缺點是計算速度確定沒有手算快,並且這是個實例方法,須要維護專門爲計算高度而生的 template layout cell
,它還要求使用者對約束設置的比較熟練,要保證 contentView 內部上下左右全部方向都有約束支撐,設置不合理的話計算的高度就成了0。
這裏還不得不提到一個 UILabel 的蛋疼問題,當 UILabel 行數大於0時,須要指定 preferredMaxLayoutWidth
後它才知道本身何時該折行。這是個「雞生蛋蛋生雞」的問題,由於 UILabel 須要知道 superview 的寬度才能折行,而 superview 的寬度還依仗着子 view 寬度的累加才能肯定。這個問題好像到 iOS8 纔可以自動解決(不過咱們找到了解決方案)
回到正題,iOS8 WWDC 中推出了 self-sizing cell
的概念,旨在讓 cell 本身負責本身的高度計算,使用 frame layout 和 auto layout 均可以享受到:

這個特性首先要求是 iOS8,要是最低支持的系統版本小於8的話,還得針對老版本單寫套老式的算高(囧),不過用的 API 到不是新面孔:
1
2
|
self.tableView.estimatedRowHeight = 213 |
這裏又不得不吐槽了,自動計算 rowHeight 跟 estimatedRowHeight 究竟是有什麼仇,若是不加上估算高度的設置,自動算高就失效了- -
PS:iOS8 系統中 rowHeight 的默認值已經設置成了 UITableViewAutomaticDimension,因此第二行代碼能夠省略。
問題:
- 這個自動算高在 push 到下一個頁面或者轉屏時會出現高度特別詭異的狀況,不過如今的版本修復了。
- 求一個能讓最低支持 iOS8 的公司- -
iOS8抽風的算高機制
相同的代碼在 iOS7 和 iOS8 上滑動順暢程度徹底不一樣,iOS8 莫名奇妙的卡。很大一部分緣由是 iOS8 上的算高機制大不相同,這是我作的小測試:

研究後發現這麼屢次額外計算有下面的緣由:
- 不開啓高度估算時,UITableView 上來就要對全部 cell 調用算高來肯定 contentSize
dequeueReusableCellWithIdentifier:forIndexPath:
相比不帶 「forIndexPath」 的版本會多調用一次高度計算
- iOS7 計算高度後有」緩存「機制,不會重複計算;而 iOS8 不論什麼時候都會從新計算 cell 高度
iOS8 把高度計算搞成這個樣子,從 WWDC 也卻是能找到點解釋,cell 被認爲隨時均可能改變高度(如從設置中調整動態字體大小),因此每次滑動出來後都要從新計算高度。
說了這麼多,究竟有沒有既能省去算高煩惱,又能保證順暢的滑動,還能支持 iOS6+ 的一站式解決方案呢?
UITableView+FDTemplateLayoutCell
使用 UITableView+FDTemplateLayoutCell
無疑是解決算高問題的最佳實踐之一,既有 iOS8 self-sizing 功能簡單的 API,又能夠達到 iOS7 流暢的滑動效果,還保持了最低支持 iOS6。
使用起來大概是這樣:
1
2
3
4
5
6
7
|
#import <UITableView+FDTemplateLayoutCell.h> - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByIndexPath:indexPath configuration:^(id cell) { |
寫完上面的代碼後,你就已經使用到了:
- 和每一個 UITableViewCell ReuseID 一一對應的 template layout cell
這個 cell 只爲了參加高度計算,不會真的顯示到屏幕上;它經過 UITableView 的 -dequeueCellForReuseIdentifier:
方法 lazy 建立並保存,因此要求這個 ReuseID 必須已經被註冊到了 UITableView 中,也就是說,要麼是 Storyboard 中的原型 cell,要麼就是使用了 UITableView 的-registerClass:forCellReuseIdentifier:
或 -registerNib:forCellReuseIdentifier:
其中之一的註冊方法。
- 根據 autolayout 約束自動計算高度
使用了系統在 iOS6 就提供的 API:-systemLayoutSizeFittingSize:
- 根據 index path 的一套高度緩存機制
計算出的高度會自動進行緩存,因此滑動時每一個 cell 真正的高度計算只會發生一次,後面的高度詢問都會命中緩存,減小了很是可觀的多餘計算。
- 自動的緩存失效機制
無須擔憂你數據源的變化引發的緩存失效,當調用如-reloadData
,-deleteRowsAtIndexPaths:withRowAnimation:
等任何一個觸發 UITableView 刷新機制的方法時,已有的高度緩存將以最小的代價執行失效。如刪除一個 indexPath 爲 [0:5] 的 cell 時,[0:0] ~ [0:4] 的高度緩存不受影響,而 [0:5] 後面全部的緩存值都向前移動一個位置。自動緩存失效機制對 UITableView 的 9 個公有 API 都進行了分別的處理,以保證沒有一次多餘的高度計算。
- 預緩存機制
預緩存機制將在 UITableView 沒有滑動的空閒時刻執行,計算和緩存那些尚未顯示到屏幕中的 cell,整個緩存過程徹底沒有感知,這使得完整列表的高度計算既沒有發生在加載時,又沒有發生在滑動時,同時保證了加載速度和滑動流暢性,下文會着重講下這塊的實現原理。
咱們在設計這個工具的 API 時斟酌了很是長的時間,既要保證功能的強大,也要保證接口的精簡,一行調用背後隱藏着不少功能。
這一套緩存機制能對滑動起多大影響呢?除了肉眼能明顯的感知到外,我還作了個小測試:
一個有 54 個內容和高度不一樣 cell 的 table view,從頭滑動到尾,再從尾滑動到頭,iOS8 系統下,iPhone6,使用 Time Profiler
監測算高函數所花費的時間:
未使用緩存API、未使用估算,共花費 877 ms:

使用緩存API、開啓估算,共花費 77 ms:

測試數據的精度先無論,從量級上就差了一個數量級,說實話本身也沒想到差距有這麼大- -
同時,工具也順手解決了-preferredMaxLayoutWidth
的問題,在計算高度前向 contentView 加了一條和 table view 寬度相同的寬度約束,強行讓 contentView 內部的控件知道了本身父 view 的寬度,再反算本身被外界約束的寬度,破除「雞生蛋蛋生雞」的問題,這裏比較 tricky,就不展開說了。下面說說利用 RunLoop 預緩存的實現。
利用RunLoop空閒時間執行預緩存任務
FDTemplateLayoutCell 的高度預緩存是一個優化功能,它要求頁面處於空閒狀態時才執行計算,當用戶正在滑動列表時顯然不該該執行計算任務影響滑動體驗。
通常來講,這個功能要耦合 UITableView 的滑動狀態才行,但這種實現十分不優雅且可能破壞外部的 delegate 結構,但好在咱們還有RunLoop
這個工具,瞭解它的運行機制後,能夠用很簡單的代碼實現上面的功能。
空閒RunLoopMode
在曾經的 RunLoop 線下分享會(視頻可戳)中介紹了 RunLoopMode 的概念。
當用戶正在滑動 UIScrollView 時,RunLoop 將切換到 UITrackingRunLoopMode
接受滑動手勢和處理滑動事件(包括減速和彈簧效果),此時,其餘 Mode (除 NSRunLoopCommonModes 這個組合 Mode)下的事件將所有暫停執行,來保證滑動事件的優先處理,這也是 iOS 滑動順暢的重要緣由。
當 UI 沒在滑動時,默認的 Mode 是 NSDefaultRunLoopMode
(同 CF 中的 kCFRunLoopDefaultMode),同時也是 CF 中定義的 「空閒狀態 Mode」。當用戶啥也不點,此時也沒有什麼網絡 IO 時,就是在這個 Mode 下。
用RunLoopObserver找準時機
註冊 RunLoopObserver 能夠觀測當前 RunLoop 的運行狀態,並在狀態機切換時收到通知:
- RunLoop開始
- RunLoop即將處理Timer
- RunLoop即將處理Source
- RunLoop即將進入休眠狀態
- RunLoop即將從休眠狀態被事件喚醒
- RunLoop退出
由於「預緩存高度」的任務須要在最無感知的時刻進行,因此應該同時知足:
- RunLoop 處於「空閒」狀態 Mode
- 當這一次 RunLoop 迭代處理完成了全部事件,立刻要休眠時
使用 CF 的帶 block 版本的註冊函數可讓代碼更簡潔:
1
2
3
4
5
6
7
|
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { |
在其中的 TODO 位置,就能夠開始任務的收集和分發了,固然,不能忘記適時的移除這個 observer
分解成多個RunLoop Source任務
假設列表有 20 個 cell,加載後展現了前 5 個,那麼開啓估算後 table view 只計算了這 5 個的高度,此時剩下 15 個就是「預緩存」的任務,而咱們並不但願這 15 個計算任務在同一個 RunLoop 迭代中同步執行,這樣會卡頓 UI,因此應該把它們分別分解到 15 個 RunLoop 迭代中執行,這時就須要手動向 RunLoop 中添加 Source 任務(由應用發起和處理的是 Source 0 任務)
Foundation 層沒對 RunLoopSource 提供直接構建的 API,可是提供了一個間接的、既熟悉又陌生的 API:
1
2
3
4
5
|
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array; |
這個方法將建立一個 Source 0 任務,分發到指定線程的 RunLoop 中,在給定的 Mode 下執行,若指定的 RunLoop 處於休眠狀態,則喚醒它處理事件,簡單來講就是「睡你xx,起來嗨!」
因而,咱們用一個可變數組裝載當前全部須要「預緩存」的 index path,每一個 RunLoopObserver 回調時都把第一個任務拿出來分發:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy; CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { if (mutableIndexPathsToBePrecached.count == 0) { CFRunLoopRemoveObserver(runLoop, observer, runLoopMode); CFRelease(observer); |
這樣,每一個任務都被分配到下個「空閒」 RunLoop 迭代中執行,其間但凡是有滑動事件開始,Mode 切換成 UITrackingRunLoopMode,全部的「預緩存」任務的分發和執行都會自動暫定,最大程度保證滑動流暢。
開始使用UITableView+FDTemplateLayoutCell
若是你以爲這個工具能幫獲得你,整合到工程也十分簡單。
使用 cocoapods:
1
|
pod search UITableView+FDTemplateLayoutCell
|
寫這篇文章時的最新版本爲 1.2,去除了前一個版本的黑魔法,增長了預緩存功能。
歡迎使用和支持這個工具,有 bug 請隨時反饋哦~
再複習下 github 地址: https://github.com/forkingdog/UITableView-FDTemplateLayoutCell
3. http://segmentfault.com/a/1190000000393179
UITableview
從08年到如今開發過的iOS應用不可勝數了,可是面試不少人的時候,發現依然不少同窗在最基本的列表控件上懂得不夠深,下面就結合各方面的資料進行再一次講解。
咱們都知道純代碼是效率最高的,可是在開發成本上已經愈來愈不如使用Storyboard性價比高,速度快,因此本文試圖結合UIStoryboard來描述一整套方案。
簡單配置
在Storyboard中拖入UITableViewController,而且修改塗塗畫畫。
在代碼區new File生成一個基於UITableviewController的自定義類,我這裏暫時取名爲Home。由於主頁就是一個複雜的列表的不在少數吧?呵呵。
而後在Identity insepctor裏修改對應的Class name,使得代碼與Storyboard產生關聯。

想要作下拉刷新嘛?系統自帶了一個給你,而且能夠自定義換標題哦。不少人真的不知道在哪兒選中,請看下圖,先選中UITableviewController,而後在選項卡中enable這個refresh選項,就自動完成了。 對應的代碼仍是複製進去,就會自動觸發。

而後你須要對UITableView作一些簡單的配置,首先要選中UITableView,不少人看不到選項,是由於默認關閉了……

下圖是對UITableview的簡單配置。
Content是動態列表/靜態列表,若是是靜態的,那你基本不用寫代碼就能所見即所得,譬如「設置」頁面就能夠套用。可是諸如微博啦,朋友圈,仍是老實的用動態列表,用代碼控制。
Prototype Cells指會出現的cell有幾種類型。這個後面再講。
Style效果現場試一下就看出來了。Separator指的分隔行樣式。

而後你就須要代碼中作一些簡單的配置了,我只列重要的,這裏不是基礎教程,基礎的仍是老實的看教科書
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
Load More加載更多
加載更多的UITableview擴展控件很是多,嘗試過各類第三方完美擴展後,我以爲這點小把戲也不至於須要擴展UITableview類吧?
那就是在列表追加一個Section,放在最後面,這個Section只有一個Cell,這個自定義的cell有3種狀態,這些均可以自由發揮。
每當willDisplayCell的時候,你就設置他正在loading狀態,給用戶形成正在加載的假象,同時觸發網絡請求。
當有網絡數據返回後,天然會insert好多內容,也輪不到這個加載更多的cell顯示的地方了,天然就釋放了。固然了,若是沒有更多內容,也能夠輕鬆的cellForRowAtIndexPath找到惟一的cell,設置爲無更多數據等狀態。
typedef enum{ JWLoadMoreNormal = 0,
Autolayout
作完這些,基本配置就完成了,下面須要根據設計師的要求進行自定義開發,譬如自定義cell

如上圖,密密麻麻的
autolayout的拖拽不會?你太老土了吧。xcode5的拖拽,可謂是異常簡單,只須要點快捷菜單的pin,設置好上下左右相對關係就能夠了。
建立custom的uitableviewcell基本差很少,拖出來,畫畫塗塗,建立代碼,改類名對應關係,按着「Control」拖拽關聯,等等。
我這裏只講一個特殊的,就是圖上「圖片」「摘要」屬於並排區域,可能沒有圖片的帖子,「摘要」就須要頂格排版。這樣的狀況該怎麼設置呢?
這就得用NSLayoutConstraint的拽出來的關聯了。把」摘要「的相對距離,鎖定在一個固定的位置上,譬如」左邊欄「,經過少許的代碼計算,便可動態的修改NSLayoutConstraint.constant的距離。
NSString *url=data.img; [self.previewImageView setClipsToBounds:YES]; if ( url!=nil && ![url isEqualToString:@""]) {
動態計算高度
作到這裏,恐怕大部分人都遇到一個門檻了。那就是如何動態計算cell的高度。最簡單的,就是網易新聞類,固定高度。return 44;
如果動態的,無非是建立一個cell,而且初始化構造好,而後輸出cell的最後一行控件的位置,最終給出位置。
可是這就致使了函數運行的低效。你想,autolayout原本就夠效率低了(由於程序猿省事兒了),再爲每一行計算2次,這效率能高?
我親測發現,動態排版效率是很是低的,不足以信任。
最好辦的,仍是土方法。獲取對應的數據,根據本身設定的排版規則,動態的計算。土歸土,效率高啊!
TopicID *data=[threadList objectAtIndex:indexPath.row];
最後別忘了Profile,計算時間,每個細節的時間優化,最終都會體如今列表的流暢度表現上。 經過以上幾點呢,再結合現成好用的SDWebImageCache,相信你們必定能夠作出真正美觀、高效的列表哦!