面試題引發(fā)的思考:
Q: 列表卡頓的原因?如何優(yōu)化?
- 卡頓主要是因?yàn)樵?主線程 執(zhí)行了 比較耗時(shí) 的操作。
Q: 卡頓解決方法?
-
CPU:
- 盡量用輕量級(jí)的對(duì)象:
CALayer
、int
等; - 不要頻繁調(diào)用
UIView
的相關(guān)屬性; - 提前計(jì)算好布局;
Autolayout
會(huì)比直接設(shè)置frame
消耗更多的CPU資源;- 圖片的
size
最好剛好跟UIImageView
的size
保持一致; - 控制一下線程的 最大并發(fā)數(shù)量;
- 盡量把 耗時(shí)的操作 放到 子線程,充分利用CPU的多核。
- 盡量用輕量級(jí)的對(duì)象:
-
GPU:
- 盡量避免短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進(jìn)行顯示;
- GPU能處理的最大紋理尺寸是4096x4096;
- 盡量減少視圖數(shù)量和層級(jí);
- 減少透明的視圖(
alpha<1
),不透明的就設(shè)置opaque
為YES
; - 盡量避免出現(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ā)離屏渲染?
-
圓角:
cornerRadius
和masksToBounds
同時(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
最好剛好跟UIImageView
的size
保持一致,如果不一致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è)置opaque
為YES
,因?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ā)離屏渲染?
-
圓角:
cornerRadius
和masksToBounds
同時(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ì)算、 繪制等基本上都在source0
和source1
。我們只要監(jiān)控一下從結(jié)束休眠(步驟08)處理soure1
(步驟09-C)一直到繞回來處理source0
(步驟05), 如果發(fā)現(xiàn)中間消耗的時(shí)間比較長(zhǎng),那么就可以證明這些操作比較耗時(shí)。
借助可以監(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é)果可知:可以檢測(cè)到cellForRowAtIndexPath
的卡頓。
下面簡(jiǎn)單介紹下LXDAppFluecyMonitor框架的核心代碼:
LXDAppFluecyMonitor框架里面就兩個(gè)文件:
LXDBacktraceLogger文件里面是關(guān)于方法調(diào)用棧的一些代碼;
LXDAppFluecyMonitor文件就是卡頓檢測(cè)文件。
進(jìn)入LXDAppFluecyMonitor文件的startMonitoring
方法: