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

前言

  • 這一塊主要爲了面試講項目準備,儘量理論與實踐相結合吧,能把搜到的資料都實現最好
  • 目前主要是爲tableView的優化進行研究,把涉及到的異步繪製【圖層問題】,網絡請求中的cell中空間高度緩存這些就用到最好

FPS

  • FPS是圖像領域中的定義,是指畫面每秒傳輸幀數,通俗來講就是指動畫或視頻的畫面數。FPS是測量用於保存、顯示動態視頻的信息數量。每秒鐘幀數越多,所顯示的動作就會越流暢。通常,要避免動作不流暢的最低是30。
  • iPhone的FPS爲60,也就是說它在一秒鐘內會刷新60次,每次間隔大概16.7ms,也就是說我們的iPhone每次有16.7ms的時間來處理事件
  • 而如果在這個時間內沒能成功刷新,就會直接丟棄這一幀,下一次一起整上去【也就是說,不會原地刷新】
  • 這樣子幀的丟失反映到用到用戶體驗上就 = APP卡頓了

CPU

  • 中央處理器(CPU,central processing unit)作爲計算機系統的運算和控制核心,是信息處理、程序運行的最終執行單元
  • 放在我們的iOS中,可以理解爲CPU負責軟件的操作,對象創建、對象調整、對象銷燬、佈局計算、文本計算、文本渲染、圖片解碼、圖像的繪製等操作是放在CPU上執行的

GPU

  • 圖形處理器(英語:Graphics Processing Unit,縮寫:GPU),又稱顯示核心、視覺處理器、顯示芯片,是一種專門在個人電腦、工作站、遊戲機和一些移動設備(如平板電腦、智能手機等)上做圖像和圖形相關運算工作的微處理器。
  • 放在iOS中,GPU處理的是硬件層面的事,紋理的渲染、視圖混合、離屏渲染都是在GPU上執行的

位圖

  • 位圖是由稱作像素(圖片元素)的單個點組成的。這些點可以進行不同的排列和染色以構成圖樣。當放大位圖時,可以看見賴以構成整個圖像的無數單個方塊。擴大位圖尺寸的效果是增大單個像素,從而使線條和形狀顯得參差不齊。然而,如果從稍遠的位置觀看它,位圖圖像的顏色和形狀又顯得是連續的。用數碼相機拍攝的照片、掃描儀掃描的圖片以及計算機截屏圖等都屬於位圖。位圖的特點是可以表現色彩的變化和顏色的細微過渡,產生逼真的效果,缺點是在保存時需要記錄每一個像素的位置和顏色值,佔用較大的存儲空間。常用的位圖處理軟件有Photoshop(同時也包含矢量功能)、Painter和Windows系統自帶的畫圖工具等,Adobe Illustrator則是矢量圖軟件

像素點如何出現在屏幕上

  • 先來看一張全部流程圖:

pixels-software-stack

  • 這張圖從從右到左就是軟件到硬件,從App到硬件屏幕上出現界面的全過程
  • 這裏再介紹下前面沒介紹的模塊

渲染參與者

  • GPU Driver:GPU驅動軟件,直接和 GPU 交流的代碼塊
  • OpenGL:提供了 2D 和 3D 圖形渲染的 API,高GPU的能力,並實現硬件加速渲染,是第一個和圖形硬件(GPU)交流的標準化方式
  • Core Graphics:Quartz 2D的一個高級繪圖引擎,常用與iOS,tvOS,macOS的圖形繪製應用開發。Core Graphics是對底層C語言的一個簡單封裝,其中提供大量的低層次,輕量級的2D渲染API。前綴爲CG,比如常見的CGPath,CGColor
  • Core Animation:是蘋果提供的一套基於繪圖的動畫框架,但不止是動畫,他同樣是繪圖的根本【因爲其前綴是CA,CALayer有多重要就不用多說了吧】

6379271-f8367726caa19e8a

  • 從圖中可以看出,最底層是圖形硬件(GPU);上層是OpenGL和CoreGraphics,提供一些接口來訪問GPU;再上層的CoreAnimation在此基礎上封裝了一套動畫的API。最上面的UIKit屬於應用層,處理與用戶的交互
  • Core Image:iOS處理圖像的框架,主要用處可以給圖片添加濾鏡效果和圖像識別功能

we are A!R!G!B!

  • 這裏講下對於像素需要知道的知識:
  • 屏幕上的像素是由紅,綠,藍三種顏色組件構成的。因此,位圖數據有時也被叫做 RGB 數據
  • 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 ...
  
這個格式經常被叫做 ARGB。每個像素佔用 4 字節(32bpp),每一個顏色組件是1字節(8bpc).每個像素有一個 alpha 值,這個值總是最先得到的(在RGB值之前),最終紅、綠、藍的值都會被預先乘以 alpha 的值

