iOS底層原理 - 性能優(yōu)化 之 卡頓優(yōu)化

面試題引發(fā)的思考:

Q: 列表卡頓的原因?如何優(yōu)化?

  • 卡頓主要是因?yàn)樵?主線程 執(zhí)行了 比較耗時(shí) 的操作。

Q: 卡頓解決方法?

  • CPU:
    • 盡量用輕量級(jí)的對(duì)象:CALayerint等;
    • 不要頻繁調(diào)用UIView的相關(guān)屬性;
    • 提前計(jì)算好布局;
    • Autolayout會(huì)比直接設(shè)置frame消耗更多的CPU資源;
    • 圖片的size最好剛好跟UIImageViewsize保持一致;
    • 控制一下線程的 最大并發(fā)數(shù)量;
    • 盡量把 耗時(shí)的操作 放到 子線程,充分利用CPU的多核。
  • GPU:
    • 盡量避免短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進(jìn)行顯示;
    • GPU能處理的最大紋理尺寸是4096x4096;
    • 盡量減少視圖數(shù)量和層級(jí);
    • 減少透明的視圖(alpha<1),不透明的就設(shè)置opaqueYES;
    • 盡量避免出現(xiàn)離屏渲染。

Q: 如何檢測(cè)卡頓?

  • 通過添加Observer到 主線程RunLoop 中,監(jiān)聽 RunLoop狀態(tài)切換 的耗時(shí),以達(dá)到監(jiān)控卡頓的目的。

Q: 何為離屏渲染?

  • 當(dāng)前屏幕渲染:GPU在 當(dāng)前屏幕緩沖區(qū) 進(jìn)行渲染操作;
  • 離屏渲染:GPU在 當(dāng)前屏幕緩沖區(qū)以外 新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。

Q: 離屏渲染觸發(fā)時(shí)機(jī)?

  • 圖層屬性的混合體 在沒有預(yù)合成之前 不能直接在屏幕中繪制,此時(shí)會(huì)觸發(fā)離屏渲染。

Q: 為什么離屏渲染消耗性能?為何要避免離屏渲染?

  • 創(chuàng)建新的緩沖區(qū);
  • 多次切換上下文切換。
  • 觸發(fā)離屏渲染時(shí),會(huì)增加GPU的工作量,可能會(huì)導(dǎo)致CPU和GPU的總工作耗時(shí)超出16.7ms,進(jìn)而導(dǎo)致UI的卡頓和掉幀。

Q: 哪些操作會(huì)觸發(fā)離屏渲染?

  • 圓角:cornerRadiusmasksToBounds同時(shí)設(shè)置時(shí);
    解決方案:UI提供圓角圖片、CoreGraphics繪制裁剪圓角;
  • 陰影:layer.shadowXXX;
    如果設(shè)置了layer.shadowPath就不會(huì)產(chǎn)生離屏渲染。
  • 光柵化:layer.shouldRasterize = YES;
  • 圖層蒙版:layer.mask。

1. 屏幕成像原理

(1) CPU和GPU的作用

在屏幕成像過程中,CPU和GPU起著至關(guān)重要的作用:

  • CPU (Central Processing Unit,中央處理器):
    作用:對(duì)象的創(chuàng)建和銷毀、對(duì)象屬性的調(diào)整、布局計(jì)算、文本的計(jì)算和排版、圖片的格式轉(zhuǎn)換和解碼、圖像的繪制(Core Graphics);
  • GPU (Graphics Processing Unit,圖形處理器):
    作用:紋理的渲染。

(2) 屏幕成像流程

屏幕成像流程如下:

屏幕成像流程
  • CPU進(jìn)行UI布局、文本計(jì)算、圖片解碼等等,計(jì)算完畢將數(shù)據(jù)提交給GPU;
  • GPU對(duì)數(shù)據(jù)進(jìn)行渲染,渲染完畢將數(shù)據(jù)放到 幀緩存 里面;
  • 視頻控制器幀緩存 讀取數(shù)據(jù);并將數(shù)據(jù)顯示到 屏幕 上。

