TableView相信只要是做iOS開發的就不會陌生,目前大多數iOS的app都是采用TabBar+NavigationBar+TableViewController這一主流框架,既然用的這么頻繁,肯定就會在開發過程中碰到一些問題--比如屏幕掉幀、卡頓等現象。這些現象大幅度的降低了用戶的性能體驗,并提高了crash的頻率。因此如何能優化好tableView就非常考驗程序猿們的功底了。
本猿~啊呸,本人就在開發公司項目的時候遇到這類問題,當快速滑動tableView并且cell中有大量圖片和其他控件需要加載時,就會出現嚴重掉幀(我們公司的項目當時大量采用xib現在逐漸用手寫代碼代替),有時還會crash。由于當時項目比較趕進度,所以沒有時間去優化性能,這種情況直到功能基本完善為止,花了大量功夫進行性能優化。
接下來我會根據tableView的delegate以及dataSource方法的執行順序進行一步一步的講解。
首先當一個tableView需要顯示內容的時候,首先會發送網絡請求,向服務器請求數據,然后將數據轉為我們可以使用的model后進行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里進行計算各控件的相對位置(利用aotulayout和xib)比如:
-(void)setModel:(ImageCellModel *)model{? ? _model = model;self.detailsLabel.text= model.news;self.priceLabel.text= [NSStringstringWithFormat:@"¥%@",model.money];NSString*iconStr = model.User.headIcon;// self.portraitImgView.layer.cornerRadius = 15;//? self.portraitImgView.layer.masksToBounds = YES;if(![iconStr isEqualToString:@""]) {? ? ? ? [self.portraitImgViewsd_setImageWithURL:[NSURLURLWithString:iconStr]];? ? }else{self.portraitImgView.image= [UIImageimageNamed:@"defaultPortrait@2x.png"];? ? }self.nicknameLabel.text= model.User.nickName;if([model.User.rankisEqualToString:@"0"]) {self.rankImgView.image= [UIImageimageNamed:@""];? ? }}
但是這樣做假如滑動比較快,且內部控件比較復雜會導致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的負載。
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 = [selfmodelFramesWithModelAry:modelAry];dispatch_async(dispatch_get_main_queue(), ^{//這里面把計算好的frameModel返回給主線程并執行正常的操作});- (NSMutableArray*)modelFramesWithModelAry:(NSArray*)modelArray{NSMutableArray*frameModels = [NSMutableArrayarray];for(ImageCellModel *modelinmodelArray) {? ? ? ? HomeModelFrame *modelFrame = [[HomeModelFrame alloc] init];? ? ? ? modelFrame.model= model;? ? ? ? [frameModels addObject:modelFrame];? ? }returnframeModels;}
上面代碼會將請求到的JSON數組轉換為model數組,然后將model數組里的model轉換為modelFrame(就是根據model的各個屬性計算出frame):
#import@classImageCellModel;@interfaceHomeModelFrame:NSObject@property(nonatomic,strong)ImageCellModel *model;@property(nonatomic,assign)CGRectavatarFrame;@property(nonatomic,assign)CGRectnameFrame;@property(nonatomic,assign)CGRectpriceFrame;@property(nonatomic,assign)CGRectphotosFrame;@property(nonatomic,assign)CGRectlabelFrame;@property(nonatomic,assign)CGRectdescriptionFrame;@property(nonatomic,assign)CGRecttimeAndDisFrame;@property(nonatomic,assign)CGFloatcellHeight;@property(nonatomic,assign)CGRectdateAndOurLabelFrame;@end//在.m中-(void)setModel:(ImageCellModel *)model{? ? _model = model;CGFloatcellW = [UIScreenmainScreen].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 = [UIImageimageNamed:@"logo"];? ? [image drawInRect:CGRectMake(0,0,100,100)];NSString*str =@"123 1234 12345 123456";CGContextRefctx =UIGraphicsGetCurrentContext();CGContextAddRect(ctx,CGRectMake(0,0,100,100));CGContextStrokePath(ctx);? ? [str drawInRect:CGRectMake(0,0,100,100) withAttributes:nil];}
47C81F4F-5E36-4F96-A5F9-8D730A099DD8.png
當然你也可以設置Dictionary給Attributes賦值達到自己想要的文字效果。這段代碼禁用了一些混合操作,減輕了GPU的負擔,從而使UITableView滑動更加流暢。
總之,性能優化要注意平衡CPU和GPU的負載。
原文鏈接:http://www.lxweimin.com/p/8f3ed86e6480