如果我們有一個橙色,他們各自的 8bpc 就像這樣: 240,99,24.一個完全不透明的橙色像素擁有的 ARGB 值爲: 255,240,99,24,它在內存中的佈局就像上面圖示那樣。如果我們有一個相同顏色的像素,但是 alpha 值爲 33%,那麼他的像素值便是:84,80,33,8.
  • xRGB:像素並沒有任何 alpha 值(他們都被假定爲100%不透明),但是內存佈局是一樣的,這樣子將會節約內存【???:這種格式容易被現代的 CPU 和繪圖算法消化,因爲每一個獨立的像素都對齊到 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通道的像素點其實就是將alpha預乘到了RGB裏面,但這不意味着alpha沒用,在合成像素點的時候我們依然需要用alpha值計算混合後的值,所以對於這種圖片就是無法設置透明度

合成

  • 在有多個圖層重疊的情況下,屏幕上的人看到的一個像素點可能是好幾個像素點合成後的結果,這就需要我們去計算出現的像素應該是什麼造型
  • 下面是理想狀態下的像素合成公式:
R = S + D * ( 1 – Sa )

結果的顏色 = 源色彩(頂端紋理) + 目標顏色(低一層的紋理) * (1 - 源顏色的透明度)
  • 但理想狀態很少見,更多情況下,我們需要進行更爲複雜的計算

透明與不透明

  • 這裏先區分三個概念

Alpha,Hidden與Opaque區別

  • alpha是不透明度,屬性爲浮點類型的值,取值範圍從0到1.0,表示從完全透明到完全不透明,其特性有當前UIView的alpha值會被其所有subview繼承。alpha值會影響到UIView跟其所有subview,alpha具有動畫效果。當alpha爲0時,跟hidden爲YES時效果一樣,但是alpha主要用於實現隱藏的動畫效果,在動畫塊中將hidden設置爲YES沒有動畫效果
  • 設置backgroundColor的alpha值隻影響當前UIView的背景,並不會影響其所有subview。Clear Color就是backgroundColor的alpha爲1.0。alpha值會影響backgroundColor最終的alpha,假設UIView的alpha爲0.8,backgroundColor的alpha爲0.5,那麼backgroundColor最終的alpha爲0.4(0.8*0.5)
  • Hidden表示UIView是否隱藏,Hidden設置爲YES表示當前UIView的所有subview也會被隱藏,忽略subview的hidden屬性。Hidden只要設置爲YES,所有的subview都會隱藏。UIView隱藏之後也會從當前的響應者事件中移除
  • opaque表示當前的UIView的是否不透明【BOOL值】,但它就是涉及到了像素渲染問題,對於opaque爲1的圖層,將會直接顯示,不會再去計算合成

合成區別

  • 顯然,儘量多用opaque 爲 YES的圖層可以節約GPU的消耗,加快渲染時間
  • 如果你加載一個沒有 alpha 通道的圖片,並且將它顯示在 UIImageView 上,會自動設置opaque 爲 YES
  • 一個圖片沒有 alpha 通道和一個圖片每個地方的 alpha 都是100%,這將會產生很大的不同。在後一種情況下,Core Animation 需要假定是否存在像素的 alpha 值不爲100%,也就是說依然需要進行運算

對齊與不對齊

  • 如果幾個圖層的模版都是完美重合,那我們只要從第一個像素到最後一個像素都計算合成一下
  • 但是如果像素沒有對齊好,我們還需要額外進行額外的移位操作,合併原紋理上的像素
  • 兩種情況會導致不對齊出現:
    • 第一個便是縮放;當一個紋理放大縮小的時候,紋理的像素便不會和屏幕的像素排列對齊
    • 另一個原因便是當紋理的起點不在一個像素的邊界上

離屏渲染(Offscreen Rendering)

  • 在屏幕中,我們要顯示的位圖直接存在當前屏幕的後備存儲區,同時這一塊內容將會很快被刷新到屏幕上呈現
  • 但加入我們需要寫一個簡單的平移動畫,假設有60幀動畫,那我們就需要重複生成60個圖層,並應用
  • 這是因爲屏幕就這麼大,我們放在當前屏幕的後備存儲區必須得是即將放上去的內容
  • 這樣的操作方式顯然很浪費GPU,所以遇到動畫Core Animation會自動觸發離屏渲染

什麼是離屏渲染(Offscreen Rendering)

CRT顯示器

  • CRT顯示器是靠電子束激發屏幕內表面的熒光粉來顯示圖像的,由於熒光粉被點亮後很快會熄滅,所以電子槍必須循環地不斷激發這些點

  • 首先,在熒光屏上塗滿了按一定方式緊密排列的紅、綠、藍三種顏色的熒光粉點或熒光粉條,稱爲熒光粉單元,相鄰的紅、綠、藍熒光粉單元各一個爲一組,學名稱之爲像素。每個像素中都擁有紅、綠、藍(R、G、B)三基色

顯示原理

  • 顯示器顯示出來的圖像是經過 CRT電子槍一行一行的掃描.(可以是橫向的也可以是縱向),掃描出來就呈現了一幀畫面,隨後電子槍又會回到初始位置循環掃描,爲了讓顯示器的顯示跟視頻控制器同步,當電子槍新掃描一行的時候.準備掃描的時候,會發送一個 水平同步信號(HSync信號),而當一幀畫面繪製完成後,電子槍回覆到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號(vertical synchronization簡稱 VSync)
  • 顯示器一般是固定刷新頻率的,這個刷新的頻率其實就是VSync信號產生的頻率. 然後CPU計算好frame等屬性,就將計算好的內容提交給GPU去渲染,GPU渲染完成之後就會放入幀緩衝區,然後視頻控制器會按照VSync信號逐行讀取幀緩衝區的數據,經過可能的數模轉換傳遞給顯示器.就顯示出來了

