前言
- 這一塊主要為了面試講項目準(zhǔn)備,盡量理論與實(shí)踐相結(jié)合吧,能把搜到的資料都實(shí)現(xiàn)最好
- 目前主要是為tableView的優(yōu)化進(jìn)行研究,把涉及到的異步繪制【圖層問題】,網(wǎng)絡(luò)請求中的cell中空間高度緩存這些就用到最好
FPS
- FPS是圖像領(lǐng)域中的定義,是指畫面每秒傳輸幀數(shù),通俗來講就是指動畫或視頻的畫面數(shù)。FPS是測量用于保存、顯示動態(tài)視頻的信息數(shù)量。每秒鐘幀數(shù)越多,所顯示的動作就會越流暢。通常,要避免動作不流暢的最低是30。
- iPhone的FPS為60,也就是說它在一秒鐘內(nèi)會刷新60次,每次間隔大概16.7ms,也就是說我們的iPhone每次有16.7ms的時間來處理事件
- 而如果在這個時間內(nèi)沒能成功刷新,就會直接丟棄這一幀,下一次一起整上去【也就是說,不會原地刷新】
- 這樣子幀的丟失反映到用到用戶體驗上就 = APP卡頓了
CPU
- 中央處理器(CPU,central processing unit)作為計算機(jī)系統(tǒng)的運(yùn)算和控制核心,是信息處理、程序運(yùn)行的最終執(zhí)行單元
- 放在我們的iOS中,可以理解為CPU負(fù)責(zé)軟件的操作,對象創(chuàng)建、對象調(diào)整、對象銷毀、布局計算、文本計算、文本渲染、圖片解碼、圖像的繪制等操作是放在CPU上執(zhí)行的
GPU
- 圖形處理器(英語:Graphics Processing Unit,縮寫:GPU),又稱顯示核心、視覺處理器、顯示芯片,是一種專門在個人電腦、工作站、游戲機(jī)和一些移動設(shè)備(如平板電腦、智能手機(jī)等)上做圖像和圖形相關(guān)運(yùn)算工作的微處理器。
- 放在iOS中,GPU處理的是硬件層面的事,紋理的渲染、視圖混合、離屏渲染都是在GPU上執(zhí)行的
位圖
- 位圖是由稱作像素(圖片元素)的單個點(diǎn)組成的。這些點(diǎn)可以進(jìn)行不同的排列和染色以構(gòu)成圖樣。當(dāng)放大位圖時,可以看見賴以構(gòu)成整個圖像的無數(shù)單個方塊。擴(kuò)大位圖尺寸的效果是增大單個像素,從而使線條和形狀顯得參差不齊。然而,如果從稍遠(yuǎn)的位置觀看它,位圖圖像的顏色和形狀又顯得是連續(xù)的。用數(shù)碼相機(jī)拍攝的照片、掃描儀掃描的圖片以及計算機(jī)截屏圖等都屬于位圖。位圖的特點(diǎn)是可以表現(xiàn)色彩的變化和顏色的細(xì)微過渡,產(chǎn)生逼真的效果,缺點(diǎn)是在保存時需要記錄每一個像素的位置和顏色值,占用較大的存儲空間。常用的位圖處理軟件有Photoshop(同時也包含矢量功能)、Painter和Windows系統(tǒng)自帶的畫圖工具等,Adobe Illustrator則是矢量圖軟件
像素點(diǎn)如何出現(xiàn)在屏幕上
- 先來看一張全部流程圖:

pixels-software-stack
- 這張圖從從右到左就是軟件到硬件,從App到硬件屏幕上出現(xiàn)界面的全過程
- 這里再介紹下前面沒介紹的模塊
渲染參與者
- GPU Driver:GPU驅(qū)動軟件,直接和 GPU 交流的代碼塊
- OpenGL:提供了 2D 和 3D 圖形渲染的 API,高GPU的能力,并實(shí)現(xiàn)硬件加速渲染,是第一個和圖形硬件(GPU)交流的標(biāo)準(zhǔn)化方式
- Core Graphics:Quartz 2D的一個高級繪圖引擎,常用與iOS,tvOS,macOS的圖形繪制應(yīng)用開發(fā)。Core Graphics是對底層C語言的一個簡單封裝,其中提供大量的低層次,輕量級的2D渲染API。前綴為CG,比如常見的CGPath,CGColor
- Core Animation:是蘋果提供的一套基于繪圖的動畫框架,但不止是動畫,他同樣是繪圖的根本【因為其前綴是CA,CALayer有多重要就不用多說了吧】

