RACollectionViewReorderableTripletLayout第三方开源库的优化

对RACollectionViewReorderableTripletLayout第三方开源库的优化

项目中用到了RACollectionViewReorderableTripletLayout这个开源库,这个库非常棒,帮我们解决了大部分问题,向作者致敬!但是在开发过程中遇到了性能方面一些问题,当数据量大的情况下,拖动图片十分卡顿,特此记录下解决办法。

关于拖图的原理网上有很多了在此不做赘述,只说关键的几个方法。在拖图的时候手指 point 变化以及自动滚动的时候都会调用moveItemIfNeeded,这个方法里调用performBatchUpdates刷新 collectionView . 由于作者在RACollectionViewTripletLayout类重写了layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:方法,刷新的时候这两个方法都会被调用。那么我们就看下这两个方法究竟写了什么,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
_oldRect = rect;
NSMutableArray *attributesArray = [NSMutableArray array];
for (NSInteger i = 0; i < self.collectionView.numberOfSections; i++) {
NSInteger numberOfCellsInSection = [self.collectionView numberOfItemsInSection:i];
for (NSInteger j = 0; j < numberOfCellsInSection; j++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
if (CGRectIntersectsRect(rect, attributes.frame)) {
[attributesArray addObject:attributes];
}
}
}
_oldArray = attributesArray;
return attributesArray;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//cellSize
CGFloat largeCellSideLength = (2.f * (_collectionViewSize.width - _insets.left - _insets.right) - _itemSpacing) / 3.f;
CGFloat smallCellSideLength = (largeCellSideLength - _itemSpacing) / 2.f;
_largeCellSize = CGSizeMake(largeCellSideLength, largeCellSideLength);
_smallCellSize = CGSizeMake(smallCellSideLength, smallCellSideLength);
if ([self.delegate respondsToSelector:@selector(collectionView:sizeForLargeItemsInSection:)]) {
if (!CGSizeEqualToSize([self.delegate collectionView:self.collectionView sizeForLargeItemsInSection:indexPath.section], RACollectionViewTripletLayoutStyleSquare)) {
_largeCellSize = [self.delegate collectionView:self.collectionView sizeForLargeItemsInSection:indexPath.section];
_smallCellSize = CGSizeMake(_collectionViewSize.width - _largeCellSize.width - _itemSpacing - _insets.left - _insets.right, (_largeCellSize.height / 2.f) - (_itemSpacing / 2.f));
}
}
if (!_largeCellSizeArray) {
_largeCellSizeArray = [NSMutableArray array];
}
if (!_smallCellSizeArray) {
_smallCellSizeArray = [NSMutableArray array];
}
_largeCellSizeArray[indexPath.section] = [NSValue valueWithCGSize:_largeCellSize];
_smallCellSizeArray[indexPath.section] = [NSValue valueWithCGSize:_smallCellSize];
//section height
CGFloat sectionHeight = 0;
for (NSInteger i = 0; i <= indexPath.section - 1; i++) {
NSInteger cellsCount = [self.collectionView numberOfItemsInSection:i];
CGFloat largeCellHeight = [_largeCellSizeArray[i] CGSizeValue].height;
CGFloat smallCellHeight = [_smallCellSizeArray[i] CGSizeValue].height;
NSInteger lines = ceil((CGFloat)cellsCount / 3.f);
sectionHeight += lines * (_lineSpacing + largeCellHeight) + _sectionSpacing;
if ((cellsCount - 1) % 3 == 0 && (cellsCount - 1) % 6 != 0) {
sectionHeight -= smallCellHeight + _itemSpacing;
}
}
if (sectionHeight > 0) {
sectionHeight -= _lineSpacing;
}
NSInteger line = indexPath.item / 3;
CGFloat lineSpaceForIndexPath = _lineSpacing * line;
CGFloat lineOriginY = _largeCellSize.height * line + sectionHeight + lineSpaceForIndexPath + _insets.top;
CGFloat rightSideLargeCellOriginX = _collectionViewSize.width - _largeCellSize.width - _insets.right;
CGFloat rightSideSmallCellOriginX = _collectionViewSize.width - _smallCellSize.width - _insets.right;
if (indexPath.item % 6 == 0) {
attribute.frame = CGRectMake(_insets.left, lineOriginY, _largeCellSize.width, _largeCellSize.height);
}else if ((indexPath.item + 1) % 6 == 0) {
attribute.frame = CGRectMake(rightSideLargeCellOriginX, lineOriginY, _largeCellSize.width, _largeCellSize.height);
}else if (line % 2 == 0) {
if (indexPath.item % 2 != 0) {
attribute.frame = CGRectMake(rightSideSmallCellOriginX, lineOriginY, _smallCellSize.width, _smallCellSize.height);
}else {
attribute.frame =CGRectMake(rightSideSmallCellOriginX, lineOriginY + _smallCellSize.height + _itemSpacing, _smallCellSize.width, _smallCellSize.height);
}
}else {
if (indexPath.item % 2 != 0) {
attribute.frame = CGRectMake(_insets.left, lineOriginY, _smallCellSize.width, _smallCellSize.height);
}else {
attribute.frame =CGRectMake(_insets.left, lineOriginY + _smallCellSize.height + _itemSpacing, _smallCellSize.width, _smallCellSize.height);
}
}
return attribute;
}

