演示項目
演示項目下載地址:https://github.com/YYProgrammer/YYTableViewDemo
項目里的低性能版是常規(guī)寫法實現(xiàn)的tableview稽揭,高性能版是做了相關優(yōu)化后的tableview菜循。
tableView滑動為什么會卡?
我們可以想象這樣一個場景:
有一個老師默辨、學生A、學生B劫窒、一個畫板、一個櫥窗纱耻。
每一秒鐘波岛,老師都要告訴學生A一個題目讓他們作畫茅坛,學生A負責研究這個題目表達的含義,然后告訴學生B應該畫什么盆色,學生B收到消息后灰蛙,在畫板上畫出對應的畫祟剔,在這一秒鐘結束之時隔躲,把畫貼到櫥窗,供外面的人觀看物延。然后繼續(xù)下一秒的審題宣旱、畫畫的步驟。
正常情況下叛薯,學生A浑吟、B都能合同愉快,在規(guī)定的時間畫好耗溜,但有時候组力,學生A審題太久,或者這一秒的量太多抖拴,學生B畫得不夠快燎字,那么這一秒腥椒,甚至下幾秒,櫥窗里的畫會保持上一次的畫候衍,直到他們畫好下一張笼蛛。
這里,
學生A就是CPU蛉鹿,負責視圖相關的計算工作并告知GPU應該怎么繪圖滨砍;
學生B就是GPU,進行圖形的繪制妖异、渲染等工作惋戏;
“每一秒鐘”就是屏幕刷新周期,通常是1/60秒随闺,即每秒屏幕刷新60次日川;
櫥窗就是手機屏幕,用來顯示GPU繪制好的內(nèi)容矩乐;
“畫得不夠快龄句,導致櫥窗的畫在接下來的幾秒里一直是上一次的畫”的情況,就是掉幀散罕,就是卡的原因分歇。
可以看出,不論是CPU欧漱,還是GPU的壓力過大职抡,都會在一個周期內(nèi)完不成工作,都會導致掉幀的情況發(fā)生误甚。
而在tableview滑動時缚甩,會頻繁出現(xiàn)對象創(chuàng)建、屬性修改窑邦、布局計算擅威、文本繪制、圖形生成等消耗資源的操作發(fā)生冈钦。
所以優(yōu)化郊丛,就是想辦法在這一秒的時間里,減輕它們的負荷瞧筛,保證每一次都能“把畫兒畫完”厉熟。
優(yōu)化的思路
首先我們來看看下面這個tableview的流程:
獲取數(shù)據(jù);
把數(shù)據(jù)轉化成model较幌、存進數(shù)組揍瑟;
tableview調(diào)用reloadData刷新數(shù)據(jù);
在代理方法cellForRowAtIndexPath里乍炉,創(chuàng)建自定義的cell绢片,把model賦值給cell嘁字;
cell在對應的model的set方法里,根據(jù)拿到的model杉畜,設置圖片的image纪蜒,設置label的text等(控件都以懶加載形式初始化);
在代理方法heightForRowAtIndexPath里此叠,根據(jù)model纯续,算出當前行應該顯示多少的高度;
在cell的layoutSubviews方法里灭袁,布局子控件猬错。
1、避免主線程阻塞
1/2步里的獲取數(shù)據(jù)茸歧、數(shù)據(jù)處理等耗時操作倦炒,應該放入后臺線程異步處理,處理好后再通知主線程刷新界面软瞎。
常用的網(wǎng)絡請求框架都是在后臺線程完成的數(shù)據(jù)請求逢唤,但有時我們會忘了,在這些請求的回調(diào)里操作數(shù)據(jù)時涤浇,是在主線程里進行的操作鳖藕,需要我們手動管理線程。
例如:AFNetworking使用時
[[AFHTTPSessionManager manager] POST:@"" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//移到異步線程做
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//1只锭、字典轉模型
//2著恩、計算每個model的數(shù)據(jù),布局參數(shù)等蜻展。
dispatch_async(dispatch_get_main_queue(), ^{
//3喉誊、回到主線程,刷新tableview等
});
});
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
總之是能在異步操作的纵顾,都異步操作伍茄。
通常來說,UIKit和CoreAnimation相關操作必須在主線程中進行片挂,其它的可以在后臺線程異步執(zhí)行幻林。比方說圖像的異步繪制等贞盯,具體的后面介紹音念。
2、避免頻繁的對象創(chuàng)建
對象的創(chuàng)建會發(fā)送內(nèi)存分配躏敢、屬性調(diào)整等闷愤。
所以,首先件余,盡量用輕量的對象代替重量的對象讥脐。比如CALayer代替UIView遭居。
接著,多利用緩存思想旬渠,對象創(chuàng)建后緩存起來俱萍,需要的時候再拿出來用。合理利用內(nèi)存開銷告丢,減少CPU開銷枪蘑。
關于這一點,系統(tǒng)已經(jīng)提供了很好的api來做cell的緩存
[tableView dequeueReusableCellWithIdentifier:ID];
但我們有時會忘了這樣一種情況:如圖岖免,這個label顯示的內(nèi)容由model的兩個參數(shù)(時間岳颇、公里數(shù))拼接而成,我們習慣在cell里model的set方法中這樣賦值
//時間
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
formatter.dateStyle = NSDateFormatterMediumStyle;
formatter.timeStyle = NSDateFormatterShortStyle;
[formatter setDateFormat:@"yyyy年MM月"];
NSDate* date = [NSDate dateWithTimeIntervalSince1970:[model.licenseTime intValue]];
NSString* licenseTimeString = [formatter stringFromDate:date];
//公里數(shù)
NSString *travelMileageString = (model.travelMileage != nil && ![model.travelMileage isEqualToString:@""]) ? [NSString stringWithFormat:@"%@萬公里",model.travelMileage] : @"里程暫無";
//賦值給label.text
self.carDescribeLabel.text = [NSString stringWithFormat:@"%@ / %@",licenseTimeString,travelMileageString];
在tableview滾動的過程中颅湘,這些對象就會被來回的創(chuàng)建话侧,并且這個計算過程是在主線程里被執(zhí)行的。
我們可以把這些操作闯参,移到第2步(字典轉模型)來做瞻鹏,計算好這個label需要顯示的內(nèi)容,作為屬性存進model中鹿寨,需要的時候直接用乙漓。
這樣,既可以避免主線程的阻塞释移,又可以避免對象的頻繁創(chuàng)建叭披。
而下面這個例子也是緩存思想的體現(xiàn):
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 15.0 + 80.0 + 15.0;
}
修改為
static float ROW_HEIGHT = 15.0 + 80.0 + 15.0;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return ROW_HEIGHT;
}
當然這不是減少對象的創(chuàng)建,而是減少了計算的次數(shù)玩讳,減少了頻繁調(diào)用方法里的邏輯涩蜘,從而達到更快的速度。
3熏纯、減少對象的屬性賦值操作
尤其是UIView的frame/bounds等屬性的賦值操作同诫,會產(chǎn)生比較大的CPU消耗。
對象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方樟澜。這里特別說一下 CALayer:CALayer 內(nèi)部并沒有屬性误窖,當調(diào)用屬性方法時,它內(nèi)部是通過運行時 resolveInstanceMethod 為對象臨時添加一個方法秩贰,并把對應屬性值保存到內(nèi)部的一個 Dictionary 里霹俺,同時還會通知 delegate、創(chuàng)建動畫等等毒费,非常消耗資源丙唧。UIView 的關于顯示相關的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調(diào)整時觅玻,消耗的資源要遠大于一般的屬性想际。對此你在應用中培漏,應該盡量減少不必要的屬性修改。
——摘自iOS 保持界面流暢的技巧
所以在cell的layoutSubviews里布局所有子控件對性能是有影響的胡本,對于frame固定的UIView牌柄,在cell創(chuàng)建時(或者懶加載方法里)布局一次即可。
另外侧甫,有時候一個tableview的cell的樣式存在頻繁的變化但又有一定的規(guī)律(比方說有一個label的高度總是在兩行友鼻、一行來回變化),這就免不了會頻繁的設置它的高度闺骚。如果追求很高的性能彩扔,可以篩分成兩個cell,從而避免頻繁的更改frame僻爽。
4虫碉、異步繪制
文本渲染、圖像繪制都是比較消耗性能的操作胸梆,而UILabel等控件都是在主線程進行的文本繪制敦捧。這會對性能產(chǎn)生比較大的影響。
UIKit和CoreAnimation相關操作必須在主線程中進行碰镜,其它的可以在后臺線程異步執(zhí)行
怎么來簡單理解這句話呢兢卵?
比方說:為一個UIImageView設置image,
imageView.image = image;
以上代碼必須在主線程進行绪颖,但這個image的繪制過程秽荤,可以在異步線程做
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// 吧啦吧啦繪圖
CGImageRef imgRef = CGBitmapContextCreateImage(ctx);//位圖
UIImage *image = [UIImage imageWithCGImage:imgRef];//轉成UIImage
dispatch_async(dispatch_get_main_queue(), ^{
//回到主線程
imageView.image = image;//設置imageView的image
});
});
所以異步繪制的思想,就是盡量把需要顯示的內(nèi)容柠横,在異步線程繪制窃款,繪制好后再通知主線程顯示。
在這個項目里VVeboTableViewDemo牍氛,作者把cell里很多需要顯示的內(nèi)容都異步繪制成圖片再顯示晨继,并實現(xiàn)了一個異步繪制的Label,是異步繪制思想一個很好的例子搬俊。
的確紊扬,優(yōu)化性能會犧牲一些開發(fā)速度,那么如何相對高效的利用異步繪制技術呢唉擂?
推薦使用YYKit的相關組件餐屎,例如YYLabel。
YYLabel是一個可以異步繪制的用來顯示文字的控件楔敌,它可以像UILabel一模一樣的使用啤挎,也可以通過賦值它的textLayout(一個YYTextLayout對象)來顯示內(nèi)容驻谆,第二種方式擁有更高的性能卵凑。
舉個例子庆聘,一般來說我們是這樣來顯示一段文字的
/** cell的.m文件 */
//懶加載一個UILabel
- (UILabel *)carVersionLabel
{
if (!_carVersionLabel)
{
_carVersionLabel = [[UILabel alloc] init];
[self.contentView addSubview:_carVersionLabel];
_carVersionLabel.backgroundColor = self.contentView.backgroundColor;
_carVersionLabel.font = [UIFont fontWithName:MAIN_CELL_TITLE_FONT_NAME size:15];
_carVersionLabel.textColor = BLACK_TEXT_COLOR;
_carVersionLabel.numberOfLines = 0;
_carVersionLabel.textAlignment = NSTextAlignmentLeft;
}
return _carVersionLabel;
}
//model的set方法
- (void)setModel:(YYLowPerCarModel *)model
{
_model = model;
self.carVersionLabel.text = model.carName;
}
用YYLabel來重構的話,
/** model的.h文件 */
//聲明YYTextLayout對象
@property (nonatomic,strong) YYTextLayout *carVersionLabelLayout;//車型Label的layout
/** model的.m文件 */
//這個方法在數(shù)據(jù)請求的方法里調(diào)用勺卢,字典轉model完成后伙判,調(diào)用這個方法來計算一些布局用的參數(shù)
- (void)setupViewModel
{
//車型布局參數(shù)
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:self.carName];
text.color = BLACK_TEXT_COLOR;
text.font = CAR_VERSION_LABEL_FONT;
text.lineSpacing = -4;
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(CAR_VERSION_LABEL_WIDTH, MAXFLOAT)];
self.carVersionLabelLayout = [YYTextLayout layoutWithContainer:container text:text];
}
/** cell的.m文件 */
//懶加載Label
- (YYLabel *)carVersionLabel
{
if (!_carVersionLabel)
{
_carVersionLabel = [[YYLabel alloc] init];
[self.contentView addSubview:_carVersionLabel];
_carVersionLabel.displaysAsynchronously = YES;//是否異步繪制
_carVersionLabel.ignoreCommonProperties = YES;//通過設置textLayout來布局時,設置這個參數(shù)為YES可以獲得更高的性能
_carVersionLabel.fadeOnHighlight = NO;//高亮漸變效果
_carVersionLabel.fadeOnAsynchronouslyDisplay = NO;//異步繪制漸變效果
}
return _carVersionLabel;
}
//model的set方法
- (void)setModel:(YYLowPerCarModel *)model
{
_model = model;
self.carVersionLabel.textLayout = model.carVersionLabelLayout;//設置layout黑忱,異步繪制
}
如果cell里的label都用YYLabel來實現(xiàn)的話宴抚,性能會得到顯著的提升。
關于YYLabel或者YYkit相關組件的使用甫煞,還需要多實踐踩坑菇曲、看博客、看YYKit的demo抚吠,感謝巨人的肩膀常潮。
5、簡化視圖結構
GPU在繪制圖像前楷力,會把重疊的視圖進行混合喊式,視圖結構越復雜,這個操作就越耗時萧朝,如果存在透明視圖岔留,混合過程會更加復雜。
所以检柬,我們可以
盡量避免復雜的圖層結構
少使用透明的視圖
不透明的視圖献联,設置opaque = YES
或者采用VVeboTableViewDemo的方法,把視圖異步繪成一張圖
6何址、減少離屏渲染
- 什么是離屏渲染酱固?
回到文章開頭的那個例子,同學B在畫板上畫畫头朱,這個畫板运悲,叫做屏幕緩沖區(qū),一般的情況项钮,GPU的渲染操作是在當前用于顯示的屏幕緩沖區(qū)中進行班眯,這個叫做當前屏幕渲染(On-Screen Rendering),而由于某些特定條件烁巫,GPU在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作署隘,就是離屏渲染(Off-Screen Rendering)
- 離屏渲染為什么耗性能?
- 創(chuàng)建新緩沖區(qū)
要想進行離屏渲染亚隙,首先要創(chuàng)建一個新的緩沖區(qū)磁餐。
- 上下文切換
離屏渲染的整個過程,需要多次切換上下文環(huán)境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后诊霹,將離屏緩沖區(qū)的渲染結果顯示到屏幕上有需要將上下文環(huán)境從離屏切換到當前屏幕羞延。而上下文環(huán)境的切換是要付出很大代價的。
——摘自iOS 事件處理機制與圖像渲染過程
- 離屏渲染觸發(fā)條件
--shouldRasterize(光柵化)
--masks(遮罩)
--shadows(陰影)
--edge antialiasing(抗鋸齒)
--group opacity(不透明)
--復雜形狀設置圓角等
--漸變
- 怎么查看哪些控件發(fā)生了離屏渲染?
利用Xcode自帶的Instruments工具來觀察脾还。
然后觀察手機屏幕伴箩,黃色標識的地方,就發(fā)生了離屏渲染鄙漏。
- 老生常談之圓角問題
圓角是開發(fā)中經(jīng)常使用到的美化方式嗤谚,但一般的設置cornerRadius時會配合masksToBounds屬性,這就會造成離屏渲染怔蚌。
關于這種問題的處理巩步,大致有兩個思路
1、異步繪制一張圓角的圖片來顯示桦踊;
2渗钉、用一個圓角而中空的圖來蓋住。
演示項目里我選擇了使用YYKit里的組件來切割圖片的圓角钞钙。
其它小tips
- 1鳄橘、tableview需要刷新數(shù)據(jù)時,使用
[tableview beginUpdates];
[tableview insertRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationNone];
[tableview endUpdates];
而非
[tableview reloadData];
主要原因在于:
- 1芒炼、刷新更少的行瘫怜,減少cpu壓力;
- 2本刽、使用YYLabel等異步繪制label時鲸湃,使用reloadData會把之前的row也重繪一次,會造成“Label閃了一下的感覺”子寓。
2暗挑、NSDateFormatter這個對象的相關操作很費時,需要避免頻繁的創(chuàng)建和計算
3斜友、對于固定行高的cell
tableview.rowHeight = 50.0;
比
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
效率更高
- 4炸裆、Autolayout使用在越復雜的界面,CPU越吃力