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都可以从容应对。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容