layoutAttributesForElementsInRect: 根据传入的 rect 参数,返回 rect 下的对应 attributes 数组,用于collectionview 布局。方法会遍历所有 section 以及所有 item.在遍历的时候还会调用layoutAttributesForItemAtIndexPath: .

layoutAttributesForItemAtIndexPath: 会根据传入的 indexPath 索引来生成 attribute 并返回。这个方法里嵌套了一个循环来计算 attribute 的 frame。

可以看出,其实这是一个三个循环的嵌套,时间复杂度为O(n^3),由于我们项目的独特性以及复杂性,时间复杂度达到了O(n^4),看来这就是导致卡顿的主要原因之一了。

既然找到了造成卡顿的症结,那么我们就着手解决吧,这里讲下我的处理办法,首先,将这个 attributes 数组缓存起来供拖动的时候使用,使用这个缓存数组前,我们需要一个标志位来告诉 layoutAttributesForElementsInRect这个方法 return 缓存数组,不需要再次遍历计算了。这个标志位在何时修改呢? 因为这个方法的触发不仅仅是在 UIGestureRecognizerState 的时候,UICollectionView 在 layoutSubview 的时候也会调用。于是我将标志位的修改放在了UIGestureRecognizerStateChanged的时候,也就是手势开始改变的时候。于是修改后的layoutAttributesForElementsInRect方法类似这个样子

1
2
3
4
5
6
7
8
9
10
11
12
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
PDCollectionView *collectionview = (PDCollectionView *)self.collectionView;
if (collectionview.shouldCacheAttributes) {
if (collectionview.attributesArray.count == 0) {
//orignal Logic
}else{
return collectionview.shouldCacheAttributes;
}
}else{
//orignal Logic
}

如此一来时间复杂度降到了O(1),流畅性经过测试已经到了60fps,效果明显。

以上是我的测试,对于这个问题的处理,如果哪位老师能找到更好的解决办法,欢迎分享出来。

虽然流畅性问题解决了,但是出现了一个费解的问题,当用户多次拖动的时候,会发现 CPU 利用率降不下来,随着拖动操作的增加,利用率高达90%,但是利用 instrument 查看,定位的代码是有关系统 CA 的方法,怀疑在系统内部进行loop导致。由于用户拖动的次数并不会很多,以及项目时间原因,这个问题的优先级放到了发版后,所以暂时没有解决,后期解决了会补充这篇 blog,同时如果哪位老师有解决方法,欢迎告知!

显示评论