UICollectionView的數據預加載及圖片加載邏輯的優化

原文? http://blog.vars.me/blog/2015/04/26/UICollectionView-Optimizing/

主題 iOS開發 C語言

當App中使用了 UICollectionView 以瀑布流的形式來呈現數據時,站在用戶的角度,用戶在自上至下一頁一頁瀏覽這些內容的過程中,當用戶感到滑動很流暢自然,每頁內容從無到有需要用戶等待的時間很短甚至幾乎感覺不到,那么 UICollectionView 才會帶給用戶一個很好的體驗。本文介紹了為了達到這兩個目的所作出的一些客戶端的優化。

數據的預加載

數據預加載的目的是不必等到用戶某一時刻瀏覽到CollectionView的末尾了,也即本地已經沒有更多數據展示了才去發請求拿下一頁數據,而是有一個預判,用戶就快要看完本地的數據了,可以向Server要下一頁數據了!

為了實現預加載,最開始的方案是在UI層面的預判。根據 UICollectionView 的基類是 UIScrollView ,大致思路是對于沿豎直方向滾動的CollectionView,考察它的 contentOffset.y 和 conetntSize.height ,結合CollectionView的 frame.size.height ,可以計算CollectionView全部內容底下還有多高沒展示出來,如果高度小于我們預先設定的閾值(用戶快滑到底了),那么就觸發加載下一頁的請求。

這樣做似乎沒什么問題,但是仔細想想,其實并不優雅。一方面,一旦有UI調整的需求,CollectionView每行的高度有調整時,我們也要去調整閾值,來決定是否去請求下一頁數據;另一方面,App中不同場景下的CollectionView每行高度不同,需要根據不同場景去Tuning,找出合適的閾值。

后來很自然想到在邏輯上進行預判,也就是我們現在使用的方案。

UICollectionView 每個Cell都需要一個數據模型對象(Data Transfer Object,下稱DTO)來支持它的顯示,通??蛻舳四玫降姆斩朔祷氐臄祿螅鲆幌盗械慕馕觯玫揭粋€一個DTO,用以支持CollectionView的展示。到代碼層面DTO們被保存在一個數組里,任意時刻在正確的狀態下 UICollectionView 的總Cell數量應該跟當前本地DTO的個數相等,Cell跟DTO是一一對應的關系, 數據的預加載本質上就是DTO的預加載 。

用戶在滾動 UICollectionView 時,當 UICollectionView 根據預定的配置覺得它該展示某行某列的Cell時,會向它的DataSource[2]發送 collectionView:cellForItemAtIndexPath: 消息[3],詢問那行那列該展示什么,這個方法返回一個Cell對象, UICollectionView 拿到這個Cell后就把它展示在相應位置。通常這個方法中要做的重要事情就是去上文提到的保存DTO的數組中根據Cell的行列索引找到這個Cell對應的DTO,根據DTO對Cell配置一番,返回給 UICollectionView 。

順著這個思路,在這個方法中可以知道當前 UICollectionView 需要展示的Cell的索引,由于Cell跟DTO是一一對應的關系,那我們也知道了當前需要的DTO在總數據模型對象中的索引,當剩下的數據模型對象不夠支持一頁的顯示時,就去請求下一頁。

表達的可能有點抽象,假設請求一次Server返回20個DTO,過程可以更形象化一點:

- CollectionView: 數據源數據源,用戶滑到第181個Cell要露出來了,快給我!

- DataSource: 好的,我首先要去拿第181個Cell對應的DTO,根據這個配置好一個Cell給你去展示!

等等,你都已經展示到第181個Cell了啊!我發現DTO目前本地總共只有200個,200 - 181 = 19 < 20不夠支持你展示下一頁所需要的20個Cell了,我先發起一個異步請求,去拿新一頁的DTO!

關鍵代碼,很簡單:

NSUInteger countOfDataModel = dataModel.count; // 目前本地有的DTO數量