在iOS中是雙緩沖機(jī)制,有前幀緩存、后幀緩存。上圖的幀緩存有兩塊區(qū)域,當(dāng)一塊區(qū)域滿了或一塊區(qū)域正在進(jìn)行其他操作,此時(shí)GPU會(huì)使用另外一塊區(qū)域緩存,提升效率。


(3) 屏幕成像原理

屏幕成像原理如下:

屏幕成像原理

手機(jī)屏幕上的動(dòng)畫是通過一幀一幀(或者說一頁)數(shù)據(jù)組成的。

  • 當(dāng)屏幕需要顯示一幀數(shù)據(jù)的時(shí)候,會(huì)發(fā)送一個(gè)垂直同步信號(hào);然后逐行發(fā)送水平同步信號(hào),直到填充整個(gè)屏幕,此時(shí)一幀數(shù)據(jù)就顯示完成。
  • 接下來再發(fā)送一個(gè)垂直同步信號(hào);然后逐行發(fā)送水平同步信號(hào),直到完成這一幀。
  • 當(dāng)所有幀數(shù)據(jù)發(fā)送完成之后,這些幀連起來就是屏幕上的動(dòng)畫了。

2. 卡頓產(chǎn)生原因

屏幕成像流程

如上圖,紅色箭頭是CPU計(jì)算需要的時(shí)間,藍(lán)色箭頭是GPU渲染需要的時(shí)間。

注意:發(fā)送一個(gè)垂直同步信號(hào),就會(huì)立即把CPU計(jì)算和GPU渲染完成的數(shù)據(jù)從幀緩存中讀取顯示到屏幕上,并且立即開始下一幀的操作。

  • 第一幀的數(shù)據(jù)需要顯示,發(fā)送一個(gè)垂直同步信號(hào),此時(shí)將CPU計(jì)算和GPU渲染完成的數(shù)據(jù)從幀緩存中讀取顯示到屏幕上,就完成了第一幀的顯示。

  • 第二幀的數(shù)據(jù)CPU計(jì)算和GPU渲染的比較快,在下一次垂直同步信號(hào)來之后,將CPU計(jì)算和GPU渲染完成的數(shù)據(jù)從幀緩存中讀取顯示到屏幕上,完成了第二幀的顯示。

  • 第三幀的數(shù)據(jù)CPU計(jì)算和GPU渲染的比較慢,在下一次垂直同步信號(hào)來到之后還沒處理完畢,此時(shí)幀緩存里面還是第二幀的數(shù)據(jù),將第二幀的數(shù)據(jù)從幀緩存中讀取顯示到屏幕上,如此便產(chǎn)生掉幀現(xiàn)象,也就是我們說的卡頓

  • 第三幀的數(shù)據(jù)會(huì)在下一幀的垂直同步信號(hào)來到之后再顯示到屏幕上,慢了一幀的時(shí)間。


3. 卡頓解決方法

卡頓解決方法是盡可能減少CPU、GPU資源消耗。

  • 一般幀率FPS(Frames Per Second每秒傳輸幀數(shù))達(dá)到60FPS就會(huì)感覺不到卡頓;
  • 按照60FPS的刷幀率,每隔16ms就會(huì)有一次VSync信號(hào)(1000ms / 60 = 16.667ms)。

(1) 卡頓優(yōu)化 CPU

