性能優(yōu)化-UITableview深度優(yōu)化

演示項目

演示項目下載地址: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的流程:

  1. 獲取數(shù)據(jù);

  2. 把數(shù)據(jù)轉化成model较幌、存進數(shù)組揍瑟;

  3. tableview調(diào)用reloadData刷新數(shù)據(jù);

  4. 在代理方法cellForRowAtIndexPath里乍炉,創(chuàng)建自定義的cell绢片,把model賦值給cell嘁字;

  5. cell在對應的model的set方法里,根據(jù)拿到的model杉畜,設置圖片的image纪蜒,設置label的text等(控件都以懶加載形式初始化);

  6. 在代理方法heightForRowAtIndexPath里此叠,根據(jù)model纯续,算出當前行應該顯示多少的高度;

  7. 在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工具來觀察脾还。


打開Instruments

選擇app并開始監(jiān)測

選擇圖中的選項

然后觀察手機屏幕伴箩,黃色標識的地方,就發(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越吃力
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲜屏,一起剝皮案震驚了整個濱河市烹看,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洛史,老刑警劉巖惯殊,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異也殖,居然都是意外死亡土思,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來己儒,“玉大人崎岂,你說我怎么就攤上這事≈吩福” “怎么了该镣?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵冻璃,是天一觀的道長响谓。 經(jīng)常有香客問我,道長省艳,這世上最難降的妖魔是什么娘纷? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮跋炕,結果婚禮上赖晶,老公的妹妹穿的比我還像新娘。我一直安慰自己辐烂,他們只是感情好遏插,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纠修,像睡著了一般胳嘲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扣草,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天了牛,我揣著相機與錄音,去河邊找鬼辰妙。 笑死鹰祸,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的密浑。 我是一名探鬼主播蛙婴,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尔破!你這毒婦竟也來了敬锐?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤呆瞻,失蹤者是張志新(化名)和其女友劉穎台夺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痴脾,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡颤介,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滚朵。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡冤灾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出辕近,到底是詐尸還是另有隱情韵吨,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布移宅,位于F島的核電站归粉,受9級特大地震影響,放射性物質發(fā)生泄漏漏峰。R本人自食惡果不足惜糠悼,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浅乔。 院中可真熱鬧倔喂,春花似錦、人聲如沸靖苇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贤壁。三九已至悼枢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工醋安, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人双揪。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像包帚,于是被迫代替她去往敵國和親渔期。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容