離屏渲染原理

  • 離屏渲染也就是說在屏幕外劃出一片緩存區作爲屏幕外緩存區,用來存儲需要渲染又不能馬上放入屏幕內緩存區的紋理,這塊大小大約有屏幕大小兩倍的空間
  • 這種做法的實質就是將CPU拉來做GPU的事,讓CPU在屏幕外進行渲染工作【雖然CPU性能比GPU強得多,但是在渲染這方面還不如GPU,用殺雞焉用牛刀的感覺】
  • 離屏渲染這個機制出現的原因在於Apple對於流暢度要求很高,寧願用空間【性能】換時間
  • 但這是一種消耗極大的方式,不僅是CPU渲染有開銷,還包括兩次昂貴的環境轉換(轉換環境到屏幕外緩衝區,然後轉換環境到幀緩衝區)
  • 這樣的好處就是可以複用,避免不必要的GPU渲染
  • 另外複用也不是隨意複用的,這個可以被複用的位圖同樣是只能存在一段時間,之後會被卸載掉,你需要計算GPU 的利用率和幀的速率來判斷這個位圖是否有用

調試方式

  • 調試主推薦Instrument裏面的Core Animation工具
  • 它可以查看項目的fps,GPU佔用率

5FAD78E6BB6CC85AD123A7F166D02A18

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

截屏2019-10-15下午8.33.40

  • 這個請注意要在運行的時候才能點開
  • Color Blended Layers 出現圖層混合的地方會標註爲紅色,沒有圖層混合的地方會顯示爲綠色,方向是紅色越少越好,綠色越多越好
  • Color Hits Green and Misses Red 當使用光柵化渲染(shouldRasterize)的時候,如果圖層是綠色,表示這些緩存被複用,如果圖層是紅色表示緩存沒有被複用會重複創建,這時候會造成性能問題。
  • 一般這兩個用的比較多
  • 還有一個Color Offscreen-rendered Yellow 用來檢測是否離屏渲染【off-screen Rendering】
    • GPU的渲染有兩種,On-screen Rendering當前屏幕渲染,是指GPU的渲染在當前屏幕的緩衝區內進行。off-screen Rendering是指在GPU的渲染髮生在當前屏幕之外新開闢的緩衝區。開闢新的緩衝區,切換緩衝區等會對性能有較大的影響
    • 同時,有七種情況會觸發離屏渲染
      1. cornerRadius以及masksToBounds同時使用時會觸發離屏渲染,單獨使用時不會觸發
      2. 設置shadow,而且shodowPath = nil時會觸發
      3. mask 設置蒙版會觸發
      4. layer.shouldRasterize的不適當使用會觸發離屏渲染
      5. layer.allowsGroupOpacity iOS7以後默認開啓;當layer.opacity != 1.0且有subLayer或者背景圖時會觸發
      6. layer.allowsEdgeAntialiasing 在iOS8以後的系統裏可能已經做了優化,並不會觸發離屏渲染,不會對性能造成影響
      7. 重寫了drawRect

iOS繪製

CALayer與UIView

img
  • 每一個UIView都有一個layer,每一個layer都有個content,這個content指向的是一塊緩存,叫做backing store
  • 在沒有重寫drawRect方法的情況下,CALayer的content爲空,重寫後系統會爲該layer的content開闢一塊緩存,大小size = width * height * scale,用來存放drawRect繪製的內容【就算重寫內容爲空也會新建緩存,注意】

代碼驗證

//
//  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>

繪製細節

  • CALayer提供了三種繪製內容的方式,若採用優先級高的方式繪製了內容,則低優先級的則不會執行

img

繪製時機

  • 當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。
  • 蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:
  • _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裏會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面
  • 當UIView被繪製時(從 CA::Transaction::commit:以後),CPU執行drawRect,通過context將數據寫入backing store。當backing store寫完後,通過render server交給GPU去渲染,將backing store中的bitmap數據顯示在屏幕上

參考文章

  1. iOS性能優化(初級)
  2. iOS性能優化(中級)
  3. iOS性能優化(中級+): 異步繪製
  4. iOS繪製與渲染–CPU繪製
  5. iOS-CPU&&GPU分別做什麼?
  6. CoreText是如何繪製文本的
  7. 【重讀iOS】認識CALayer
  8. 爲什麼必須在主線程操作UI
  9. 異步繪製
  10. 位圖:百度百科
  11. 繪製像素到屏幕上
  12. iOS開發-Alpha,Hidden與Opaque區別
  13. [技巧]UIView的hidden和alpha的妙用
  14. 關於離屏渲染
  15. iOS-離屏渲染詳解
  16. 離屏渲染優化詳解:實例示範+性能測試
  17. 淺談iOS中的視圖優化
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章