iOS學習筆記-TableView效能優化篇1

Yang1492955186752發表於2017-12-13

2017-3-3更新

慎重呼叫drawRect方法,因為呼叫這方面之前,CALayer會建立一個與 (寬x高 x 4位元組)大小的一個的圖形上下文,假如是7plus的尺寸,那麼就是1920×1080x4 = 8M記憶體,假如是記憶體敏感的介面,需要斟酌一下,避免記憶體開銷太大。

2017-3-7更新

當網路請求成功返回來資料後通過AFNetworking會將json轉換成字典(絕大多數情況會這麼做,除非指定了AFNetworking的其他序列化方式),我們可能會直接利用MJExtension或者YYmodel之類的工具,將字典對映為model。假如為了更好的效能,可以採取去model化,直接對dictionary進行取值,避免了利用runtime進行取遍歷ivar,property等操作。這樣子效能會好一些,弊端是可擴充套件性差一些。

原文

TableView相信只要是做iOS開發的就不會陌生,目前大多數iOS的app都是採用TabBar+NavigationBar+TableViewController或者CollectionViewController這一主流組合,既然用的這麼頻繁,肯定就會在開發過程中碰到一些問題--比如螢幕掉幀、卡頓等現象。這些現象大幅度的降低了使用者的效能體驗,並提高了crash的頻率。因此如何能優化好tableView就非常考驗程式猿們的功底了。 本猿~啊呸,本人就在開發公司專案的時候遇到這類問題,當快速滑動tableView並且cell中有大量圖片和其他控制元件需要載入時,就會出現嚴重掉幀(我們公司的專案當時大量採用xib現在逐漸用手寫程式碼代替),有時還會crash。由於當時專案比較趕進度,所以沒有時間去優化效能,這種情況直到功能基本完善為止,花了大量功夫進行效能優化。 接下來我會根據tableView的delegate以及dataSource方法的執行順序進行一步一步的講解。

一個tableView需要顯示內容的時候,首先會傳送網路請求,向伺服器請求資料,然後將資料轉為我們可以使用的model或dictionary後進行reload操作。 接下來會向delegate和dataSource傳送資料。這時候會先呼叫- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 這個方法(假設section為1)。根據model獲取cell的行數然後呼叫-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath,根據model計算出cell的高度。由於tableView是繼承自scrollView,所以tableView也會有contentSize屬性。它的contentSize取決於所有cell的高度和。和scrollView有一點不同,tableView只會管理可視的cell高度,這樣做的目的是避免不必要的效能開銷。

大多數情況下我們是將model直接傳給cell然後在cell裡進行計算各控制元件的相對位置(利用AutoLayout和xib)比如:

-(void)setModel:(ImageCellModel *)model {
_model = model;

self.detailsLabel.text = model.news;
self.priceLabel.text = [NSString stringWithFormat:@"¥%@",model.money];
NSString *iconStr = model.User.headIcon;
if (![iconStr isEqualToString:@""]) {
[self.portraitImgView sd_setImageWithURL:[NSURL URLWithString:iconStr]];
}else{
self.portraitImgView.image = [UIImage imageNamed:@"defaultPortrait@2x.png"];
}
self.nicknameLabel.text = model.User.nickName;
if ([model.User.rank isEqualToString:@"0"]) {
self.rankImgView.image = [UIImage imageNamed:@""];
}
}
複製程式碼