NSUInteger currentRequestIndex = indexPath.row; // 當前需要的Cell索引,也即當前需要的數據模型索引

if (countOfDataModel - currentRequestIndex < 19) {

[self fetchNextPageAsync];

}

要注意的問題是要做好防止重復發送請求的保護工作。

圖片加載邏輯優化

當 UICollectionView 的每個Cell都需要展示一個(或多個)圖片時,在上文提到的根據DTO配置Cell過程中,會根據DTO中指定的圖片的URL,發送一個異步的圖片請求,等到圖片請求完畢了,再把圖片展示到對應的Cell上(當然,可以把這一切交給 SDWebImage : )。

或許你會問,加載圖片已經是異步了啊,我還要優化什么?不,這遠遠不夠。在實際的測試中,這種樸素的做法依然會帶來明顯的滑動過程的卡頓。使用Instruments進行profile發現,在滑動過程中始終會丟那么15幀左右,不能忍!

再回到 UICollectionView 繼承自 UIScrollView 上來。通過 UIScrollView 的Delegate,我們能感知到滑動過程中CollectionView的各種關鍵狀態,包括用戶的手是否正在拖拽,以及CollectionView是否正在滑動、減速等等,這就是我們優化的秘密武器!

那么,本著不該做的事情不要做,或者等到不得不做的時候再做的原則,讓我們分析用戶在滑動CollectionView的過程中有哪些地方可以細摳。

用戶在滑動(拖拽)CollectionView時(手與屏幕正在接觸),很有可能是用戶在認真逐個瀏覽每個Cell,要去加載當前可見Cell的圖片

用戶滑動CollectionView結束后,手離開了屏幕,并引發了CollectionView減速時, 預判 CollectionView減速結束后靜止時的狀態,對于那些將來靜止時用戶可見的Cell,提前去加載它們的圖片;對于那些只是“曇花一現”的Cell,即它們只是在減速的過程中出現那么一剎那,就被“頂”上去了,只加載這些Cell中圖片在本地有緩存的圖片(從內存中加載,不值得去發網絡請求,即使是異步的也不值得)

減速結束后,CollectionView處于靜止狀態,加載當前全部可見Cell的圖片

OK,那么來看我們怎么實現它。

對于CollectionView的每個Cell,我們給它添加一個異步加載圖片的方法 loadImage 。直接上關鍵代碼,看了便知。

// CollectionView將來靜止時可見的區域,同時也是標識CollectionView當前是正在被用戶拖拽還是已經被拖拽完畢并正在減速

@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,在根據Cell的DTO配置Cell時為其賦值

if (!cell.targetURLString) {

return;

}

// Cell的imageURLString是Cell的當前正在顯示的圖片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;

// 如果正在減速而且當前Cell的frame不在將來滑動停止后的可見區域

if (self.targetRect && !CGRectIntersectsRect(self.targetRect.CGRectValue, cellFrame)) {

// 那么只有Cell的targetURL在內存的緩存中,才去加載它

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一下,發現網速良好情況下滑動時幀率只丟了那么1、2幀,而且滑動起來無明顯卡頓!

要么不做,要么做絕

哈哈,這個有點狠啊,頗有朱元璋的風格。

做了這么多后,我們發現,數據預加載完畢后,向CollectionView發送 reloadData 消息通知它數據模型變化時,就在這一瞬間,還是會導致CollectionView卡頓那么一下下。

好吧不能忍,封裝一個我們自己的 reloadData 方法,在這里簡單的hold住reload,根據上文中的 targetRect 屬性的標記作用,當且僅當在CollectionView減速停止后,再去真正向它發送 reloadData 消息。在這里僅提供思路,不做贅述了。

此外,在開發中,我們把這一系列的方法以 NSObject 類的Category形式做一個封裝,這樣不管誰是CollectionView的Delegate或者DataSource都可以從容應對。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容