iOS優(yōu)化(二)滑動(dòng)優(yōu)化的一些經(jīng)驗(yàn)

優(yōu)化緣由

此次優(yōu)化的契機(jī)是App內(nèi)瀑布流頁(yè)面大數(shù)據(jù)量時(shí)進(jìn)入/滑動(dòng)異常卡頓,F(xiàn)PS 在7P上30-40,6P上10,5C上僅僅只有5。

前期準(zhǔn)備

集成GDPerformanceView以方便查看FPS

優(yōu)化過(guò)程

1.排除干擾項(xiàng)

排除以下可能影響加載速度的干擾項(xiàng):
1)去除加載/緩存/繪制圖片過(guò)程;
2)所有scrollView相關(guān)的delegate代碼去除;
3)跟滑動(dòng)有關(guān)的KVO,主要是contensize相關(guān)去除;
4)檢查是否hook了類(lèi)里的一些方法(如果有隊(duì)友這么做了,拿著刀找他去就行了);
去除以上干擾的狀態(tài),我稱(chēng)之為“白板狀態(tài)”,先優(yōu)化好此狀態(tài)的幀率,再加回這些進(jìn)行進(jìn)一步優(yōu)化,不幸的是,白板狀態(tài)下,F(xiàn)PS并沒(méi)有顯著提升。

2.優(yōu)化開(kāi)始

使用instrument的Time Profiler調(diào)試
1)collectionView的willDisplayCell方法耗時(shí)較多,檢查代碼發(fā)現(xiàn)是同事為了嘗試解決首次進(jìn)入時(shí)cell寬高會(huì)執(zhí)行從0到其實(shí)際寬度動(dòng)畫(huà)的bug,調(diào)用了layoutIfNeeded,去除后并未復(fù)現(xiàn)該bug。此時(shí),7P表現(xiàn)良好FPS達(dá)到了55+,6P仍然處于20+,此時(shí)思路應(yīng)該是由于大量調(diào)用CPU,故6P的FPS仍然表現(xiàn)不佳;
.
2)經(jīng)檢查,瀑布流滑動(dòng)時(shí),CollectionViewLayout的prepareLayout調(diào)用了過(guò)多次數(shù)(對(duì)瀑布流原理不清楚的同學(xué)可以先看我這一篇UICollectionView的靈活布局 --從一個(gè)需求談起),而prepareLayout意味著重新計(jì)算布局,圖片量越大,計(jì)算越耗時(shí)(我們的瀑布流使用了循環(huán)嵌套循環(huán))。根據(jù)collectionView的生命周期,當(dāng)contentSize變化的時(shí)候才會(huì)調(diào)用prepareLayout,或者在Layout里重寫(xiě)了方法:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;

經(jīng)檢查發(fā)現(xiàn)隨著滑動(dòng),newBounds的y一直變化,導(dǎo)致持續(xù)重新布局。之所以重寫(xiě)shouldInvalidateLayoutForBoundsChange方法,是因?yàn)槲覀兪褂玫?a target="_blank" rel="nofollow">DZNEmptyDataSet在有contentOffset的情況下,刪除collectionView所有數(shù)據(jù)源之后的布局出現(xiàn)問(wèn)題。因此我們要針對(duì)shouldInvalidate方法的調(diào)用進(jìn)行優(yōu)化,添加magic number,判斷僅當(dāng)size變化時(shí),再重新繪制瀑布流:

@property (assign, nonatomic) CGSize newBoundsSize;

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    if (CGSizeEqualToSize(self.newBoundsSize, newBounds.size)) {
        return NO;
    }
    self.newBoundsSize = newBounds.size;
    return YES;
}

此時(shí)白板狀態(tài)7P的FPS已達(dá)60。6p正常滑動(dòng)已經(jīng)50+,快速滑動(dòng)時(shí)仍會(huì)降至20-30;
.
3)下一步的優(yōu)化思路就是處理快速滑動(dòng),參照VVeboTableViewDemo中的方法進(jìn)行優(yōu)化。
需要注意和這個(gè)demo不同的是:
(1)collectionView無(wú)法通過(guò)CGRect取到即將顯示的所有indexPath;
(2)collectionView通過(guò)point取indexPath的時(shí)候point不能是右下角邊緣值,不然會(huì)返回item為0的indexPath;
(3)因?yàn)閐emo里是寫(xiě)了個(gè)tableView的子類(lèi),而我直接在controller里處理了,所以hitTest 這里我取了一個(gè)折中的處理,但是效果并不是100%完美,在decelerating的過(guò)程中用手指停住,手指只要不松開(kāi)就不會(huì)加載當(dāng)前的cell;
(4)targetContentOffset:(inout CGPoint *)targetContentOffset 最好不要直接賦值,inout參數(shù)在別處改動(dòng)了會(huì)很麻煩。

下面貼上代碼來(lái)說(shuō)明這四點(diǎn):