6379271-f8367726caa19e8a
- 從圖中可以看出,最底層是圖形硬件(GPU);上層是OpenGL和CoreGraphics,提供一些接口來訪問GPU;再上層的CoreAnimation在此基礎(chǔ)上封裝了一套動畫的API。最上面的UIKit屬于應(yīng)用層,處理與用戶的交互
- Core Image:iOS處理圖像的框架,主要用處可以給圖片添加濾鏡效果和圖像識別功能
we are A!R!G!B!
- 這里講下對于像素需要知道的知識:
- 屏幕上的像素是由紅,綠,藍(lán)三種顏色組件構(gòu)成的。因此,位圖數(shù)據(jù)有時也被叫做 RGB 數(shù)據(jù)
- ARGB:32bits-per-pixel(bpp), 8bits-per-componet(bpc),透明度會首先被乘以到像素值上【也就是說對于透明度的處理我們直接就是百分比乘到RGB值里面】
A R G B A R G B A R G B
| pixel 0 | pixel 1 | pixel 2
0 1 2 3 4 5 6 7 8 9 10 11 ...
這個格式經(jīng)常被叫做 ARGB。每個像素占用 4 字節(jié)(32bpp),每一個顏色組件是1字節(jié)(8bpc).每個像素有一個 alpha 值,這個值總是最先得到的(在RGB值之前),最終紅、綠、藍(lán)的值都會被預(yù)先乘以 alpha 的值
如果我們有一個橙色,他們各自的 8bpc 就像這樣: 240,99,24.一個完全不透明的橙色像素?fù)碛械?ARGB 值為: 255,240,99,24,它在內(nèi)存中的布局就像上面圖示那樣。如果我們有一個相同顏色的像素,但是 alpha 值為 33%,那么他的像素值便是:84,80,33,8.
- xRGB:像素并沒有任何 alpha 值(他們都被假定為100%不透明),但是內(nèi)存布局是一樣的,這樣子將會節(jié)約內(nèi)存【???:這種格式容易被現(xiàn)代的 CPU 和繪圖算法消化,因為每一個獨(dú)立的像素都對齊到 32-bit 的邊界】
x R G B x R G B x R G B
| pixel 0 | pixel 1 | pixel 2
0 1 2 3 4 5 6 7 8 9 10 11 ...
- xRGB即沒有alpha通道的圖片,它無法選擇透明度,雖然我們我們說有alpha通道的像素點(diǎn)其實(shí)就是將alpha預(yù)乘到了RGB里面,但這不意味著alpha沒用,在合成像素點(diǎn)的時候我們依然需要用alpha值計算混合后的值,所以對于這種圖片就是無法設(shè)置透明度
合成
- 在有多個圖層重疊的情況下,屏幕上的人看到的一個像素點(diǎn)可能是好幾個像素點(diǎn)合成后的結(jié)果,這就需要我們?nèi)ビ嬎愠霈F(xiàn)的像素應(yīng)該是什么造型
- 下面是理想狀態(tài)下的像素合成公式:
R = S + D * ( 1 – Sa )
結(jié)果的顏色 = 源色彩(頂端紋理) + 目標(biāo)顏色(低一層的紋理) * (1 - 源顏色的透明度)
- 但理想狀態(tài)很少見,更多情況下,我們需要進(jìn)行更為復(fù)雜的計算
透明與不透明
- 這里先區(qū)分三個概念
Alpha,Hidden與Opaque區(qū)別
- alpha是不透明度,屬性為浮點(diǎn)類型的值,取值范圍從0到1.0,表示從完全透明到完全不透明,其特性有當(dāng)前UIView的alpha值會被其所有subview繼承。alpha值會影響到UIView跟其所有subview,alpha具有動畫效果。當(dāng)alpha為0時,跟hidden為YES時效果一樣,但是alpha主要用于實(shí)現(xiàn)隱藏的動畫效果,在動畫塊中將hidden設(shè)置為YES沒有動畫效果
- 設(shè)置backgroundColor的alpha值只影響當(dāng)前UIView的背景,并不會影響其所有subview。Clear Color就是backgroundColor的alpha為1.0。alpha值會影響backgroundColor最終的alpha,假設(shè)UIView的alpha為0.8,backgroundColor的alpha為0.5,那么backgroundColor最終的alpha為0.4(0.8*0.5)
- Hidden表示UIView是否隱藏,Hidden設(shè)置為YES表示當(dāng)前UIView的所有subview也會被隱藏,忽略subview的hidden屬性。Hidden只要設(shè)置為YES,所有的subview都會隱藏。UIView隱藏之后也會從當(dāng)前的響應(yīng)者事件中移除
- opaque表示當(dāng)前的UIView的是否不透明【BOOL值】,但它就是涉及到了像素渲染問題,對于opaque為1的圖層,將會直接顯示,不會再去計算合成
合成區(qū)別
- 顯然,盡量多用opaque 為 YES的圖層可以節(jié)約GPU的消耗,加快渲染時間
- 如果你加載一個沒有 alpha 通道的圖片,并且將它顯示在 UIImageView 上,會自動設(shè)置opaque 為 YES
- 一個圖片沒有 alpha 通道和一個圖片每個地方的 alpha 都是100%,這將會產(chǎn)生很大的不同。在后一種情況下,Core Animation 需要假定是否存在像素的 alpha 值不為100%,也就是說依然需要進(jìn)行運(yùn)算
對齊與不對齊
- 如果幾個圖層的模版都是完美重合,那我們只要從第一個像素到最后一個像素都計算合成一下
- 但是如果像素沒有對齊好,我們還需要額外進(jìn)行額外的移位操作,合并原紋理上的像素
- 兩種情況會導(dǎo)致不對齊出現(xiàn):
- 第一個便是縮放;當(dāng)一個紋理放大縮小的時候,紋理的像素便不會和屏幕的像素排列對齊
- 另一個原因便是當(dāng)紋理的起點(diǎn)不在一個像素的邊界上
離屏渲染(Offscreen Rendering)
- 在屏幕中,我們要顯示的位圖直接存在當(dāng)前屏幕的后備存儲區(qū),同時這一塊內(nèi)容將會很快被刷新到屏幕上呈現(xiàn)
- 但加入我們需要寫一個簡單的平移動畫,假設(shè)有60幀動畫,那我們就需要重復(fù)生成60個圖層,并應(yīng)用
- 這是因為屏幕就這么大,我們放在當(dāng)前屏幕的后備存儲區(qū)必須得是即將放上去的內(nèi)容
- 這樣的操作方式顯然很浪費(fèi)GPU,所以遇到動畫Core Animation會自動觸發(fā)離屏渲染
什么是離屏渲染(Offscreen Rendering)
CRT顯示器
CRT顯示器是靠電子束激發(fā)屏幕內(nèi)表面的熒光粉來顯示圖像的,由于熒光粉被點(diǎn)亮后很快會熄滅,所以電子槍必須循環(huán)地不斷激發(fā)這些點(diǎn)
首先,在熒光屏上涂滿了按一定方式緊密排列的紅、綠、藍(lán)三種顏色的熒光粉點(diǎn)或熒光粉條,稱為熒光粉單元,相鄰的紅、綠、藍(lán)熒光粉單元各一個為一組,學(xué)名稱之為像素。每個像素中都擁有紅、綠、藍(lán)(R、G、B)三基色
顯示原理
- 顯示器顯示出來的圖像是經(jīng)過 CRT電子槍一行一行的掃描.(可以是橫向的也可以是縱向),掃描出來就呈現(xiàn)了一幀畫面,隨后電子槍又會回到初始位置循環(huán)掃描,為了讓顯示器的顯示跟視頻控制器同步,當(dāng)電子槍新掃描一行的時候.準(zhǔn)備掃描的時候,會發(fā)送一個 水平同步信號(HSync信號),而當(dāng)一幀畫面繪制完成后,電子槍回復(fù)到原位,準(zhǔn)備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization簡稱 VSync)
- 顯示器一般是固定刷新頻率的,這個刷新的頻率其實(shí)就是VSync信號產(chǎn)生的頻率. 然后CPU計算好frame等屬性,就將計算好的內(nèi)容提交給GPU去渲染,GPU渲染完成之后就會放入幀緩沖區(qū),然后視頻控制器會按照VSync信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器.就顯示出來了
離屏渲染原理
- 離屏渲染也就是說在屏幕外劃出一片緩存區(qū)作為屏幕外緩存區(qū),用來存儲需要渲染又不能馬上放入屏幕內(nèi)緩存區(qū)的紋理,這塊大小大約有屏幕大小兩倍的空間
- 這種做法的實(shí)質(zhì)就是將CPU拉來做GPU的事,讓CPU在屏幕外進(jìn)行渲染工作【雖然CPU性能比GPU強(qiáng)得多,但是在渲染這方面還不如GPU,用殺雞焉用牛刀的感覺】
- 離屏渲染這個機(jī)制出現(xiàn)的原因在于Apple對于流暢度要求很高,寧愿用空間【性能】換時間
- 但這是一種消耗極大的方式,不僅是CPU渲染有開銷,還包括兩次昂貴的環(huán)境轉(zhuǎn)換(轉(zhuǎn)換環(huán)境到屏幕外緩沖區(qū),然后轉(zhuǎn)換環(huán)境到幀緩沖區(qū))
- 這樣的好處就是可以復(fù)用,避免不必要的GPU渲染
- 另外復(fù)用也不是隨意復(fù)用的,這個可以被復(fù)用的位圖同樣是只能存在一段時間,之后會被卸載掉,你需要計算GPU 的利用率和幀的速率來判斷這個位圖是否有用
調(diào)試方式
- 調(diào)試主推薦Instrument里面的Core Animation工具
- 它可以查看項目的fps,GPU占用率