1> CPU卡頓優(yōu)化方式

  • 盡量用輕量級(jí)的對(duì)象,比如無需事件處理的地方使用CALayer取代UIView、能用int就不用NSNumber

  • 不要頻繁地調(diào)用UIView的相關(guān)屬性,比如frame、bounds、transform等屬性,盡量減少不必要的修改;

  • 盡量提前計(jì)算好布局,在有需要時(shí)一次性調(diào)整對(duì)應(yīng)的屬性,不要多次修改屬性;

  • Autolayout會(huì)比直接設(shè)置frame消耗更多的CPU資源,因?yàn)?code>Autolayout本身性能就不是很高;

  • 圖片的size最好剛好跟UIImageViewsize保持一致,如果不一致CPU就會(huì)對(duì)圖片進(jìn)行伸縮操作,這樣比較消耗CPU資源;

  • 控制一下線程的最大并發(fā)數(shù)量,不要無限制的并發(fā),這樣比較消耗CPU資源;

  • 盡量把耗時(shí)的操作放到子線程,充分利用CPU的多核。

2> 耗時(shí)操作:文本處理(尺寸計(jì)算、繪制)

比如:boundingRectWithSize計(jì)算文字寬高是可以放到子線程去計(jì)算的,或者drawWithRect文本繪制,也是可以放到子線程去繪制的,如下:

- (void)text {
    // 此類操作可以放到子線程
    // 文字計(jì)算
    [@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) 
    options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];

    // 文字繪制
    [@"text" drawWithRect:CGRectMake(0, 0, 100, 100) 
    options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
}

3> 耗時(shí)操作:圖片處理(解碼、繪制)

我們經(jīng)常會(huì)寫如下代碼加載圖片:

- (void)image {
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(100, 100, 100, 56);
    //加載圖片
    imageView.image = [UIImage imageNamed:@"timg"]; 
    [self.view addSubview:imageView];
    self.imageView = imageView;
}

通過imageNamed加載圖片,加載完成后是不會(huì)直接顯示到屏幕上面的,因?yàn)榧虞d后的是經(jīng)過壓縮的圖片二進(jìn)制,當(dāng)真正想要渲染到屏幕上的時(shí)候再拿到圖片二進(jìn)制解碼成屏幕顯示所需要的格式,然后進(jìn)行渲染顯示,而這種解碼一般默認(rèn)是在主線程操作的,如果圖片數(shù)據(jù)比較多比較大的話也會(huì)產(chǎn)生卡頓。

一般的做法是在子線程提前解碼圖片二進(jìn)制,不需要主線程解碼,這樣在圖片渲染顯示之前就已經(jīng)解碼出來了,主線程拿到解碼后的數(shù)據(jù)進(jìn)行渲染顯示就可以了,這樣主線程就不會(huì)卡頓。網(wǎng)上很多圖片處理框架都有這個(gè)異步解碼功能的。下面演示一下:

圖片異步解碼

上面代碼,不單單通過imageNamed加載的本地圖片可以提前渲染,通過imageWithContentsOfFile加載的網(wǎng)絡(luò)圖片也可以這樣進(jìn)行提前渲染,只要獲取到UIImage對(duì)象都可以對(duì)UIImage對(duì)象進(jìn)行提前渲染。


(2) 卡頓優(yōu)化 GPU

1> GPU卡頓優(yōu)化方式

  • 盡量避免短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進(jìn)行顯示,這樣只渲染一張圖片,渲染更快;

  • GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個(gè)尺寸,就會(huì)占用CPU資源進(jìn)行處理,所以紋理盡量不要超過這個(gè)尺寸;

  • 盡量減少視圖數(shù)量和層級(jí),視圖層級(jí)太多會(huì)增加渲染時(shí)間;

  • 減少透明的視圖(alpha<1),不透明的就設(shè)置opaqueYES,因?yàn)橐坏┯型该鞯囊晥D就會(huì)進(jìn)行很多混合計(jì)算增加渲染的資源消耗;

  • 盡量避免出現(xiàn)離屏渲染。

2> 離屏渲染

在OpenGL中,GPU有2種渲染方式:

  • On-Screen Rendering:當(dāng)前屏幕渲染,在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行渲染操作;
  • Off-Screen Rendering:離屏渲染,在當(dāng)前屏幕緩沖區(qū)以外新開辟一個(gè)緩沖區(qū)進(jìn)行渲染操作。

