阿里、字節:一套高效的iOS面試題(九 - 視圖&圖像相關 - 下)

視圖 & 圖像相關

擼面試題中,文中內容基本上都是搬運自大佬博客及自我理解,可能有點亂,不喜勿噴!!!html

原文題目來自:阿里、字節:一套高效的iOS面試題ios

阿里、字節:一套高效的iOS面試題(九 - 視圖&圖像相關 - 上git

3、UI 繪製

二、UI 顯示到屏幕上

先看一下來自 繪製像素到屏幕上 的圖:github

Display:顯示器的主要做用就是顯示 RGB 數據,大部分顯示器都具備調整自身顯示偏移、亮度、飽和度的能力。總結起來就是對傳入的 RGB 數據進行處理。面試

GPU:Display 的上一層是圖形處理單元 GPU,GPU 是專門爲圖形高併發計算而量身定作的處理單元。GPU 能夠高效地合成不一樣的紋理。算法

GPU Driver:GPU 的驅動, 是直接和 GPU 交流的代碼。是它爲不一樣的 GPU 定製了統一的接口。典型的接口由 OpenGL / OpenGL ES,固然 Apple 如今有了自家的 Metal。緩存

OpenGL:全稱 Open Graphics Library,是一個和 GPU 直接交流的標準化接口,提供了 2D 與 3D 圖像渲染的 API。OpenGL 代碼能夠直接操做 GPU,實現最高的渲染效率。bash

OpenGL ES:全稱 OpenGL for Embeded System,是 OpenGL 的一個子集,主要針對手機等嵌入式設備。併發

Core Graphics:Quartz 2D 的一個高級繪圖引擎。Core Graphics 是對底層 C 語言的封裝,其中提供大量的底層地,輕量級的 2D 渲染 API。(前綴爲 CG,如 CGPath,CGColor)app

Core Animation:Apple 提供的一套基於繪圖的動畫框架。但不止是動畫,它一樣是繪圖的根本。(前綴爲 CA,如 CALayer)

Core Image:iOS 提供的圖形處理框架,主要用於圖像識別,給圖片添加濾鏡。

2.1 像素和點

  • 像素

每個像素均由三個顏色組件構成:紅、綠、藍。只要根據須要將三個獨立的顏色以給定的數值顯示到一個屏幕像素上,就能夠達到咱們想要的結果。幸運的是,咱們不須要從這裏開始寫代碼。

一般咱們使用一個字節(也便是 8 位,最大 255)來表示一個顏色單位的數值,也就是說,一個顏色單位顯示的亮度由一個字節來控制。這樣算的話,三個顏色單位就須要 3 個字節,好比:白色的數值是 0xFFFFFF(當紅綠藍三個單元的顯示亮度同時達到最大值時,最終合成結果就是白色),黑色就是 0x000000(三個單元的顯示亮度同時爲 0,就是一片漆黑了)。

關於這一點,既能夠在 PS 中驗證,也能夠將圖片讀取並轉化爲 Data 查看:

UIImage *image = [UIImage imageNamed:@"theFox.jpg"];
CFDateRef data = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
NSLog(@"%@", data);
CFRelease(data);


/// 輸出:都是白色對不對,由於這張圖大部分都是白色啊~~~~
{length = 1890624, bytes = 0xffffffff ffffffff ffffffff ffffffff ... ffffffff ffffffff }
複製代碼

等等,爲何打印出來的是 ffffffff,而不是 ffffff。多出來了 ff 其實就是 透明度 alpha 了,它也由一個字節表示。這張圖的分辨率爲 687x688,那咱們算一下: 687 x 688 x 4 = 1890624,結果跟 log 中的 length 徹底相符。

平常開發中,咱們進場會指定 UIView.frame = CGRectMake(50, 50, 200, 200),這裏的 50 等數值都是開發使用的邏輯座標系中的點。使用原生繪製 UIKit、Core Animation、Quartz 時,其繪製座標系與視圖座標系都是邏輯座標系。而屏幕中的像素點則被稱爲物理座標系。

系統會自動根據視圖的點座標映射到設備的像素上去。但因爲尺寸等因素,物理座標系的像素與邏輯座標系的點並不必定是一一對應的。開發中使用點代替像素的主要目的就是爲了保證視圖在各種設備上都呈現出合適的效果。具體多少像素對應一個點,這是由系統根據設備硬件決定的。

咱們都知道視網膜屏幕這個概念,它首次出如今 iPhone 4 上。在視網膜屏幕中,一條線的繪製對應着多個像素的線條寬度。這種映射關係使普通顯示屏與視網膜屏幕上的視圖大小基本保持一致。

在 iOS 中,UIScreen、UIView、UIImage、CALayer 都提供用於描述像素和點之間的映射比例。例如 UIView 的 contentScaleFactor,CALayer 的 contentsScale,而 UIScreen 與 UIImage 是 scale。在普通顯示屏中,該值爲 1.0,視網膜屏幕中爲 2.0,而 plus 系統爲 3.0。(這些屬性都是出現於 iOS 4)

2.2 紋理合成

一個紋理,就是一個包含 RGBA 值得矩形存儲空間。瞭解 AR 或者 U3D 開發的朋友應該明白這個概念。紋理,在 Core Animation 中就至關於 CALayer。

這樣理解下來,每個 layer 都是一個紋理,全部的紋理按照特定層級順序以某種方式堆疊起來所獲得的結果紋理就是最終顯示在屏幕上的。對於屏幕上的每個像素,GPU 都須要計算出具體的 RGB 值。

所以,咱們只須要搞清楚一個像素的合成便能理解成哥紋理的合成了。假定兩個像素 S 和 D(S 在頂端),那麼 (S + D) -> R 的合成算法爲:

R = S + D * (1 - S.a)   /// S.a 是 S 像素的透明度
複製代碼

合成結果 = 源色彩(頂端紋理) + 目標色彩(第一層的紋理) * (1 - 源色彩的透明度)

固然,在這個公式裏,全部的像素的顏色都已經預先計算過其透明度了。

假定 S 爲紅色(1, 0, 0),D 爲藍色(0, 0, 1):

  1. S.alpha = 1:

    源色彩徹底不透明,S = (1, 0, 0);

    目標色彩 D = (0, 0, 1) * (1 - 1) = (0, 0, 0);

    合成結果爲 R = (1, 0, 0),紅色。

  2. S.alpha = 0.5:

    源色彩爲 50% 透明,此時 S = (0.5, 0, 0);

    目標色彩爲 D = (0, 0, 1) * (1 - 0.5) = (0, 0, 0.5);

    合成結果爲 R = (0.5, 0, 0.5),紫色。

2.2 圖層透明

當源紋理徹底不透明時,合成結果就等於原紋理。這能夠節省 GPU 很大的工足量,這樣只須要單純的拷貝源紋理而不須要合成全部的像素值。那麼,有沒有這樣一種方法告訴 GPU 紋理上的像素究竟是不是透明的呢?

CALayer 都存在一個名爲 opaque,類型爲 BOOL 的屬性。這個單詞翻譯過來就是 「不透明的」。當咱們將這個屬性設置爲 YES 時,GPU 將不會作任何合成,而是直接從這個 layer 拷貝,徹底不考慮其下方的任何東西,這能夠大大節省 GPU 的工做量。

因此,UIView 纔會將從 layer 包裝而來的 opaque 默認爲 YES,這是一個至關有用的優化。【可是,CALayer 的 opaque 屬性默認值爲 NO】

咱們至少有兩種方便的方式來查看當前佈局中哪些 layer 是透明的:

  1. 工具 Instruments 中的 color blended layers 功能【目前已集成到 Xcode -> Debug -> View Debugging -> Rendering 中】;

  2. 模擬器 Simulator 的菜單 Debug -> Color Blended layers

因此,若是知道一個 layer 是不透明的,將他的 opaque 設置爲 YES。

若是加載一張沒有 alpha 通道的圖片並顯示在 UIImageView 上,上述操做會自動設置。但一個沒有 alpha 通道的圖片與一個帶有透明通道但任何地方 alpha 都爲 1 的圖片,這兩種狀況是徹底不一樣的。在後一種狀況下,Core Animation 須要假定是否存在像素的 alpha 值不爲 1。

在 Finder 中,可使用 Get Info(顯示簡介)並檢查 More Info (更多信息)部分,來肯定圖片是否包含 alpha 通道:

2.3 像素對齊

到如今爲止,咱們都考慮的是像素完美對齊的狀況。當全部像素都是對齊的時候,咱們獲得相對簡單的數學公式。當 GPU 須要計算屏幕上一個像素是什麼顏色時,只須要將每一個 layers 上對應的單個像素合成到一塊兒就能夠了。或者,若是頂層紋理是不透明度的,此時 GPU 簡單拷貝頂層紋理的像素便可。

當一個 layer 上的像素與屏幕上的像素完美對齊時,這個 layer 就是像素對齊的。形成不對齊的緣由主要有兩個。第一個就是 scale,當一個紋理放大或縮小的時候,紋理的像素便不會和屏幕的像素對齊。另外一個緣由即是紋理的起點不在像素的邊界上。

在這兩種狀況下,CPU 須要作額外的計算。它須要將源紋理上的多個像素混合一塊兒,生成一個用於合成的值。在像素對齊的狀況下,GPU 須要作的工做並很少。

有兩種方便的方式來檢查這個問題:

  1. 工具 Instrucments 中的 Color Misaligned Images 功能【目前已集成到 Xcode -> Debug -> View Debugging -> Rendering 中】;

  2. 模擬器 Simulator 的菜單 Debug -> Color Blended layers

2.4 深刻 CALayer

磨刀不誤砍柴工,UIView 的繪製其實就是 CALayer 的繪製,咱們先從這裏開始吧。

如何爲 layer 提供 contents

使用 Image

直接將 CGImageRef 對象賦值給 contents 屬性便可。其好處在於 layer 直接使用該 CGImageRef 對象,不建立副本(在多個地方使用相同圖像的話,能夠節省內存)。

可是在 retain 屏幕上,咱們須要設置 contentsScale 屬性。

讓 delegate 提供

當咱們使用 delegate 爲 layer 提供顯示內容的時候。咱們能夠選擇實現 displayLayer: 方法或 drawLayer:InContext 方法。

若是同時重寫兩個方法,那麼只會調用 displayLayer:

  • displayLayer:

此時須要咱們本身建立位圖進行繪畫,最後賦值給 contents 屬性。

這裏的操做徹底能夠在後臺線程執行,也就是 異步繪製

- (void)display {

    /// self.contents = (__bridge id)[UIImage imageNamed:@"theGirl.JPG"].CGImage;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 100, 100)];
        [[UIColor systemPinkColor] setFill];
        [path fill];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)image.CGImage;
        });
    });
}