@property (nonatomic, strong) NSMutableArray *needLoadArr;
//為了處理(3)中的hitTest
@property (nonatomic, assign) CGPoint targetOffset;
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    //conference : https://github.com/johnil/VVeboTableViewDemo/blob/master/VVeboTableViewDemo/VVeboTableView.m
    //FPS optimition part 3-1 : loadCell if needed
    NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:*targetContentOffset];
    NSIndexPath *firstVisibleIndexPath = [[self.collectionView indexPathsForVisibleItems] firstObject];
    //判斷快速滑動(dòng)超過(guò)20個(gè)item后不加載中間的cell
    NSInteger skipCount = 20;
    if (labs(firstVisibleIndexPath.item - indexPath.item) > skipCount) {
        //處理(1)中無(wú)法跟tableView一樣根據(jù)CGRect取到indexPath
        NSIndexPath *firstIndexPath = [self.collectionView indexPathForItemAtPoint:CGPointMake(0, targetContentOffset->y)];
        NSIndexPath *lastIndexPath = [self.collectionView indexPathForItemAtPoint:CGPointMake(self.collectionView.frame.size.width - 10.f,
        targetContentOffset->y + self.collectionView.frame.size.height - 10.f)]; 
        // - 10.f 是為了處理(2)中的point準(zhǔn)確性

        NSMutableArray *arr = [NSMutableArray new];
        for (NSUInteger i = firstIndexPath.item; i <= lastIndexPath.item; i++) {
            [arr addObject:[NSIndexPath indexPathForItem:i inSection:0]];
        }
        
        NSUInteger loadSum = 6;
        if (velocity.y < 0) {
            //scroll up
            if ((lastIndexPath.item + loadSum) < self.viewModel.numberOfItems) {
                for (NSUInteger i = 1; i <= loadSum; i++ ) {
                    [arr addObject:[NSIndexPath indexPathForItem:lastIndexPath.item + i inSection:0]];
                }
            }
        } else {
            //scroll down
            if (firstIndexPath.item > loadSum) {
                for (NSUInteger i = 1; i <= loadSum; i++ ) {
                    [arr addObject:[NSIndexPath indexPathForItem:firstIndexPath.item - i inSection:0]];
                }
            }
        }
        [self.needLoadArr addObjectsFromArray:arr];
        //(4)中處理inout參數(shù)
        self.targetOffset = CGPointMake(targetContentOffset->x, targetContentOffset->y);
    }
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    //FPS optimition part 3-2 : loadCell if needed (when you touch and end decelerating)
    //if use collectionView as subView override - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event instead
    if (!CGPointEqualToPoint(self.targetOffset, CGPointZero) &&
        fabs(self.targetOffset.y - scrollView.contentOffset.y) > 10.f &&
        scrollView.contentOffset.y > scrollView.frame.size.height &&
        scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)) {
        [self.needLoadArr removeAllObjects];
        self.targetOffset = CGPointZero;
        [self.collectionView reloadData];
        
    } else {
        [self.needLoadArr removeAllObjects];
    }
}

最后在cell繪制的時(shí)候判斷,比VVebo多的是還判斷了一步是否在當(dāng)前緩存中。為了讓瀑布流滑動(dòng)體驗(yàn)更好,在進(jìn)入瀑布流的頁(yè)面,我將緩存上限提高到70M,按我們一張圖片在200k-900k的大小來(lái)看,可以滿足大部分情況下的需求。

//FPS optimition part 3-3 : loadCell if needed
if (self.needLoadArr.count > 0 &&
[self.needLoadArr indexOfObject:indexPath] == NSNotFound &&
//判斷是否在緩存中
![rawPhoto imageExistInMemoryWithType:type]) {          
      [cell.imageView loadingState];
      cell.imageView.image = nil;
      return cell;
}

此時(shí)6p幀率升至55+;

4)這里我也嘗試將collectionView的滑動(dòng)速率降至0.5,以減少cell加載次數(shù),但效果并不理想;
5)沉浸式體驗(yàn)下, tabbar的多次出現(xiàn)隱藏對(duì)FPS并無(wú)顯著影響;
6)對(duì)scrollView 的contentSize 的 KVO,設(shè)置了底部圖片frame,對(duì)FPS影響不大,仍然優(yōu)化為到頂部/底部 才改變frame;
7)scollView的didScroll代理調(diào)用了NSDateFormatter,調(diào)用次數(shù)密集, 根據(jù)以往經(jīng)驗(yàn),NSDateFormatter在autorelease下往往不能及時(shí)釋放,故加如autoreleasepool 以保證及時(shí)釋放;

進(jìn)入速度優(yōu)化

進(jìn)入瀑布流之時(shí),所有的cell都會(huì)加載一遍,導(dǎo)致卡頓。起初我以為是collectionViewLayout的某個(gè)方法調(diào)用錯(cuò)了。后來(lái)經(jīng)過(guò)debug發(fā)現(xiàn),collectionView的數(shù)據(jù)源傳入的是mutableArr的count,數(shù)據(jù)在變化,只要deepCopy出來(lái)一份,就解決了這個(gè)問(wèn)題。

總結(jié)

多人協(xié)作開(kāi)發(fā)時(shí),往往就會(huì)因?yàn)楦鞣N原因?qū)е律厦娴膯?wèn)題出現(xiàn),若深入理解相關(guān)知識(shí)的生命周期和一些基礎(chǔ)知識(shí),可以減少上面至少一半的問(wèn)題。
.
另外優(yōu)化也是個(gè)持續(xù)的過(guò)程,平時(shí)測(cè)試也很少可以發(fā)現(xiàn),當(dāng)積累到一定量級(jí)之后才會(huì)顯現(xiàn),也許下次某位同事修復(fù)某個(gè)bug,又會(huì)帶出性能問(wèn)題,整個(gè)優(yōu)化過(guò)程,其實(shí)是十分有趣的,而且優(yōu)化成功后,成就感十足,讓我們痛并快樂(lè)著吧。

簡(jiǎn)書(shū)已經(jīng)棄用,歡迎移步我的小專(zhuān)欄:
https://xiaozhuanlan.com/dahuihuiiOS

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容