當(dāng)前用于顯示的屏幕緩沖區(qū)就是下圖的幀緩存:

屏幕成像流程

Q: 離屏渲染觸發(fā)時(shí)機(jī)?

  • 圖層屬性的混合體 在沒有預(yù)合成之前 不能直接在屏幕中繪制時(shí),會(huì)觸發(fā)離屏渲染。

Q: 為什么離屏渲染消耗性能?為何要避免離屏渲染

  • 創(chuàng)建新的緩沖區(qū);
  • 多次切換上下文切換。
  • 觸發(fā)離屏渲染時(shí),會(huì)增加GPU的工作量,可能會(huì)導(dǎo)致CPU和GPU的總工作耗時(shí)超出16.7ms,進(jìn)而導(dǎo)致UI的卡頓和掉幀。

Q: 哪些操作會(huì)觸發(fā)離屏渲染?

  • 圓角:cornerRadiusmasksToBounds同時(shí)設(shè)置時(shí);
    解決方案:UI提供圓角圖片、CoreGraphics繪制裁剪圓角;
  • 陰影:layer.shadowXXX
    如果設(shè)置了layer.shadowPath就不會(huì)產(chǎn)生離屏渲染。
  • 光柵化:layer.shouldRasterize = YES
  • 圖層蒙版:layer.mask

Q: 為什么要開辟新的緩沖區(qū)?

因?yàn)樯厦孢M(jìn)行的那些操作比較耗性能、資源,當(dāng)前屏幕緩沖區(qū)不夠用(就算是雙緩沖機(jī)制也不夠用),所以才會(huì)開辟新的緩沖區(qū)。


(3) 卡頓檢測(cè)

“卡頓”主要是因?yàn)樵谥骶€程執(zhí)行了比較耗時(shí)的操作;

通過添加Observer到主線程RunLoop中,監(jiān)聽RunLoop狀態(tài)切換的耗時(shí),以達(dá)到監(jiān)控卡頓的目的。

如下圖,主線程的大部分操作,比如點(diǎn)擊事件的處理,view的計(jì)算、 繪制等基本上都在source0source1。我們只要監(jiān)控一下從結(jié)束休眠(步驟08)處理soure1(步驟09-C)一直到繞回來處理source0(步驟05), 如果發(fā)現(xiàn)中間消耗的時(shí)間比較長(zhǎng),那么就可以證明這些操作比較耗時(shí)。

RunLoop運(yùn)行邏輯

借助可以監(jiān)控哪個(gè)方法卡頓的第三方庫<LXDAppFluecyMonitor>,進(jìn)行檢測(cè):

// TODO: ----------------- ViewController類 -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    //開啟卡頓檢測(cè)
    [[LXDAppFluecyMonitor sharedMonitor] startMonitoring];
    [self.tableView registerClass: [UITableViewCell class] forCellReuseIdentifier: @"cell"];
}

#pragma mark -
#pragma mark - UITableViewDataSource
- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)section {
    return 1000;
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: @"cell"];
    cell.textLabel.text = [NSString stringWithFormat: @"%lu", indexPath.row];

    if (indexPath.row > 0 && indexPath.row % 30 == 0) {
        // 模擬卡頓
        sleep(2.0);
    }
    return cell;
}

運(yùn)行以上代碼,打印結(jié)果如下:

打印結(jié)果

由打印結(jié)果可知:可以檢測(cè)到cellForRowAtIndexPath的卡頓。

下面簡(jiǎn)單介紹下LXDAppFluecyMonitor框架的核心代碼:

LXDAppFluecyMonitor框架里面就兩個(gè)文件:
LXDBacktraceLogger文件里面是關(guān)于方法調(diào)用棧的一些代碼;
LXDAppFluecyMonitor文件就是卡頓檢測(cè)文件。

進(jìn)入LXDAppFluecyMonitor文件的startMonitoring方法:

startMonitoring方法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

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