LyLayer *theLayer = [LyLayer new];
theLayer.frame = self.view.bounds;
[self.view.layer addSublayer:theLayer];

[theLayer setNeedsDisplay]; /// 記住這句
複製代碼
  • drawLayer:InContext

重寫 drawLayer:InContext 方法時,Core Animation 會自動爲咱們建立好一個位圖,和一個圖形上下文 CGContextRef。咱們須要作的就是使用這個 CGContextRef 來繪製咱們想要的內容。

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    NSLog(@"%s", __func__);

    CGContextAddArc(ctx, 80, 80, 10, 0, 2 * M_PI, 1); // 畫圓
    CGContextSetLineWidth(ctx, 3); // 設置線粗
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); // 畫線的顏色
    CGContextStrokePath(ctx); // 畫線
}
複製代碼
子類化 CALayer

這種狀況下,能夠重寫 displaydrawInContext: 方法來繪圖。

  • display

這是繪製方法的主入口,重寫該方法能夠完成掌控繪製流程,可是這就意味着須要咱們來建立原本就要分配給 contents 的 CGImageRef。

這裏的演示代碼能夠直接套用 displayLayer: 的。

  • drawInContext:

若是隻是想繪製內容,重寫這個方法纔是較好的選擇(layer 會自動給咱們建立後備存儲)

