系列文章:
最近在搞什么,所以就順手寫(xiě)點(diǎn)什么咯~
這兩天一直在搞一個(gè)TableView的工具類舰蟆,因?yàn)橛X(jué)得這個(gè)東西寫(xiě)完可以一勞永逸乒疏,所以就去搞了一下陕习,主要是有助于TableView的快捷開(kāi)發(fā)钦勘。沒(méi)什么好廢話的了淀散,直接說(shuō)事吧=氧卧。=
在今天的博客中你可能會(huì)看到:
- VVeboTableView中Cell加載邏輯的解析
- TableView代碼解耦的基本思路
恩桃笙,東西不多,一點(diǎn)一點(diǎn)說(shuō)~
VVeboTableView
其實(shí)這是VVebo項(xiàng)目中作者分享剝離的一個(gè)Demo沙绝,來(lái)告訴我們他是怎么優(yōu)化TableView的流暢性的搏明。
那么VVebo是什么呢?看名字你就猜吧闪檬,像不像微博星著,是的,它就是一款新浪微博的第三方客戶端粗悯,當(dāng)年還是有很多人追捧的虚循,不過(guò)后來(lái)新浪逐漸收回開(kāi)發(fā)接口導(dǎo)致很多功能無(wú)法實(shí)現(xiàn)就把VVebo給坑了。
那么為什么VVebo使用率那么高呢样傍?一方面是當(dāng)時(shí)新浪微博客戶端的確不行横缔,另一方面VVebo簡(jiǎn)約的風(fēng)格和流暢的體驗(yàn)俘獲了一大批用戶。所以今天我們就來(lái)探究一下他是如何做到TableView的絲滑體驗(yàn)的衫哥。
首先你可以在這里現(xiàn)在一份源碼茎刚,畢竟源碼面前沒(méi)有秘密。
在老司機(jī)看來(lái)撤逢,作者最有效的優(yōu)化分為4部分:
- TableViewCell圓角優(yōu)化
- 緩存行高
- 相對(duì)固定的圖片及文字采用CoreText繪制
- TableView加載數(shù)據(jù)邏輯優(yōu)化
1.圓角
這部分作者的優(yōu)化很簡(jiǎn)單膛锭,他沒(méi)有畫(huà)圓角!圓角是TableViewCell的幀率殺手大家都知道吧蚊荣,所以人家根本就沒(méi)有畫(huà)圓角初狰。他是怎么做的呢?覆蓋了與背景色同色的圓角圖片
妇押,簡(jiǎn)單粗暴跷究,果然是個(gè)心機(jī)boy。
不過(guò)關(guān)于圓角的優(yōu)化敲霍,還是有更好的解決辦法的俊马,在這里丁存。不想看的話我給你總結(jié)一下,就兩點(diǎn):
- 別冤枉cornerRadius柴我,問(wèn)題不在它解寝。而在于maskToBounds。普通的UIView繪制圓角時(shí)并不需要maskToBounds屬性艘儒。也就是普通的視圖圓角對(duì)卡頓沒(méi)有影響聋伦。
- 既然有普通就有特殊:UIImageView和UILabel以及我還沒(méi)有發(fā)現(xiàn)的=。=對(duì)于UIImage的處理建議先借助CoreGraphic處理圖片吧界睁,直接繪制一個(gè)帶圓角的圖片給ImageView吧觉增。對(duì)于Label沒(méi)有太好的優(yōu)化方案,是在不行只能CoreText了翻斟。其實(shí)你會(huì)發(fā)現(xiàn)逾礁,UILable這個(gè)控件對(duì)中文十!分访惜!不嘹履!友!好债热!很多細(xì)節(jié)上中文跟英文或者字符會(huì)有很大的差異砾嫉,但是你有不能不用他,好氣哦=窒篱。=
2.緩存行高
這部分內(nèi)容老司機(jī)在上一期講述過(guò)不定高cell行高緩存的必要性及緩存的方法焕刮,這里不再贅述。
3.CoreText繪制文本
首先舌剂,復(fù)雜的層級(jí)關(guān)系同樣會(huì)給cell在繪制時(shí)添加很大的負(fù)擔(dān)
济锄,這點(diǎn)是毋庸置疑的,所以VVebo的作者選擇了將一些相對(duì)重復(fù)性很大的視圖選擇使用CoreText和CoreGraphic技術(shù)直接繪制在一個(gè)視圖上霍转,這樣就減少了視圖的層級(jí)
荐绝,為流暢性又添了一份可能。CoreText繪制文本的和圖片的技術(shù)你可以在老司機(jī)的CoreText實(shí)現(xiàn)圖文混排系列中得到詳細(xì)的實(shí)現(xiàn)方法避消,想看的去看吧低滩。
4.TableView加載數(shù)據(jù)邏輯優(yōu)化
到現(xiàn)在為止終于要講點(diǎn)之前沒(méi)有說(shuō)過(guò)的了=。=
說(shuō)以下主體思路岩喷,VVebo的作者認(rèn)為恕沫,當(dāng)用戶快速滑動(dòng)的時(shí)候,事實(shí)上他對(duì)滑動(dòng)過(guò)程中的內(nèi)容是不關(guān)心的纱意,他只關(guān)心滾動(dòng)結(jié)束處的內(nèi)容婶溯,那么用戶不關(guān)心的內(nèi)容她就選擇了不加載。
這是他的主體思路,來(lái)看下這部分的實(shí)現(xiàn)代碼:
- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
NSDictionary *data = [datas objectAtIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
//清除cell內(nèi)容迄委,解決復(fù)用問(wèn)題
[cell clear];
cell.data = data;
//判斷如果needLoadArr中含有需要加載的indexPath而當(dāng)前indexPath又不在其中的時(shí)候褐筛,則不繪制cell直接返回
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
}
//判斷如果scrollToToping為真的時(shí)候(及點(diǎn)擊狀態(tài)欄快速回到TableView頂部的時(shí)候)不繪制cell
if (scrollToToping) {
return;
}
//上面都沒(méi)問(wèn)題的話,繪制cell
[cell draw];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell==nil) {
cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"cell"];
}
[self drawCell:cell withIndexPath:indexPath];
return cell;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
[needLoadArr removeAllObjects];
}
//按需加載 - 如果目標(biāo)行與當(dāng)前行相差超過(guò)指定行數(shù)叙身,只在目標(biāo)滾動(dòng)范圍的前后指定3行加載渔扎。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
//取出滾動(dòng)停止時(shí)展示的第一個(gè)cell的indexPath
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
//取出當(dāng)前展示的第一個(gè)cell的indexPath
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
//如果兩者之間差距很大則認(rèn)為滑動(dòng)速度很快,中間用戶都不關(guān)心信轿,直接把滾動(dòng)停止時(shí)的展示的cell加入到needLoadArr數(shù)組中
if (labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
//根據(jù)滾動(dòng)方向在前或后額外添加三個(gè)需要展示的cell晃痴,這樣看起來(lái)好像更加平滑的樣子
if (velocity.y<0) {
NSIndexPath *indexPath = [temp lastObject];
if (indexPath.row+3<datas.count) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
}
} else {
NSIndexPath *indexPath = [temp firstObject];
if (indexPath.row>3) {
[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];
}
}
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
scrollToToping = YES;
return YES;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
scrollToToping = NO;
[self loadContent];
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
scrollToToping = NO;
[self loadContent];
}
//用戶觸摸時(shí)第一時(shí)間加載內(nèi)容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!scrollToToping) {
[needLoadArr removeAllObjects];
[self loadContent];
}
return [super hitTest:point withEvent:event];
}
- (void)loadContent{
if (scrollToToping) {
return;
}
if (self.indexPathsForVisibleRows.count<=0) {
return;
}
if (self.visibleCells&&self.visibleCells.count>0) {
for (id temp in [self.visibleCells copy]) {
VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
[cell draw];
}
}
}
其實(shí)是就100行代碼,思路還是很清晰明了的财忽。作者主要是通過(guò)
-drawCell:withIndexPath:
這個(gè)方法來(lái)控制cell的繪制行為的倘核。我們看看他做了什么?
首先他cell調(diào)用了clear方法即彪,這是VVeboTableViewCell中作者自己實(shí)現(xiàn)的方法笤虫,用于清除cell上面展示的內(nèi)容,這樣可以避免因cell重用而導(dǎo)致沒(méi)有繪制的cell會(huì)顯示之前的內(nèi)容的問(wèn)題祖凫。然后是判斷needLoadArr中是否包含有當(dāng)前indexPath,若沒(méi)有返回酬凳。繼續(xù)判斷當(dāng)前TableView是否處于快速回到頂部的過(guò)程中惠况,如果是的話也不繪制。最后上述條件都滿足的時(shí)候再進(jìn)行cell的繪制宁仔。
所以重點(diǎn)來(lái)了稠屠,needLoadArr什么時(shí)候添加的元素?如何獲取到TableView快速回到頂部的時(shí)間點(diǎn)翎苫?
第二點(diǎn)好說(shuō)权埠,點(diǎn)擊狀態(tài)欄的時(shí)候,TableView會(huì)詢問(wèn)代理
- scrollViewShouldScrollToTop:
只有返回YES的時(shí)候才會(huì)快速回到頂部煎谍,這時(shí)我們可以在這捕獲到這個(gè)狀態(tài)攘蔽。但是可以看到作者并沒(méi)有在這選擇添加頂部可能要展示的cell進(jìn)needLoadArr數(shù)組,那么當(dāng)他滾動(dòng)到頂部的時(shí)候我們要將頂部的cell進(jìn)行直接更新呐粘,所以通過(guò)- scrollViewDidEndScrollingAnimation:
和- scrollViewShouldScrollToTop:
兩個(gè)代理拿到到達(dá)頂部的狀態(tài)后直接更新當(dāng)前cell满俗。
回過(guò)頭來(lái)我們說(shuō)下第一點(diǎn),needLoadArr是怎么操作呢作岖?
我們知道我們是要判斷TableView快速滑動(dòng)唆垃,那我們?cè)趺茨玫竭@個(gè)行為呢?要知道沒(méi)有什么代理是直接反應(yīng)滾動(dòng)速度的痘儡,這里作者很取巧的用到了-scrollViewWillEndDragging:withVelocity:targetContentOffset:
這個(gè)代理辕万。
這個(gè)代理在手指即將結(jié)束拖動(dòng)的時(shí)候出發(fā),他會(huì)告訴外界當(dāng)前的速度及這次會(huì)滾動(dòng)到的位置。
所以作者在這里判斷了目標(biāo)位置與當(dāng)前位置相差間隔渐尿,如果很大的話則認(rèn)為中間內(nèi)容不需加載醉途,直接添加目標(biāo)位置的內(nèi)容進(jìn)入數(shù)組。
恩涡戳,以上就是VVebo作者對(duì)數(shù)據(jù)加載邏輯的優(yōu)化结蟋。
這是依靠著上述四點(diǎn),VVebo才獲得了完美的滑動(dòng)體驗(yàn)渔彰,其思路也是我們開(kāi)發(fā)中可以學(xué)習(xí)和借鑒的
嵌屎。
TableView解耦
這部分內(nèi)容也不是什么新鮮事,也是比較靠譜的一個(gè)思路恍涂。當(dāng)然了這部分內(nèi)容不是對(duì)性能的優(yōu)化宝惰,而是對(duì)代碼的優(yōu)化。
天天寫(xiě)TableView里面的代理是不是很煩人啊再沧,千篇一律又不能不寫(xiě)尼夺。所以想一個(gè)方法只寫(xiě)一次以后拿來(lái)直接用吧=。=
放一個(gè)效果圖顷扩,老司機(jī)寫(xiě)的控制器里面看不到任何一個(gè)TableView代理然而還是能正常顯示并實(shí)現(xiàn)很多功能拐邪。
但是代碼怎么可能不寫(xiě),只是我在別的地方寫(xiě)過(guò)了隘截,并且花了大把時(shí)間進(jìn)行解耦扎阶,讓每一個(gè)TableView都能拿來(lái)就直接使用抚垃。
那么這個(gè)解耦的類我們要怎么寫(xiě)呢茉兰?
好的大磺,我們來(lái)新建一個(gè)文件负蠕。
這個(gè)類只需要一個(gè)屬性坟募,是一個(gè)數(shù)組寒锚。就是你平常寫(xiě)TableView的時(shí)候的數(shù)據(jù)源粹湃。
然后在.m中我們就可以像平常寫(xiě)TableView一樣在這里面寫(xiě)代理了蕴侧。
無(wú)視我的cell和model呵哨,嫌累沒(méi)創(chuàng)建=谤逼。=
最后在VC中把TableView的dataSource設(shè)成Helper就好了。
重點(diǎn)是別忘了持有helper類。tableView對(duì)dataSource是弱引用纹坐,如果不持有helper就被釋放了枝冀。
就是這么一個(gè)思路。的確該寫(xiě)你都寫(xiě)了,不過(guò)好處就是你以后把helper類拿到另一個(gè)工程還可以直接用果漾。
恩球切,思路就是這么簡(jiǎn)單的一個(gè)思路,不過(guò)你可以把你的helper類寫(xiě)的功能更加豐富一些绒障。比如說(shuō)我的helper類吨凑。老司機(jī)添加了高度緩存、滾動(dòng)優(yōu)化等優(yōu)化功能户辱,并且對(duì)選擇鸵钝、展示動(dòng)畫(huà)、無(wú)數(shù)據(jù)占位圖等常用功能都進(jìn)行了支持庐镐。而且老司機(jī)也在不斷的豐富helper類的功能恩商。
只放一個(gè)版本更新記錄吧,代碼放不下=必逆。=
/**
DWTableViewHelper
TableView工具類
抽出TableView代理怠堪,減小VC壓力,添加常用代理映射
version 1.0.0
添加常用代理映射
添加helper基礎(chǔ)屬性
version 1.0.1
去除注冊(cè)名眉,改為更適用的重用模式
version 1.0.2
添加多分組模式
version 1.0.3
添加選擇模式及相關(guān)api
version 1.0.4
添加helper設(shè)置cell類型及復(fù)用標(biāo)識(shí)
version 1.0.5
將cell的基礎(chǔ)屬性提出協(xié)議粟矿,helper與model同時(shí)遵守協(xié)議
version 1.0.6
修正占位視圖展示時(shí)機(jī),提供兩個(gè)刷新列表擴(kuò)展方法损拢,提供展示嚷炉、隱藏占位圖接口
version 1.0.7
添加選則模式下單選多選控制
version 1.0.8
補(bǔ)充組頭視圖、尾視圖行高代理映射并簡(jiǎn)化代理鏈
version 1.0.9
cell基類添加父類實(shí)現(xiàn)強(qiáng)制調(diào)用宏探橱、斷言中給出未能加載的cell類名
version 1.1.0
改變cell劃線機(jī)制,改為系統(tǒng)分割線绘证,添加分割線歸0方法
添加自動(dòng)行高計(jì)算并緩存
cell添加xib支持
修復(fù)選擇模式選中后關(guān)閉再次開(kāi)啟選擇同一個(gè)無(wú)法選中bug
更換去除選擇背景方式隧膏,解決與選擇模式的沖突
映射所有代理
version 1.1.1
添加自適應(yīng)模式最小行高限制及最大行高設(shè)置
添加數(shù)據(jù)源的容錯(cuò)機(jī)制,但這并不是你故意寫(xiě)錯(cuò)的理由=嚷那。=
添加屏幕判斷胞枕,當(dāng)位置方向時(shí),默認(rèn)返回豎屏
額外補(bǔ)充動(dòng)畫(huà)代理魏宽、支持CAAnimation及DWAnimation
version 1.1.2
展示動(dòng)畫(huà)邏輯修改腐泻,DWAnimation動(dòng)畫(huà)展示方法替換
version 1.1.3
滾動(dòng)優(yōu)化模式添加
高速忽略模式完成
懶加載模式完成
懶加載模式動(dòng)畫(huà)隱藏,更加平滑队询,修復(fù)刷新bug派桩。
有沒(méi)有美工妹子給切幾張占位圖。蚌斩。我做的圖太丑了铆惑。。
*/
是的,所以說(shuō)你玩去那可以寫(xiě)一個(gè)什么都能做的Helper员魏。
正如我最開(kāi)始的效果圖丑蛤。如果你想看看我還對(duì)Helper做了什么你可以去我的倉(cāng)庫(kù)上面看DWTableViewHelper。
你想直接用也可以撕阎,你可以去GitHub上面直接托一份受裹,也可以用cocoaPods集成:
pod 'DWTableViewHelper', '~> 1.1.2'
DWTableViewHelper類當(dāng)前為1.1.2版本,滾動(dòng)優(yōu)化在1.1.3版本pod還沒(méi)有發(fā)虏束,因?yàn)樵跍y(cè)試看有沒(méi)有什么bug棉饶,而且老司機(jī)做的圖有的丑,急需會(huì)美工的妹子幫我切兩張圖魄眉,漢子也行砰盐,愿意幫忙的私信我
=。=
如果你想看看老司機(jī)的所有pods項(xiàng)目的話坑律,你也可以打開(kāi)終端岩梳,輸入
pod search wicky
最后,雙擊666晃择,加波關(guān)注冀值,點(diǎn)波star,老鐵沒(méi)毛补馈列疗!