UITableView中的UITableViewCell是可以重用的。
什么是重用耍属,字面意思就是把內(nèi)容換一下寸宵,大體結(jié)構(gòu)不發(fā)生變化的二次使用檩帐。打個(gè)比方就是家里做飯术幔,一般都是吃完飯把餐具洗一下,下次再用湃密,而不是吃完就扔诅挑,下次再買(mǎi)新的(一次性除外)四敞。
所以重用的好處就是節(jié)省,較少每次創(chuàng)建或銷(xiāo)毀對(duì)象的開(kāi)銷(xiāo)拔妥,直觀的感受就是你的TableView滑起來(lái)更流暢了忿危,把成千上萬(wàn)條數(shù)據(jù)加載進(jìn)去,內(nèi)存也不會(huì)劇增没龙,app也就不不容易崩了铺厨。
以上都是我看文檔或者別的大牛說(shuō)的,自己沒(méi)試驗(yàn)過(guò)硬纤,親身感受也不真切解滓,于是乎我打算自己嘗試動(dòng)手去實(shí)現(xiàn)重用機(jī)制,利用的就是UIScrollView筝家。為什么要選UIScrollView洼裤,簡(jiǎn)單的原因就是它是UITableView的超類(lèi),只是后者實(shí)現(xiàn)了重用機(jī)制溪王,前者沒(méi)有腮鞍。
事先聲明一點(diǎn),為了作對(duì)比莹菱,我先做了一個(gè)非常簡(jiǎn)單的圖片瀏覽器移国,由于是實(shí)驗(yàn)性質(zhì),所以一些細(xì)節(jié)并沒(méi)有很好地去實(shí)現(xiàn)芒珠,我把主要精力放在對(duì)比沒(méi)有使用重用機(jī)制和使用了重用機(jī)制的內(nèi)存使用及滑動(dòng)的流暢度上桥狡。
先上圖說(shuō)明結(jié)果:
結(jié)果非常明顯,沒(méi)有使用重用機(jī)制的皱卓,雖然我也用了懶加載的模式裹芝,但是當(dāng)我把所用圖片都看一遍后,內(nèi)存就蹭蹭蹭的往上漲了娜汁,沒(méi)有下降的痕跡嫂易;而使用了重用機(jī)制,當(dāng)只有l(wèi)oad到大一點(diǎn)的圖片的時(shí)候掐禁,內(nèi)存占用才會(huì)上漲怜械,但是過(guò)后又會(huì)降下來(lái),可喜傅事。
接下來(lái)我會(huì)分享一下我是如何實(shí)現(xiàn)這個(gè)重用機(jī)制的缕允。因?yàn)槲沂峭ㄟ^(guò)UITableView才想到這個(gè)重用的,所以我所有的思路都是參考我在使用UITableView時(shí)所做過(guò)的事蹭越,然后推測(cè)那樣做的目的障本,再自行實(shí)現(xiàn)代碼的。如果有不恰當(dāng)?shù)牡胤秸?qǐng)各位斧正,謝謝驾霜。
首先想到的是我在寫(xiě)TableViewCell的時(shí)候案训,在實(shí)現(xiàn)
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
這個(gè)方法的時(shí)候,第一句寫(xiě)的就是
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"XXX"];
這句話(huà)的作用就是在Reusable池中dequeue一個(gè)可重用的粪糙,標(biāo)記為XXX的cell出來(lái)强霎,于是我們馬上可以想到,要實(shí)現(xiàn)重用蓉冈,目前需要以下幾個(gè)要素:
思路1.0
- 存放可供重用的cell的Reusable池
- 正在屏幕上顯示的cell的Visible池
- 從Reusable池中取出cell的操作城舞,dequeue
分析
上述三個(gè)方面,前兩個(gè)需要我們各生成一個(gè)存儲(chǔ)對(duì)象洒擦,把用于展示的“cell”和可供重用的“cell”存放起來(lái)椿争,那這里我們有哪些可供利用呢?NSMutableArray
熟嫩,NSMutableDictionary
等秦踪?
參考了其他大神的說(shuō)法,Apple是利用了上述兩者掸茅,我猜想用字典的原因應(yīng)該是為不同的Identifier生成不同的key椅邓,然后每個(gè)key里面又是一個(gè)可變數(shù)組,存放對(duì)應(yīng)Identifier的cell(純屬瞎猜)昧狮【澳伲考慮到這個(gè)圖片瀏覽器目前功能簡(jiǎn)單,所以這里我選擇的是NSMutableSet
逗鸣,具體好處后面會(huì)提到合住。
至于第三點(diǎn),取出cell的操作很顯然就是一個(gè)方法撒璧,于是乎我們就可以先實(shí)現(xiàn)這么幾個(gè)啦:
@property (nonatomic) NSMutableSet *visibleViews;
@property (nonatomic) NSMutableSet *reusableViews;
- (ZoomingScrollView *)dequeReusableView {
ZoomingScrollView *reusableView = [_reusableViews anyObject];
if (reusableView == nil) {
reusableView = [[ZoomingScrollView alloc] initWithFrame:kScreenFrame];
} else {
[_reusableViews removeObject:reusableView];
}
return reusableView;
}
- (ZoomingScrollView *)viewAtIndex:(NSUInteger) index {
ZoomingScrollView *zoomingView = [self dequeReusableView];
// 這里有沒(méi)有什么問(wèn)題透葛?大家思考一下~
UIImage *image = [UIImage imageNamed:imageName];
[zoomingView setDisplayImage:image];
CGFloat xPosition = index * kScreenFrame.size.width;
CGRect viewFrame = CGRectMake(xPosition, 0, kScreenFrame.size.width, kScreenFrame.size.height);
zoomingView.frame = viewFrame;
zoomingView.tag = 1000 + index;
[_visibleViews addObject:zoomingView];
return zoomingView;
}
這里把viewAtIndex
這個(gè)方法一并在類(lèi)里實(shí)現(xiàn)了,其實(shí)這里應(yīng)該要設(shè)計(jì)成由代理來(lái)實(shí)現(xiàn)卿樱,但是由于是實(shí)驗(yàn)僚害,所以沒(méi)有直接實(shí)現(xiàn),這樣比較直觀繁调。(后面我還是改成代理模式吧萨蚕。。蹄胰。這樣的耦合性很高)
dequeReusableView
方法的作用就是從reusableViews
取出可重用的view岳遥,當(dāng)沒(méi)有可以重用的view時(shí),就新創(chuàng)建一個(gè)對(duì)象裕寨。最后返回這個(gè)view寒随,這里的邏輯還是比較簡(jiǎn)單的。
在viewAtIndex:
方法中,[_visibleViews addObject:zoomingView];
這句的作用是把顯示在屏幕上的view放到_visibleViews
池中妻往,然后給大家一個(gè)小小的問(wèn)題,方法里用[UIImage imageNamed:imageName]
這句導(dǎo)入圖片试和,這種做好好不好讯泣,有什么問(wèn)題~?
目前為止阅悍,我們已經(jīng)把思路1.0里的所有方面都實(shí)現(xiàn)了好渠,理清一下思路,我們現(xiàn)在所做的方向节视,是從Reusable池中取出view拳锚,放到Visible池中顯示到屏幕上,所以接下來(lái)我們要做的就是當(dāng)view離開(kāi)屏幕時(shí)寻行,把無(wú)需顯示的view從Visible池放入Reusable池中霍掺,還有即將進(jìn)入屏幕的view要及時(shí)創(chuàng)建。
思路2.0
- 滑動(dòng)時(shí)即將出現(xiàn)的view
- 把過(guò)時(shí)的view從Visible池中放入Reusable池中
- 清理過(guò)時(shí)的view中的數(shù)據(jù)(如圖片拌蜘,data等)
分析
向左向右滑動(dòng)后杆烁,當(dāng)前的view還沒(méi)完全從屏幕中移走,此時(shí)千萬(wàn)不可把當(dāng)前view放入到Reusable池中简卧;
其次要提前生成即將出現(xiàn)的view兔魂,生成的方法可以利用思路1.0中已經(jīng)實(shí)現(xiàn)的viewAtIndex:
方法;
最后根據(jù)當(dāng)前view的index举娩,把index-1和index+1以外的view全部放入Reusable池中等待重用析校。繼續(xù)上代碼:
- (void)showNewImage {
ZoomingScrollView *previousView = nil;
ZoomingScrollView *nextView = nil;
NSInteger previousIndex = _currentIndex - 1;
NSInteger nextIndex = _currentIndex + 1;
// 滑動(dòng)時(shí)最多保留3個(gè)圖,n是當(dāng)前頁(yè)面數(shù)铜涉,加上左右的2個(gè)
if (_currentIndex == 0) {
// 第一張圖
previousIndex = 0;
} else if (_currentIndex == kTotalImage - 1) {
// 最后一張圖
nextIndex = kTotalImage - 1;
}
if (![self isShowingViewAtIndex:previousIndex]) {
previousView = [self viewAtIndex:previousIndex];
}
if (![self isShowingViewAtIndex:nextIndex]) {
nextView = [self viewAtIndex:nextIndex];
}
[_scrollView addSubview:previousView];
[_scrollView addSubview:nextView];
// 其余全部放到reusableViews里
for (ZoomingScrollView *view in _visibleViews) {
NSInteger viewIndex = view.tag - 1000;
if (viewIndex < previousIndex || viewIndex > nextIndex) {
[_reusableViews addObject:view];
view.imageView.image = nil;
// 記得從主視圖中remove
[view removeFromSuperview];
}
}
// 從visibleViews里刪除剛剛?cè)サ舻膙iew
[_visibleViews minusSet:_reusableViews];
}
其實(shí)后來(lái)想了一下智玻,到底需不需要保留左右兩個(gè)view。因?yàn)槲野裇crollView設(shè)成了pagingEnable骄噪,每次只滑過(guò)一個(gè)頁(yè)面尚困,所以在屏幕上最多也只會(huì)同時(shí)出現(xiàn)2個(gè)頁(yè)面,那再減少保留一個(gè)view應(yīng)該也是可以的链蕊。但是當(dāng)時(shí)在寫(xiě)代碼的時(shí)候老是會(huì)出現(xiàn)下標(biāo)事甜、Rect等問(wèn)題(設(shè)想不周全。滔韵。逻谦。),所以最好為了保險(xiǎn)起見(jiàn)還是多留一個(gè)吧……后面優(yōu)化一下應(yīng)該就不需要了~
還有陪蜻,這里回答一下為什么我選擇NSMutableSet
邦马,原因是[_visibleViews minusSet:_reusableViews];
,這里一句話(huà)就可以把Visible池中的過(guò)時(shí)view去掉,節(jié)省了遍歷的時(shí)間滋将,而且又方便邻悬。
到目前為止,重用機(jī)制所需要的操作我們都實(shí)現(xiàn)了随闽,那我們應(yīng)該在什么時(shí)機(jī)去調(diào)用這些操作呢父丰?
由于在我們開(kāi)始滑動(dòng)的時(shí)候,無(wú)論是左滑還是右滑掘宪,只要滑動(dòng)了蛾扇,前一個(gè)或下一個(gè)view就會(huì)馬上出現(xiàn)了,所以上面的showNewImage
方法應(yīng)該是在滑動(dòng)開(kāi)始時(shí)就馬上調(diào)用魏滚。
沒(méi)錯(cuò)镀首,我們應(yīng)該要在UIScrollViewDelegate
的方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView
中調(diào)用showNewImage
方法。
并且鼠次,當(dāng)滑動(dòng)結(jié)束后更哄,及時(shí)更新當(dāng)前顯示的view的index。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self showNewImage];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
_currentIndex = _scrollView.contentOffset.x / 320;
}
好了须眷,到這里為止竖瘾,重用機(jī)制的思路和調(diào)用時(shí)機(jī)都講完了,其實(shí)里面的思路都比較簡(jiǎn)單花颗,在實(shí)現(xiàn)過(guò)程中計(jì)較容易出錯(cuò)的是由于index弄錯(cuò)而導(dǎo)致了view顯示不正確捕传,或者整個(gè)app崩掉。這時(shí)要需要來(lái)個(gè)斷點(diǎn)調(diào)試扩劝,把斷點(diǎn)設(shè)在新加入的代碼中庸论,一步一步看各個(gè)變量,對(duì)象的值有沒(méi)有異常棒呛。
最后聂示,回到思路1.0代碼中的問(wèn)題,那樣的圖片導(dǎo)入方法好不好簇秒,有沒(méi)有問(wèn)題鱼喉?
答案肯定是有問(wèn)題的,而且問(wèn)題大得很趋观。
用UIImage *image = [UIImage imageNamed:imageName];
導(dǎo)入的圖就算在后面把image設(shè)成nil
扛禽,圖片也是不會(huì)自動(dòng)釋放的。這個(gè)方法適用場(chǎng)景是UI的背景圖皱坛,或者需要大量重復(fù)使用同一個(gè)圖片的地方编曼。
而我們的圖片瀏覽器顯然不符合上述情況,圖片無(wú)法自動(dòng)釋放直接導(dǎo)致內(nèi)存不斷上漲剩辟,那再多的重用也是百搭……
所以這里用UIImage *image = [UIImage imageWithContentsOfFile:path];
更好掐场,圖片可以自動(dòng)釋放往扔,正合我們心意~
- (ZoomingScrollView *)viewAtIndex:(NSUInteger) index {
ZoomingScrollView *zoomingView = [self dequeReusableView];
NSString *imageName = [NSString stringWithFormat:@"%lu", index+1];
// // 這是一個(gè)坑爹的方法,用這個(gè)方法load的圖無(wú)法自動(dòng)釋放
// UIImage *image = [UIImage imageNamed:imageName];
NSString *path = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
[zoomingView setDisplayImage:image];
CGFloat xPosition = index * kScreenFrame.size.width;
CGRect viewFrame = CGRectMake(xPosition, 0, kScreenFrame.size.width, kScreenFrame.size.height);
zoomingView.frame = viewFrame;
zoomingView.tag = 1000 + index;
[_visibleViews addObject:zoomingView];
return zoomingView;
}
先到這里吧熊户,感謝觀看萍膛,謝謝!
如有不足嚷堡,懇請(qǐng)不吝賜教卦羡!及時(shí)私信我吧~