5FAD78E6BB6CC85AD123A7F166D02A18
- 但是我遇到了一個問題就是這個工具只能在真機(jī)上用,在模擬機(jī)上都說該設(shè)備不支持這個操作,不知道是哪一步整錯了
- 另一個是Xcode -> Debug > View Debugging > Rendering大法

截屏2019-10-15下午8.33.40
- 這個請注意要在運(yùn)行的時候才能點(diǎn)開
- Color Blended Layers 出現(xiàn)圖層混合的地方會標(biāo)注為紅色,沒有圖層混合的地方會顯示為綠色,方向是紅色越少越好,綠色越多越好
- Color Hits Green and Misses Red 當(dāng)使用光柵化渲染(shouldRasterize)的時候,如果圖層是綠色,表示這些緩存被復(fù)用,如果圖層是紅色表示緩存沒有被復(fù)用會重復(fù)創(chuàng)建,這時候會造成性能問題。
- 一般這兩個用的比較多
- 還有一個Color Offscreen-rendered Yellow 用來檢測是否離屏渲染【off-screen Rendering】
- GPU的渲染有兩種,On-screen Rendering當(dāng)前屏幕渲染,是指GPU的渲染在當(dāng)前屏幕的緩沖區(qū)內(nèi)進(jìn)行。off-screen Rendering是指在GPU的渲染發(fā)生在當(dāng)前屏幕之外新開辟的緩沖區(qū)。開辟新的緩沖區(qū),切換緩沖區(qū)等會對性能有較大的影響
- 同時,有七種情況會觸發(fā)離屏渲染
- cornerRadius以及masksToBounds同時使用時會觸發(fā)離屏渲染,單獨(dú)使用時不會觸發(fā)
- 設(shè)置shadow,而且shodowPath = nil時會觸發(fā)
- mask 設(shè)置蒙版會觸發(fā)
- layer.shouldRasterize的不適當(dāng)使用會觸發(fā)離屏渲染
- layer.allowsGroupOpacity iOS7以后默認(rèn)開啟;當(dāng)layer.opacity != 1.0且有subLayer或者背景圖時會觸發(fā)
- layer.allowsEdgeAntialiasing 在iOS8以后的系統(tǒng)里可能已經(jīng)做了優(yōu)化,并不會觸發(fā)離屏渲染,不會對性能造成影響
- 重寫了drawRect
iOS繪制
CALayer與UIView
<img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8jqfuqu0wj30eg03paam.jpg" alt="img" style="zoom:200%;" />
- 每一個UIView都有一個layer,每一個layer都有個content,這個content指向的是一塊緩存,叫做backing store
- 在沒有重寫drawRect方法的情況下,CALayer的content為空,重寫后系統(tǒng)會為該layer的content開辟一塊緩存,大小
size = width * height * scale,用來存放drawRect繪制的內(nèi)容【就算重寫內(nèi)容為空也會新建緩存,注意】
代碼驗證
//
// MyView.m
// TableView-Optimizing-ChemeDemo
//
// Created by Kevin.J on 2019/11/2.
// Copyright ? 2019 姜凱文. All rights reserved.
//
#import "MyView.h"
@implementation MyView
-(void)drawRect:(CGRect)rect{
NSLog(@"4MyView.before.drawRect.layer.contents %@",self.layer.contents);
// CGContextRef context = UIGraphicsGetCurrentContext();
// draw something
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"3MyView.after.drawRect.layer.contents %@",self.layer.contents);
});
}
@end
//
// ViewController.m
// TableView-Optimizing-ChemeDemo
//
// Created by Kevin.J on 2019/10/8.
// Copyright ? 2019 姜凱文. All rights reserved.
//
#import "ViewController.h"
#import "AsyncLabel.h"
#import "MyView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//AsyncLabel *label = [[UILabel alloc] init];
MyView* myView = [[MyView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1MyView.after.drawRect.layer.contents %@",myView.layer.contents);
});
[self.view addSubview:myView];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2MyView.after.drawRect.layer.contents %@",myView.layer.contents);
});
}
@end
輸出的順序為:
4MyView.before.drawRect.layer.contents (null)
1MyView.after.drawRect.layer.contents <CABackingStore 0x7f9aa8f00ff0>
2MyView.after.drawRect.layer.contents <CABackingStore 0x7f9aa8f00ff0>
3MyView.after.drawRect.layer.contents <CABackingStore 0x7f9aa8f00ff0>
繪制細(xì)節(jié)
- CALayer提供了三種繪制內(nèi)容的方式,若采用優(yōu)先級高的方式繪制了內(nèi)容,則低優(yōu)先級的則不會執(zhí)行