但是這樣做假如滑動比較快,且內部控制元件比較複雜會導致CPU的計算量過大,從而導致掉幀。肯定會有人疑惑為什麼會掉幀,這是因為GPU 一個機制叫做垂直同步(簡寫也是 V-Sync),當開啟垂直同步後,GPU 會等待顯示器的 VSync 訊號發出後,才進行新的一幀渲染和緩衝區更新。這樣能解決畫面撕裂現象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。當GPU發出垂直同步即VSync訊號後,CPU開始進行內部控制元件的建立、佈局、解碼和控制元件的相對位置計算。然後將計算好的內容交給GPU進行變換、合成、渲染。然後等待下一個VSync訊號。(這段理論部分來自於YY大神)假如在VSync訊號發出後,CPU進行計算的時間過長,或者GPU進行渲染的時間過長導致兩段時間加起來超過了1個VSync週期,就會將這一幀動畫丟棄,並維持上一幀的畫面從而導致掉幀。 ####那麼我們如何進行優化呢? 最終目的:平衡CPU和GPU的壓力。正確地利用了CPU和GPU資源,使它們均勻地負載,這樣子做FPS會保持在60幀。避免出現CPU滿載GPU低負載或者GPU滿載CPU低負載的情況。 #####如何避免出現CPU滿載GPU低負載呢? 1.不要用AutoLayout,不要用AutoLayout,不要用AutoLayout(這裡的情景是子檢視較多的情況下),重要的事情說3遍。我們進行手動佈局可能會沒那麼方便,但是通過簡單的加減乘除就可以獲取控制元件相對位置和cell的高度。儘管蘋果推薦使用AutoLayout。但是對於那些比較古老的裝置比如我的5S,CPU通過AutoLayout計算佈局會比較吃力,尤其是cell內部的控制元件數量較多的時候。使用的子檢視越多,AutoLayout的效率越低,這是事實。那麼為什麼AutoLayout相對低效呢。是因為它要根據底層“Cassowary”的約束求解系統進行約束計算,從而得到一個唯一解,這時AutoLayout才不會報警告或錯誤(相信拖控制元件的同學肯定遇到過各種黃色警告和紅包約束衝突吧)。假如內部的子控制元件越多,它需要進行的線性或非線性計算量越大,需要求解的約束越多,CPU計算耗費大量時間從而導致超過了一個VSync週期。相反的,假如我們進行手動佈局,都是非常簡單的線性計算,CPU就不用浪費那麼時間,CPU的壓力不會很大,從而平衡了CPU的負載。 借用Draveness的一張圖:

performance-layout-10-90.jpeg

由此可以看出Autolayout和frame的效能差異。 tips :我們可以在tableView進行網路請求成功後立刻進行後臺的佈局計算。比如是我,利用AFN請求資料成功後會在success 的block裡面進行後臺的計算:


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//後臺執行緒進行佈局計算
NSMutableArray *modelAry = [ImageCellModel mj_objectArrayWithKeyValuesArray:responseObject[@"data"]];
NSMutableArray *modelFrameAry = [self modelFramesWithModelAry:modelAry];
dispatch_async(dispatch_get_main_queue(), ^{
//這裡面把計算好的frameModel返回給主執行緒並執行正常的操作
});

- (NSMutableArray *)modelFramesWithModelAry:(NSArray *)modelArray
{
NSMutableArray *frameModels = [NSMutableArray array];
for (ImageCellModel *model in modelArray) {
HomeModelFrame *modelFrame = [[HomeModelFrame alloc] init];
modelFrame.model = model;
[frameModels addObject:modelFrame];
}
return frameModels;
}
複製程式碼

上面程式碼會將請求到的JSON陣列轉換為model陣列,然後將model陣列裡的model轉換為modelFrame(就是根據model的各個屬性計算出frame):

#import <Foundation/Foundation.h>
@class ImageCellModel;
@interface HomeModelFrame : NSObject
@property (nonatomic,strong)ImageCellModel *model;
@property(nonatomic,assign)CGRect avatarFrame;
@property(nonatomic,assign)CGRect nameFrame;
@property(nonatomic,assign)CGRect priceFrame;
@property(nonatomic,assign)CGRect photosFrame;
@property(nonatomic,assign)CGRect labelFrame;
@property(nonatomic,assign)CGRect descriptionFrame;
@property(nonatomic,assign)CGRect timeAndDisFrame;
@property(nonatomic,assign)CGFloat cellHeight;
@property(nonatomic,assign)CGRect dateAndOurLabelFrame;
@end