這裏的演示代碼也能夠套用上邊的 drawLayer:InContext:


我來畫一個流程圖:

NOTE UIView 的 drawRect: 是從 drawInContext: 調用過去的。

若是建立 view 自動建立的 layer

若是這個 layer 是建立 view 而自動建立的,那狀況有所不一樣。

若是 view 沒有實現 drawRect: 方法,上述四個方法一個都不會被調用到。

若是 view 實現了 drawRect: 方法,也分兩種狀況:

  1. view 實現了 displayLayer: 方法,此時的調用棧是:

  2. view 沒有實現 displayLayer: 方法,此時的調用棧是:

繪製方法的調用時機

  • 改變 bounds 是不會調用 繪製方法的。

除非其屬性 needsDisplayOnBoundsChange 爲 YES(就像 UIView 的 contentModeUIViewContentModeRedraw 同樣)。

不管經過設置 contents 仍是本身在 layer 上繪圖,都會快照,這個後備存儲裏邊保存着 layer 的圖像緩存。當 layer 改變大小時,只須要拉伸這個圖像緩存就行了。

  • layer 不會主動重繪

不管是 display 仍是 drawInContext: 方法,CALayer 都不會主動去調用這些繪製回調方法的。須要顯示或重繪時,須要咱們手動調用 [layer setNeedsDsiplay] 方法。

UIView 首次展現時,系統會自動調用其 setNeedsDisplay,並且會自動傳遞給這個 view 的 layer,因此 view 自帶的 layer 無需調用 setNeedsDisplay 方法。

這裏在 視圖繪製週期 ,以及 layer 的繪製方法 這兩節的說法一致。

  • 繪製回調的優先級

display 系列方法內部是經過給 contents 設置 CGImage 來完成繪製目的的。而 draw系列方法是調用 CoreGraphics 的 API 。

一旦 delegate 響應了 displayLayer: 方法,draw 系列方法是沒有出場機會的。

這是因爲 兩個系列方法的調用機制來決定的。也就是在講 drawInContext: 方法時的那張圖。

CALayer 的後備存儲 backing store

WWDC 2012: iOS Performance: Graphics And Animations

這是 WWDC 2012: iOS Performance: Graphics And Animations 的一張圖。每個 CALayer 都有一個像素位圖的後備存儲,它會被映射成 GPU 上的一個紋理圖屏幕上顯示出來。