img
繪制時機(jī)
- 當(dāng)在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個全局的容器去。
- 蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個很長的函數(shù):
- _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數(shù)里會遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面
- 當(dāng)UIView被繪制時(從 CA::Transaction::commit:以后),CPU執(zhí)行drawRect,通過context將數(shù)據(jù)寫入backing store。當(dāng)backing store寫完后,通過render server交給GPU去渲染,將backing store中的bitmap數(shù)據(jù)顯示在屏幕上
參考文章
- iOS性能優(yōu)化(初級)
- iOS性能優(yōu)化(中級)
- iOS性能優(yōu)化(中級+): 異步繪制
- iOS繪制與渲染--CPU繪制
- iOS-CPU&&GPU分別做什么?
- CoreText是如何繪制文本的
- 【重讀iOS】認(rèn)識CALayer
- 為什么必須在主線程操作UI
- 異步繪制
- 位圖:百度百科
- 繪制像素到屏幕上
- iOS開發(fā)-Alpha,Hidden與Opaque區(qū)別
- [技巧]UIView的hidden和alpha的妙用
- 關(guān)于離屏渲染
- iOS-離屏渲染詳解
- 離屏渲染優(yōu)化詳解:實(shí)例示范+性能測試
- 淺談iOS中的視圖優(yōu)化