object-c基于collectionView和SDWebImage做的預(yù)加載優(yōu)化
當(dāng)App中使用了 UICollectionView 以瀑布流的形式來呈現(xiàn)數(shù)據(jù)時(shí),站在用戶的角度,用戶在自上至下一頁一頁瀏覽這些內(nèi)容的過程中,當(dāng)用戶感到滑動(dòng)很流暢自然,每頁內(nèi)容從無到有需要用戶等待的時(shí)間很短甚至幾乎感覺不到,那么 UICollectionView 才會(huì)帶給用戶一個(gè)很好的體驗(yàn)。本文介紹了為了達(dá)到這兩個(gè)目的所作出的一些客戶端的優(yōu)化。
數(shù)據(jù)的預(yù)加載
數(shù)據(jù)預(yù)加載的目的是不必等到用戶某一時(shí)刻瀏覽到CollectionView的末尾了,也即本地已經(jīng)沒有更多數(shù)據(jù)展示了才去發(fā)請(qǐng)求拿下一頁數(shù)據(jù),而是有一個(gè)預(yù)判,用戶就快要看完本地的數(shù)據(jù)了,可以向Server要下一頁數(shù)據(jù)了!
為了實(shí)現(xiàn)預(yù)加載,最開始的方案是在UI層面的預(yù)判。根據(jù) UICollectionView 的基類是 UIScrollView ,大致思路是對(duì)于沿豎直方向滾動(dòng)的CollectionView,考察它的 contentOffset.y 和 conetntSize.height ,結(jié)合CollectionView的 frame.size.height ,可以計(jì)算CollectionView全部內(nèi)容底下還有多高沒展示出來,如果高度小于我們預(yù)先設(shè)定的閾值(用戶快滑到底了),那么就觸發(fā)加載下一頁的請(qǐng)求。
這樣做似乎沒什么問題,但是仔細(xì)想想,其實(shí)并不優(yōu)雅。一方面,一旦有UI調(diào)整的需求,CollectionView每行的高度有調(diào)整時(shí),我們也要去調(diào)整閾值,來決定是否去請(qǐng)求下一頁數(shù)據(jù);另一方面,App中不同場景下的CollectionView每行高度不同,需要根據(jù)不同場景去Tuning,找出合適的閾值。
后來很自然想到在邏輯上進(jìn)行預(yù)判,也就是我們現(xiàn)在使用的方案。
UICollectionView 每個(gè)Cell都需要一個(gè)數(shù)據(jù)模型對(duì)象(Data Transfer Object,下稱DTO)來支持它的顯示,通常客戶端拿到的服務(wù)端返回的數(shù)據(jù)后,做一系列的解析,得到一個(gè)一個(gè)DTO,用以支持CollectionView的展示。到代碼層面DTO們被保存在一個(gè)數(shù)組里,任意時(shí)刻在正確的狀態(tài)下 UICollectionView 的總Cell數(shù)量應(yīng)該跟當(dāng)前本地DTO的個(gè)數(shù)相等,Cell跟DTO是一一對(duì)應(yīng)的關(guān)系, 數(shù)據(jù)的預(yù)加載本質(zhì)上就是DTO的預(yù)加載 。
用戶在滾動(dòng) UICollectionView 時(shí),當(dāng) UICollectionView 根據(jù)預(yù)定的配置覺得它該展示某行某列的Cell時(shí),會(huì)向它的DataSource[2]發(fā)送 collectionView:cellForItemAtIndexPath: 消息[3],詢問那行那列該展示什么,這個(gè)方法返回一個(gè)Cell對(duì)象, UICollectionView 拿到這個(gè)Cell后就把它展示在相應(yīng)位置。通常這個(gè)方法中要做的重要事情就是去上文提到的保存DTO的數(shù)組中根據(jù)Cell的行列索引找到這個(gè)Cell對(duì)應(yīng)的DTO,根據(jù)DTO對(duì)Cell配置一番,返回給 UICollectionView 。
順著這個(gè)思路,在這個(gè)方法中可以知道當(dāng)前 UICollectionView 需要展示的Cell的索引,由于Cell跟DTO是一一對(duì)應(yīng)的關(guān)系,那我們也知道了當(dāng)前需要的DTO在總數(shù)據(jù)模型對(duì)象中的索引,當(dāng)剩下的數(shù)據(jù)模型對(duì)象不夠支持一頁的顯示時(shí),就去請(qǐng)求下一頁。
表達(dá)的可能有點(diǎn)抽象,假設(shè)請(qǐng)求一次Server返回20個(gè)DTO,過程可以更形象化一點(diǎn):
- CollectionView: 數(shù)據(jù)源數(shù)據(jù)源,用戶滑到第181個(gè)Cell要露出來了,快給我!
- DataSource: 好的,我首先要去拿第181個(gè)Cell對(duì)應(yīng)的DTO,根據(jù)這個(gè)配置好一個(gè)Cell給你去展示!
等等,你都已經(jīng)展示到第181個(gè)Cell了啊!我發(fā)現(xiàn)DTO目前本地總共只有200個(gè),200 - 181 = 19 < 20不夠支持你展示下一頁所需要的20個(gè)Cell了,我先發(fā)起一個(gè)異步請(qǐng)求,去拿新一頁的DTO!
關(guān)鍵代碼,很簡單:
NSUInteger countOfDataModel = dataModel.count; // 目前本地有的DTO數(shù)量
NSUInteger currentRequestIndex = indexPath.row; // 當(dāng)前需要的Cell索引,也即當(dāng)前需要的數(shù)據(jù)模型索引
if (countOfDataModel - currentRequestIndex < 19) {
[self fetchNextPageAsync];
}
要注意的問題是要做好防止重復(fù)發(fā)送請(qǐng)求的保護(hù)工作。
圖片加載邏輯優(yōu)化
當(dāng) UICollectionView 的每個(gè)Cell都需要展示一個(gè)(或多個(gè))圖片時(shí),在上文提到的根據(jù)DTO配置Cell過程中,會(huì)根據(jù)DTO中指定的圖片的URL,發(fā)送一個(gè)異步的圖片請(qǐng)求,等到圖片請(qǐng)求完畢了,再把圖片展示到對(duì)應(yīng)的Cell上(當(dāng)然,可以把這一切交給 SDWebImage : )。
或許你會(huì)問,加載圖片已經(jīng)是異步了啊,我還要優(yōu)化什么?不,這遠(yuǎn)遠(yuǎn)不夠。在實(shí)際的測試中,這種樸素的做法依然會(huì)帶來明顯的滑動(dòng)過程的卡頓。使用Instruments進(jìn)行profile發(fā)現(xiàn),在滑動(dòng)過程中始終會(huì)丟那么15幀左右,不能忍!
再回到 UICollectionView 繼承自 UIScrollView 上來。通過 UIScrollView 的Delegate,我們能感知到滑動(dòng)過程中CollectionView的各種關(guān)鍵狀態(tài),包括用戶的手是否正在拖拽,以及CollectionView是否正在滑動(dòng)、減速等等,這就是我們優(yōu)化的秘密武器!
那么,本著不該做的事情不要做,或者等到不得不做的時(shí)候再做的原則,讓我們分析用戶在滑動(dòng)CollectionView的過程中有哪些地方可以細(xì)摳。
用戶在滑動(dòng)(拖拽)CollectionView時(shí)(手與屏幕正在接觸),很有可能是用戶在認(rèn)真逐個(gè)瀏覽每個(gè)Cell,要去加載當(dāng)前可見Cell的圖片
用戶滑動(dòng)CollectionView結(jié)束后,手離開了屏幕,并引發(fā)了CollectionView減速時(shí), 預(yù)判 CollectionView減速結(jié)束后靜止時(shí)的狀態(tài),對(duì)于那些將來靜止時(shí)用戶可見的Cell,提前去加載它們的圖片;對(duì)于那些只是“曇花一現(xiàn)”的Cell,即它們只是在減速的過程中出現(xiàn)那么一剎那,就被“頂”上去了,只加載這些Cell中圖片在本地有緩存的圖片(從內(nèi)存中加載,不值得去發(fā)網(wǎng)絡(luò)請(qǐng)求,即使是異步的也不值得)
減速結(jié)束后,CollectionView處于靜止?fàn)顟B(tài),加載當(dāng)前全部可見Cell的圖片
OK,那么來看我們?cè)趺磳?shí)現(xiàn)它。
對(duì)于CollectionView的每個(gè)Cell,我們給它添加一個(gè)異步加載圖片的方法 loadImage 。直接上關(guān)鍵代碼,看了便知。
// CollectionView將來靜止時(shí)可見的區(qū)域,同時(shí)也是標(biāo)識(shí)CollectionView當(dāng)前是正在被用戶拖拽還是已經(jīng)被拖拽完畢并正在減速
@property (nonatomic, strong) CGRect *targetRect;
#pragma mark - UICollectionView DataSource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// ....
[self loadImageForCell:cell atIndexPath:indexPath];
// ....
}
#pragma mark - UIScrollView Delegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.targetRect = nil;
[self loadImageForVisibleCells];
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
self.targetRect = CGRectMake(targetContentOffset->x, targetContentOffset->y, scrollView.frame.size.width, scrollView.frame.size.height);
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.targetRect = nil;
[self loadImageForVisibleCells];
}
#pragma mark - Decide to Load Image For Cells
- (void)loadImageForCell:(AESmartCollectionFlowViewCell *)cell
atIndexPath:(NSIndexPath *)indexPath {
// Cell的targetURLString是指派給Cell的新的圖片URL,在根據(jù)Cell的DTO配置Cell時(shí)為其賦值
if (!cell.targetURLString) {
return;
}
// Cell的imageURLString是Cell的當(dāng)前正在顯示的圖片URL
if (![cell.targetURLString isEqualToString:cell.imageURLString] || cell.isDisplayingPlaceholderNow) {
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UICollectionViewLayoutAttributes *attr = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
CGRect cellFrame = attr.frame;
BOOL shouldLoadImageForCurrentCell = YES;
// 如果正在減速而且當(dāng)前Cell的frame不在將來滑動(dòng)停止后的可見區(qū)域
if (self.targetRect && !CGRectIntersectsRect(self.targetRect.CGRectValue, cellFrame)) {
// 那么只有Cell的targetURL在內(nèi)存的緩存中,才去加載它
SDImageCache *imageCache = [SDImageCache sharedImageCache];
NSString *key = [manager cacheKeyForURL:[NSURL URLWithString:cell.targetURLString]];
if (![imageCache imageFromMemoryCacheForKey:key]) {
shouldLoadImageForCurrentCell = NO;
}
}
if (shouldLoadImageForCurrentCell) {
[cell loadImage];
}
}
}
- (void)loadImageForVisibleCells {
NSArray *visibleCells = [self.collectionView visibleCells];
for (UICollectionViewCell *cell in visibleCells) {
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
[self loadImageForCell:cell atIndexPath:indexPath];
}
}
做了這些努力后,再去profile一下,發(fā)現(xiàn)網(wǎng)速良好情況下滑動(dòng)時(shí)幀率只丟了那么1、2幀,而且滑動(dòng)起來無明顯卡頓!
要么不做,要么做絕
哈哈,這個(gè)有點(diǎn)狠啊,頗有朱元璋的風(fēng)格。
做了這么多后,我們發(fā)現(xiàn),數(shù)據(jù)預(yù)加載完畢后,向CollectionView發(fā)送 reloadData 消息通知它數(shù)據(jù)模型變化時(shí),就在這一瞬間,還是會(huì)導(dǎo)致CollectionView卡頓那么一下下。
好吧不能忍,封裝一個(gè)我們自己的 reloadData 方法,在這里簡單的hold住reload,根據(jù)上文中的 targetRect 屬性的標(biāo)記作用,當(dāng)且僅當(dāng)在CollectionView減速停止后,再去真正向它發(fā)送 reloadData 消息。在這里僅提供思路,不做贅述了。
此外,在開發(fā)中,我們把這一系列的方法以 NSObject 類的Category形式做一個(gè)封裝,這樣不管誰是CollectionView的Delegate或者DataSource都可以從容應(yīng)對(duì)。
作者:指尖的跳動(dòng)
鏈接:http://www.itdecent.cn/p/32fe33caca3f
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。