1. 程式人生 > >iOS優化(二)滑動優化的一些經驗

iOS優化(二)滑動優化的一些經驗

原文


優化緣由 

此次優化的契機是App內瀑布流頁面大資料量時進入/滑動異常卡頓,FPS 在7P上30-40,6P上10,5C上僅僅只有5。


前期準備 


整合GDPerformanceView
以方便檢視FPS

 

優化過程


1.排除干擾項 

排除以下可能影響載入速度的干擾項:

 

1)去除載入/快取/繪製圖片過程;

 

2)所有scrollView相關的delegate程式碼去除;

 

3)跟滑動有關的KVO,主要是contensize相關去除;

 

4)檢查是否hook了類裡的一些方法(如果有隊友這麼做了,拿著刀找他去就行了);

 

去除以上干擾的狀態,我稱之為“白板狀態”,先優化好此狀態的幀率,再加回這些進行進一步優化,不幸的是,白板狀態下,FPS並沒有顯著提升。


2.優化開始 

使用instrument的Time Profiler除錯

 

1)collectionView的willDisplayCell方法耗時較多,檢查程式碼發現是同事為了嘗試解決首次進入時cell寬高會執行從0到其實際寬度動畫的bug,呼叫了layoutIfNeeded,去除後並未復現該bug。此時,7P表現良好FPS達到了55+,6P仍然處於20+,此時思路應該是由於大量呼叫CPU,故6P的FPS仍然表現不佳;

 


2)經檢查,瀑布流滑動時,CollectionViewLayout的prepareLayout呼叫了過多次數(對瀑布流原理不清楚的同學可以先看我這一篇UICollectionView的靈活佈局 --從一個需求談起
),而prepareLayout意味著重新計算佈局,圖片量越大,計算越耗時(我們的瀑布流使用了迴圈巢狀迴圈)。根據collectionView的生命週期,當contentSize變化的時候才會呼叫prepareLayout,或者在Layout裡重寫了方法:


- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds; 


經檢查發現隨著滑動,newBounds的y一直變化,導致持續重新佈局。之所以重寫shouldInvalidateLayoutForBoundsChange方法,是因為我們使用的DZNEmptyDataSet
在有contentOffset的情況下,刪除collectionView所有資料來源之後的佈局出現問題。因此我們要針對shouldInvalidate方法的呼叫進行優化,新增magic number,判斷僅當size變化時,再重新繪製瀑布流:


@property (assign, nonatomic) CGSize newBoundsSize; 
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { 
if (CGSizeEqualToSize(self.newBoundsSize, newBounds.size)) { 
return NO; 

self.newBoundsSize = newBounds.size; 
return YES; 

此時白板狀態7P的FPS已達60。6p正常滑動已經50+,快速滑動時仍會降至20-30;

 


3)下一步的優化思路就是處理快速滑動,參照VVeboTableViewDemo
中的方法進行優化。

 

需要注意和這個demo不同的是:

 

(1)collectionView無法通過CGRect取到即將顯示的所有indexPath;

 

(2)collectionView通過point取indexPath的時候point不能是右下角邊緣值,不然會返回item為0的indexPath;

 

(3)因為demo裡是寫了個tableView的子類,而我直接在controller裡處理了,所以hitTest 這裡我取了一個折中的處理,但是效果並不是100%完美,在decelerating的過程中用手指停住,手指只要不鬆開就不會載入當前的cell;

 

(4)targetContentOffset:(inout CGPoint *)targetContentOffset 最好不要直接賦值,inout引數在別處改動了會很麻煩。

 

下面貼上程式碼來說明這四點:


@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]; 
//判斷快速滑動超過20個item後不載入中間的cell 
NSInteger skipCount = 20; 
if (labs(firstVisibleIndexPath.item - indexPath.item) > skipCount) { 
//處理(1)中無法跟tableView一樣根據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準確性 
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; iloadSum) { 
for (NSUInteger i = 1; i 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繪製的時候判斷,比VVebo多的是還判斷了一步是否在當前快取中。為了讓瀑布流滑動體驗更好,在進入瀑布流的頁面,我將快取上限提高到70M,按我們一張圖片在200k-900k的大小來看,可以滿足大部分情況下的需求。


//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; 

此時6p幀率升至55+;

 

4)這裡我也嘗試將collectionView的滑動速率降至0.5,以減少cell載入次數,但效果並不理想;

 

5)沉浸式體驗下, tabbar的多次出現隱藏對FPS並無顯著影響;

 

6)對scrollView 的contentSize 的 KVO,設定了底部圖片frame,對FPS影響不大,仍然優化為到頂部/底部 才改變frame;

 

7)scollView的didScroll代理呼叫了NSDateFormatter,呼叫次數密集, 根據以往經驗,NSDateFormatter在autorelease下往往不能及時釋放,故加如autoreleasepool 以保證及時釋放;


進入速度優化 

進入瀑布流之時,所有的cell都會載入一遍,導致卡頓。起初我以為是collectionViewLayout的某個方法呼叫錯了。後來經過debug發現,collectionView的資料來源傳入的是mutableArr的count,資料在變化,只要deepCopy出來一份,就解決了這個問題。


總結 

多人協作開發時,往往就會因為各種原因導致上面的問題出現,若深入理解相關知識的生命週期和一些基礎知識,可以減少上面至少一半的問題。

 

另外優化也是個持續的過程,平時測試也很少可以發現,當積累到一定量級之後才會顯現,也許下次某位同事修復某個bug,又會帶出效能問題,整個優化過程,其實是十分有趣的,而且優化成功後,成就感十足,讓我們痛並快樂著吧。