可是並非全部狀況這個後備存儲實際存在。當咱們使用 displaydrawRect: 準備在 layer 上繪圖時,layer 就會自動建立一塊與 layer 相同大小的內存區域,在以後繪圖的結果就保存在這塊區域中,而這塊區域就被稱爲 backing store

準確來講,並非 displaydrawRect: ,而是 drawRect: 這一個。

看一下 display 這個方法的官方介紹:

Do not call this method directly. The layer calls this method at appropriate times to update the layer’s content. If the layer has a delegate object, this method attempts to call the delegate’s displayLayer: method, which the delegate can use to update the layer’s contents. If the delegate does not implement the displayLayer: method, this method creates a backing store and calls the layer’s drawInContext: method to fill that backing store with content. The new backing store replaces the previous contents of the layer.

重點在斜體加粗那一句。若是 delegate 沒有實現 displayLayer: 方法,這個方法將建立一個後備存儲

若是咱們將一個 CGImage 賦值給 contents,那麼 layer 就不會建立這個後備存儲,此時 layer 的 contents 就是咱們傳進去的 CGImage,在渲染時會直接拷貝這個 CGImage 到幀緩衝區中。

2.4 UIView 的繪製流程

圖片來自 iOS——圖像顯示原理以及UI流暢性優化方案

iOS——圖像顯示原理以及UI流暢性優化方案

調用 view 的 setNeedsDisplay ,該方法內部調用這個 view 的 layer 的同名方法,這個 layer 被標記爲 dirty。隨後在當前 runlop 快要結束的時候調用 CALayer.display 方法,纔會進行當前視圖真正的繪製流程。

CALayer.display 方法內部會先判斷該 layer 的 delegate 是否響應 displayLayer 方法。若是沒法響應,就會進入系統的繪製流程中;若是響應,就會調用異步繪製的接口。

系統繪製流程

這是我本身通過測試,畫出的系統繪製流程圖:

系統繪製流程

這是證據:

drawRect: 調用棧

另外,若是在 LyView 中不從寫 drawRect: 這個方法,就算 layer 重寫 drawInContext: 且 LyView 重寫 drawLayer:InContext:,重寫的這兩個方法也不會被調用,同時符號斷點 [UIView drawRect:] 也不會進入。

總結下來,系統繪製的流程爲:

  1. 判斷 view 是否實現了 drawRect: 方法?

  2. 若是沒有實現,走鮮爲人知的流程。。。

  3. 若是實現了,就是上邊的流程圖。

異步繪製流程

若是 layer 的 delegate 實現了 displayLayer: 方法,就能夠進入異步繪製的流程中。此時,什麼 drawInContext:drawLayer:InContext:drawRect: 都沒有出場機會的。

不過,進入異步繪製時,咱們須要負責建立對應位圖 bitmap,並將內容繪製在這個位圖中。繪製完成後,將這個 bitmap 設置爲 layer 的 contents。

其流程以下:

異步繪製流程

2.5 離屏渲染

什麼是離屏渲染?

  • On-Screen Rendering

在屏渲染:GPU 的渲染操做在當前用於顯示的幀緩衝區中進行的。

  • Off-Screen Rendering

離屏渲染:GPU 在當前用於顯示的幀緩衝區以外新開闢一個緩衝區進行渲染操做的。

正常狀況下,與雙緩衝機制 GPU 在當前用於顯示的幀緩衝區內渲染下一幀畫面。這這種狀況下渲染出來的畫面能夠直接顯示在屏幕上。而若是因爲咱們設置某些特殊的 UI 視圖屬性,從而觸發了在預合成以前沒法用於直接顯示的指令,就會觸發離屏渲染來預處理這部份內容。

由於須要進行預處理操做來預合成這部分沒法直接用於顯示的內容,GPU 須要先開闢一塊另外的緩衝區,並將渲染上下文 Rendering Context 切換到這塊區域。而後 GPU 就觸發 OpenGL 多通道渲染管線來進行這部份內容的預合成操做,執行完成以後再將上下文切換回本來用於顯示的緩衝區。

總結下來:

  1. 建立一塊緩衝區:GPU 沒法在某一個 layer 渲染完成以後,再回過頭來改變其中的某個部分——這一 layer 以前的若干 layer 像素數據已經在渲染過程被永久覆蓋了。對於一個 layer,除非能找到一種經過單次遍歷就能完成渲染的方法,不然只能另開一片內存來完成屢次的修改操做;

  2. 切換渲染上下文:從用於屏幕顯示的緩衝區切換到剛剛建立的緩衝區;

  3. 預合成:觸發 OpenGL 多通道管線執行須要預合成內容的操做;

  4. 切換渲染上下文:將預合成的結果拷貝至屏幕緩衝區,從建立的緩衝區切換回用於屏幕顯示的緩衝區;

這一系列操做會增長 GPU 的工做量,尤爲是切換上下文(必須刷新其渲染管線和屏障)。因此,在平常開發中,應儘可能避免離屏渲染。

  • CPU 離屏渲染:特殊的「離屏渲染」

若是咱們在 UIView 中實現了 drawRect: 方法,就算其函數體內沒有實際代碼,系統依然會爲這個 view 申請一塊內存區域和一個圖像上下文,等待 Core Graphics 可能的繪畫操做。這也就是上邊所說的 backing store 和 CGContextRef。

由於不是直接把繪製結果放進用於顯示的緩衝區中,而是在其餘地方執行這些操做。全部 CPU 進行的光柵化操做(如文字渲染、圖片解碼),都沒法直接繪製到由 GPU 管理的幀緩衝區中,只能暫時存放在別的內存區域,因此也稱爲 「離屏渲染」。

可是,根據 Apple 工程師的說法 這並非真正的離屏渲染。除此以外,還有一個證據:若是咱們在 UIView 中實現了 drawRect: ,不管是 Xcode -> Debug -> View Debugging -> Rendering -> Color Offscreen-Rendered Yellow 仍是 Simulator -> Debug -> Color off-screen Rendered 都沒有把這部分標記爲黃色。

有趣的是, UINavigationBarUITabBar 、 輔助觸摸 、App 切換器 都是 黃色的。。。就不截圖了,有興趣的朋友本身玩玩哈

離屏渲染到底哪裏很差?

2014 WWDC - Advanced Graphics and Animations for iOS Apps 中,Apple 以 UIVisualEffectView 爲例描述了 GPU 的處理邏輯,這裏有 5 個 Rendering Pass。上邊的藍色爲 Tiler 操做的時間分佈,紅色爲 Renderer 操做。

Tiler 是什麼?看這個,Apple 也描述了 Core Animation 的渲染機制:

GPU 大部分時間都花在 Renderer 操做上,其中最後一個 Rendering Pass 爲在屏渲染,也就是說 UIVisualEffectView 存在四個離屏渲染的 Rendering Pass。

Rendering Pass 之間存在黃色的豎條,它叫無用時間 Idle Time,是上下文轉換 Context Switch 的時間。一個 Context Switch 大概會佔用 0.1ms - 0.2ms,UIVisualEffectView 有四次 Context Switch,因此其全部 Rendering Pass 會積累 0.4ms - 0.8ms 的 Idle Time。看起來不多,可是每一幀的繪製時間只有 1000 / 60 = 16.67ms。

總結下來,離屏渲染很差的地方在於:

  1. 須要更多的 Rendering Pass,加大 GPU 的工做量;

  2. Rendering Pass 之間須要 Context Switch,致使存在很多的 Idle Time。

哪些操做會觸發離屏渲染?

本節所有操做都已開啓 Simulator -> Debug -> Color Off-screen Rendered。

前菜:clipsToBounds 與 masksToBounds

clipsToBounds 是 UIView 的屬性:subview 是否才叫到這個 view 的邊界。

masksToBounds 是 CALayer 的屬性:sublayer 是否裁減到這個 layer 的邊界。

其實這兩個屬性的做用是同樣。前邊咱們說到,UIView 封裝了 CALayer 的大部分屬性,而 clipsToBounds 也是從 masksToBounds 獲得的一個數據。

在設置 view 的 clipsToBounds 時,真正設置的就是 layer 的 masksToBounds 。

圓角 cornerRadius(> 0) + masksToBounds(YES)

先看一段 Apple 官網文檔對 cornerRadius 的描述:

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background.

設置正數半徑將使 layer 在其背景中繪製圓角。

By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer.

默認狀況下,cornerRadius 僅僅做用於 layer 背景顏色和邊框,不會做用於 layer.contents 上的圖像。

However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

然而,masksToBounds = YES 會致使整個 contents 被裁剪爲圓角。

也就是說,單單設置 cornerRadius 只能影響 背景顏色 backgroundColor邊框 border

for (int i = 0; i < theArray.count; ++i) {
    CGFloat viewX = hMargin + (viewWidth + hMargin) * i;
    CGFloat viewY = 80;
    
    /// 第一行
    UIView *view0 = [[UIView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
    [self.view addSubview:view0];
    view0.backgroundColor = [UIColor grayColor];
    view0.layer.cornerRadius = LyGetHeight(view0) * 0.5;
    view0.layer.borderColor = [UIColor blackColor].CGColor;
    view0.layer.borderWidth = 2;
}
複製代碼

  • 結論一: 單純設置 cornerRadius 並不會觸發離屏渲染。

接下來,咱們將左邊的 view 設置爲 masksToBounds = YES

view0.layer.masksToBounds = (0 == i);
複製代碼

這裏也沒有觸發離屏渲染,這貌似有悖 cornerRadius + masksToBounds 會觸發離屏渲染

那咱們給這個 view 加一個 subview 試試:

UIView *subview0 = [[UIView alloc] initWithFrame:CGRectMake(20, 0, viewWidth + 40, 80)];
subview0.backgroundColor = [UIColor redColor];
[view0 addSubview:subview0];
複製代碼

UIView with subview

  • 結論二:masksToBounds 不會觸發離屏渲染。

針對結論二的解決方案就是:不設置 masksToBounds = YES,大多數 UIView 此屬性默認值爲 NO(UITextView 爲 YES,爲了保險能夠顯式設置)。

經常使用控件之 UILabel(此時咱們須要使用設置 label.layer.backgroundColor 來代替 label.backgroundColor):

/// 第二行  /// UILabel
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
[self.view addSubview:label1];
label1.text = theArray[i];
label1.textAlignment = NSTextAlignmentCenter;
label1.layer.cornerRadius = LyGetHeight(label1) * 0.5;
if (0 == i) {
    label1.backgroundColor = [UIColor grayColor];
} else {
    label1.layer.backgroundColor = [UIColor grayColor].CGColor;
}
複製代碼

UILabel

經常使用控件之 UITextView:

/// 第三行  /// UITextView
UITextView *textView2 = [[UITextView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
[self.view addSubview:textView2];
[textView2 setText: @"我是 UITextView"];
textView2.backgroundColor = [UIColor grayColor];
textView2.layer.cornerRadius = LyGetHeight(textView2) * 0.5;
if (1 == i) textView2.layer.masksToBounds = NO;
複製代碼

UITextView

經常使用控件之 UIImageView:

/// 第四行  /// UIImageView
UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[self.view addSubview:imageView3];
if (0 == i) {
    imageView3.image = [UIImage imageNamed:@"vortex.jpeg"];
    imageView3.layer.cornerRadius = LyGetHeight(imageView3) * 0.5;
    imageView3.layer.masksToBounds = YES;
} else {
    imageView3.image = [[UIImage imageNamed:@"vortex.jpeg"] drawCornerInRect:imageView3.bounds cornerRadius:LyGetWidth(imageView3) * 0.5];
}
複製代碼

解釋:iOS 9.0 以後 UIImageView 設置圓角不會觸發離屏渲染,但若是是陰影依然會觸發。

經常使用控件之 UIButton:

/// 第五行 /// UIButton 設置圓角圖片
UIButton *button4 = [[UIButton alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[self.view addSubview:button4];
if (0 == i) {
    [button4 setImage:[UIImage imageNamed:@"vortex.jpeg"] forState:UIControlStateNormal];
    button4.layer.cornerRadius = LyGetHeight(button4) * 0.5;
    button4.layer.masksToBounds = YES;
} else {
    [button4 setImage:[[UIImage imageNamed:@"vortex.jpeg"] drawCornerInRect:button4.bounds cornerRadius:LyGetHeight(button4) * 0.5]
             forState:UIControlStateNormal];
}
複製代碼

  • 結論三:存在 subview 的 UIView,設置 cornerRadius(> 0) + masksToBounds(YES) 會觸發離屏渲染。
陰影 shadow

設置陰影時,注意必定更要設置 陰影透明度 shadowOpacity 這個屬性,其默認值爲 0。

要想設置陰影致使的避免離屏渲染,只須要在正常設置陰影以後爲該 layer 設置一個由貝塞爾曲線 UIBezierPath 生成的 shadowPath 便可。

/// 第六行 /// 陰影 shadow
UIImageView *imageView5 = [[UIImageView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[self.view addSubview:imageView5];
imageView5.image = [UIImage imageNamed:@"vortex.jpeg"];
imageView5.layer.shadowColor = [UIColor blackColor].CGColor;
imageView5.layer.shadowOffset = CGSizeMake(5, 5);
imageView5.layer.shadowRadius = 5;
imageView5.layer.shadowOpacity = 0.8;
if (1 == i) {
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView5.bounds];
    imageView5.layer.shadowPath = path.CGPath;
}
複製代碼

  • shadow 會觸發離屏渲染,除非設置 shadowPath
遮罩 mask

先看下 mask 繪製的流程:

Mask 繪製流程

一共有三步,對應三個 Rendering Pass。最後的 Compisiting pass 輸出到最後的幀緩存,是在屏渲染。而前面的 pass1 和 pass2 是繪製到紋理 texture 供最後的 Compisiting pass 所用,即離屏渲染。

遮罩最經常使用的就是 部分圓角 了。部分圓角的原理即是貝塞爾曲線:

咱們來改寫一下 左邊的 label1:

/// 第二行  /// UILabel 部分圓角
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
[scrollView addSubview:label1];
label1.text = theArray[i];
label1.textAlignment = NSTextAlignmentCenter;
label1.layer.cornerRadius = LyGetHeight(label1) * 0.5;
if (0 == i) {
    label1.backgroundColor = [UIColor grayColor];
    
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:label1.bounds
                                               byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
                                                     cornerRadii:CGSizeMake(20, 20)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.path = path.CGPath;
    label1.layer.mask = maskLayer;
    
} else {
    label1.layer.backgroundColor = [UIColor grayColor].CGColor;
}
複製代碼

經過 mask,雖然實現了圓角,卻觸發了離屏渲染,有點不值當。

其實 masksToBounds 也是經過 mask 來實現的。

  • mask 會觸發離屏渲染
組透明 allowsGroupOpacity(YES) + opacity(< 1)

When the value is true and the layer’s opacity property value is less than 1.0, the layer is allowed to composite itself as a group separate from its parent.

若 allowsGroupOpacity 爲 true 且這個 layer 的 opacity 小於 1,這個 layer 會被容許從 superview 獨立出來成一個組。

This gives correct results when the layer contains multiple opaque components, but may reduce performance.

若這個 layer 包含多個不透明的組件,這種狀況會表現的比較完美,可是會影響性能。

The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is true for apps linked against the iOS 7 SDK or later and false for apps linked against an earlier SDK.

默認值從 info.plist 讀取 UIViewGroupOpacity ,若不存在則爲 YES。後邊的不翻譯了,誰如今還從 iOS 6 開始支持!!!!!

說人話,當 allowsGrounOpacity 爲 true 時,layer 將從 superlayer 繼承其 opacity 值,不過這裏繼承的是最大的 opacity。當 layer 能夠設置比 superlayer 更低的值,但沒法超過 superlayer。而且獨立出來,怎麼獨立呢?

/// 第七行 /// 組透明 allowsGroupOpacity
UIView *view6 = [[UIView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 60)];
[scrollView addSubview:view6];
view6.backgroundColor = [UIColor grayColor];
view6.layer.opacity = 0.5;
view6.layer.allowsGroupOpacity = (0 == i);

UIView *subview6 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 100)];
[view6 addSubview:subview6];
subview6.backgroundColor = [UIColor redColor];
複製代碼

就是這個獨立法!!!可是會觸發離屏渲染:

測試的時候,注意 superview 自己必定要有點內容,至少設置個背景色。

  • 總結下來:allowsGroupOpacity 觸發離屏渲染的條件是 allowsGroupOpacity(YES) + opacity(< 1) + 存在 sublayer 或 背景圖
抗鋸齒 allowsEdgeAntialiasing(YES) (貌似已優化)

allowsEdgeAntialiasing 決定 layer 是否容許執行反鋸齒。默認值從 info.plist -> UIViewEdgeAntialiasing 讀取,不存在則爲 false。

edgeAntialiasingMask 決定 layer 如何反鋸齒(left、right、top、bottom)。默認值爲全部邊界。

/// 第八行 /// 反鋸齒 allowsEdgeAntialiasing / edgeAntialiasingMask
UIImageView *imageView7 = [[UIImageView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[scrollView addSubview:imageView7];
imageView7.image = [UIImage imageNamed:@"vortex.jpeg"];

CATransform3D trans = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
trans = CATransform3DScale(trans, 1.5, 1.5, 1);
imageView7.layer.transform = trans;

if (0 == i) {
    imageView7.layer.allowsEdgeAntialiasing = YES;
    imageView7.layer.edgeAntialiasingMask = kCALayerLeftEdge | kCALayerRightEdge;
}
複製代碼

經測試,開啓 allowsEdgeAntialiasing 並 layer 並不會觸發離屏渲染,或許已經優化。(經查閱資料,發現此操做從 iOS 8 已經不會觸發離屏渲染了)

毛玻璃 UIBlurEffect
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
blurEffectView.frame = CGRectMake(viewX, viewY, viewWidth, viewWidth);
[scrollView addSubview:blurEffectView];
複製代碼

仍是添加了一個 effect 爲 UIBlurEffectUIVisualEffectView 而已,其餘啥也沒作,就黃了!!!

柵格化 shouldRasterize(YES)

shouldRasterize 決定 layer 在組合以前是否渲染成一個位圖紋理。

當設置 shouldRasterize = YES 時,layer 將會在自身座標空間被渲染成一個紋理位圖,而後才與其餘的內容組合到最終結果。陰影效果和任何 濾鏡 都會被柵格化幷包含在這個紋理位圖中。

當值被設置爲 false 時,只要能夠,layer 都會被直接混合到最終結果中。可是若是某些合成模型須要,layer 仍是可能被提早柵格化,好比濾鏡。

默認值爲 false。

Rasterization

根據這張圖多說一點:

  1. 使用 GPU 一次性混合成圖像;

  2. 提早渲染的這個紋理位圖會被緩存起來,可是超過 100ms 不適用就會被釋放;

  3. 更新內容是會發生額外的離屏渲染流程;

  4. 不要過分使用,緩存大小爲 2.5 倍的屏幕尺寸。

/// 第九行  /// shouldRasterize
UIView *view8 = [[UIView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 60)];
[scrollView addSubview:view8];
[view8 setBackgroundColor:[UIColor grayColor]];
view8.layer.shouldRasterize = (0 == i);
複製代碼

離屏渲染爲何要存在?

先上結論:正確使用離屏渲染能夠優化性能。

從上一張圖提兩句話出來:1. 這個紋理位圖會被緩存起來,更新內容纔會重繪。這句話意味着:只要咱們不更新內容,那就能夠直接使用這份緩存來顯示。

那麼,對於某些靜態內容,咱們徹底能夠設置 shouldRasterize = YES。好比 UITableViewCell 的陰影效果與部分圓角效果。

如下內容來自於 從OpenGL再說離屏渲染

咱們都知道,GPU 是專門爲圖形而生的,它很是適合作簡單運算,作大量重複的工做。對應 Tiler 中的頂點運算,Renderer 中的混合着色等都很適合在 GPU 上並行運算。GPU 一次只能繪製簡單的圖元 Primitives,對應到 OpenGL 中就是 點 GL_POINTS、線 GL_LINES、三角形 GL_TRIANGLES

全部複雜圖形都是一個個三角形組成的,普通 Layer 由兩個三角形組成,GPU 只須要一個 Rendering Pass 就能完成繪製。可是 mask 效果 是將一個 layer 做爲 「形狀」 來繪製另外一個 layer,這種 「形狀」 是沒法經過點、線、三角形這些基本圖元來描述,所以 mask 效果沒法用 GPU 一次性繪製出來,只能經過多步組合繪製出來。因此 mask 的繪製流程分爲三步。

Rasterization

仍是這張圖,Rasterization 會使用 GPU 將多個 Layer 繪製到一個紋理位圖中,而且這個紋理位圖會被緩存起來,以便後續直接使用緩存進行渲染。

在 Rendering 階段,由一個操做叫 顏色混合 ,對應到每個像素點,繪製時取 renderBuffer 中的原有顏色與當前顏色按照指定公式計算顏色值,其 OpenGL 代碼爲:

glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
複製代碼

這個操做在 GPU 是比較耗時的。若是 CALayer 的樹結構比較複雜,數量大,GPU 每一幀都須要混合全部的 layer,這回消耗 GPU 的大量性能。

而 Rasterization 將剛說的這個操做渲染成一張紋理位圖並緩存起來,下次渲染直接使用緩存,從而避免 GPU 的無用消耗。可是這個操做會增長內存的消耗,記得僅使用在靜態場景。

參考連接

AutoLayout 的原理性能

《Solving Linear Arithmetic Constraints for User Interface Applications》

Cassowary 網站

Cassowary

Cassowary - Python

單純算法 Simplex

Masonry

SnapKit

從 Auto Layout 的佈局算法談性能

深刻理解 Autolayout 與列表性能 -- 背鍋的 Cassowary 和偷懶的 CPU

WWDC 2018:高性能 Auto Layout

Apple 官方教程 VFL

WWDC 2018 - High Performance Auto Layout

Auto Layout Guide

How do you set UILayoutPriority? - stack overflow

IOS開發之自動佈局--VFL語言

iOS Auto Layout 中的對齊選項

Apple 官方教程 VFL

UIStackView學習分享, 純代碼實現

自動佈局 Auto Layout (原理篇)

詳解CALayer 和 UIView的區別和聯繫

View-Layer 協做

View-Layer Synergy

繪製像素到屏幕上【這篇文章推薦對照英文原版查看,就是下一個】

Getting Pixels onto the Screen

iOS 保持界面流暢的技巧

iOS——圖像顯示原理以及UI流暢性優化方案

Core Animation Programming Guide

深刻理解 iOS Rendering Process

iOS 渲染框架

iOS - 渲染原理

關於iOS離屏渲染的深刻研究

iOS-高效設置圓角

How to make a UIView's subviews' alpha change according to it's parent's alpha?

Information Property List Key Reference

Advanced Graphics and Animations for iOS Apps

Advanced Graphics and Animations for iOS Apps.md

iOS-圖片高級處理(2、圖片的編碼解碼)

iOS - 圖形高級處理 (1、圖片顯示相關理論)

iOS-圖片高級處理(3、圖片處理實踐)

談談 iOS 中圖片的解壓縮

Which CGImageAlphaInfo should we use?

Quartz 2D Programming Guide

iOS-圖片高級處理(2、圖片的編碼解碼)

iOS 圖片解碼

iOS圖片內存優化

iOS圖片加載過程以及優化

iOS 圖片加載速度優化

iOS 圖片渲染及優化

相關文章
相關標籤/搜索