//在.m中
-(void)setModel:(ImageCellModel *)model {
_model = model;
CGFloat cellW = [UIScreen mainScreen].bounds.size.width;

if (model.PicList.count) {
_photosFrame = CGRectMake(0, 0, cellW , (cellW ) / 2);
}
else{
_photosFrame = CGRectMake(0, 0, cellW, 50);
}
_priceFrame = CGRectMake(cellW - 60, CGRectGetMaxY(_photosFrame) - 28, 60, 20);
_avatarFrame = CGRectMake(15, CGRectGetMaxY(_photosFrame) - 19, 38, 38);
_labelFrame = CGRectMake(CGRectGetMaxX(_avatarFrame) + 10, CGRectGetMaxY(_photosFrame) - 9, 18, 18);
_descriptionFrame = CGRectMake(CGRectGetMaxX(_avatarFrame), CGRectGetMaxY(_labelFrame) + 11, cellW - CGRectGetMaxX(_avatarFrame) - 17, 13);
_dateAndOurLabelFrame = CGRectMake(CGRectGetMaxX(_avatarFrame), CGRectGetMaxY(_descriptionFrame) + 14,180 , 11);
_timeAndDisFrame = CGRectMake(cellW - 150, CGRectGetMaxY(_descriptionFrame) + 14, 150, 11);
_cellHeight = CGRectGetMaxY(_timeAndDisFrame) + 9;
}
複製程式碼

提前計算後各個控制元件的frame並把cell的高度提前快取起來等到呼叫-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath時直接return _cellHeight,沒有任何計算量,從而減輕CPU的負載。 假如想進一步優化,可以嘗試呼叫控制元件的view.layer.displaysAsynchronously屬性為YES。 之前我們呼叫了dataSource和delegate的兩個關於row和height的方法,接下來tableView會呼叫- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 這個返回建立並返回特定row的cell。在這個方法內部,我們對cell裡的每個控制元件進行賦值並計算各個控制元件的佈局。只需要把各個控制元件的frame指向之前已經計算好的各個modelFrame就可以了,不需要進行多餘的佈局計算,然後對每個控制元件的內容進行一一賦值,這樣子就能在呼叫cellForRowAtIndexPath這個方法的時候迅速的返回一個cell。

#####如何避免出現GPU滿載CPU低負載呢? 1.當多個檢視重疊時,GPU會對其進行合成渲染,而渲染最慢的操作之一是混合,因此當檢視結構太複雜就會消耗大量GPU資源,所以當一個控制元件本身是不透明的,注意設定opaque = YES,這樣子可以避免無用的alpha通道合成,降低GPU負載。 2.對控制元件設定cornerRadius後對其進行clip或mask操作時,會導致offscreen rendering,而這個是在GPU中進行的,所以快速滑動tableView時,假如圓角物件較多,會導致GPU負載大增。這時候我們可以設定layer的shouldRasterize屬性為YES,可以將負載轉移給CPU。更為徹底的做法是直接在後臺繪製圓角圖片然後輸出到主執行緒顯示,避免使用圓角、陰影、遮罩等屬性。(這種最徹底的做法我沒試過) 3.將GPU的部分渲染轉接給CPU,那麼如何轉接呢?我們可以在單個控制元件中過載drawRect:方法,直接將文字和圖片繪製然後輸出到主執行緒上。

-(void)drawRect:(CGRect)rect {
UIImage *image = [UIImage imageNamed:@"logo"];
[image drawInRect:CGRectMake(0, 0, 100, 100)];
NSString *str = @"123 1234 12345 123456";
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextAddRect(ctx, CGRectMake(0, 0, 100, 100));
CGContextStrokePath(ctx);
//這裡會增加 100 *100 * 4 = 40kb大小的記憶體
[str drawInRect:CGRectMake(0, 0, 100, 100) withAttributes:nil];
}
複製程式碼

drawRect.png

當然你也可以設定Dictionary給Attributes賦值達到自己想要的文字效果。這段程式碼禁用了一些混合操作,減輕了GPU的負擔,從而使UITableView滑動更加流暢。

總之,效能優化要注意平衡CPU和GPU的負載。

相關文章: 從 Auto Layout 的佈局演算法談效能 iOS 保持介面流暢的技巧

相關文章