在嘗試異步繪制前,我去了解了這些iOS渲染知識

前言

  • 這一塊主要為了面試講項目準(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ā)離屏渲染
      1. cornerRadius以及masksToBounds同時使用時會觸發(fā)離屏渲染,單獨(dú)使用時不會觸發(fā)
      2. 設(shè)置shadow,而且shodowPath = nil時會觸發(fā)
      3. mask 設(shè)置蒙版會觸發(fā)
      4. layer.shouldRasterize的不適當(dāng)使用會觸發(fā)離屏渲染
      5. layer.allowsGroupOpacity iOS7以后默認(rèn)開啟;當(dāng)layer.opacity != 1.0且有subLayer或者背景圖時會觸發(fā)
      6. layer.allowsEdgeAntialiasing 在iOS8以后的系統(tǒng)里可能已經(jīng)做了優(yōu)化,并不會觸發(fā)離屏渲染,不會對性能造成影響
      7. 重寫了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ù)顯示在屏幕上

參考文章

  1. iOS性能優(yōu)化(初級)
  2. iOS性能優(yōu)化(中級)
  3. iOS性能優(yōu)化(中級+): 異步繪制
  4. iOS繪制與渲染--CPU繪制
  5. iOS-CPU&&GPU分別做什么?
  6. CoreText是如何繪制文本的
  7. 【重讀iOS】認(rèn)識CALayer
  8. 為什么必須在主線程操作UI
  9. 異步繪制
  10. 位圖:百度百科
  11. 繪制像素到屏幕上
  12. iOS開發(fā)-Alpha,Hidden與Opaque區(qū)別
  13. [技巧]UIView的hidden和alpha的妙用
  14. 關(guān)于離屏渲染
  15. iOS-離屏渲染詳解
  16. 離屏渲染優(yōu)化詳解:實(shí)例示范+性能測試
  17. 淺談iOS中的視圖優(yōu)化
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容