iOS UI繪製原理

劉哈發表於2019-03-04

高質量的圖形展示在app的互動介面中扮演非常重要的角色。高質量的圖形展示讓使用者更能喜歡使用它。iOS系統主要提供兩種途徑去建立高質量的圖形:OpenGL或者使用原生Quarts、Core Animation和UIKit。本文會展開講一下後者。

Quartz是主要的繪製途徑,它提供了基於路徑繪製、抗鋸齒繪製、漸變色、圖形繪製、顏色、變形和PDF文件的建立展示和解析能力。UIKit是對Quartz的線條、圖片和顏色操作的封裝。Core Animation提供了對在動畫中修改UIView屬性的的支援,同時還可以實現自定義動畫。

這個章節會講述iOS App中的渲染過程,並說明其中用到的繪製原理。可以幫你學習到一些可以讓你自己app優化渲染的小技巧。

重要:並非所有UIKit的類都不是執行緒安全的。請確認在執行繪製的時候處於主執行緒。

UIKit 圖形系統

在iOS中,無論使用哪種技術(OpenGL、Quartz、UIKit或者Core Animation)繪製在UIView或者子類中,都會在螢幕上展示。檢視定義自己在螢幕上如何繪製,或者說如何展現自己。系統提供的檢視會自動定義自己的展現。自定義檢視你必須定義檢視如何展現。本章就會講解通過Quartz、Core Animation和UIKit去繪製自定義圖形。

另外,可以離屏繪製點陣圖和PDF圖形上下文(譯者吐槽:是不是意味著這部分可以不在主執行緒繪製?)。當你離屏繪製時,因為沒有在檢視上進行繪製,所以檢視的生命週期也對此不起作用。

檢視生命週期

UIView的子類包含了最基本的圖形繪製模型 -- 根據需要更新繪製自身。UIView類通過批量更新繪製以及在合適的時機更新繪製自身來做到讓更新自身繪製變得簡單和高效。

當一個檢視第一次或者某部分需要更新的時候iOS系統總是會去請求drawRect:方法。

以下是觸發檢視更新的一些操作:

  • 移動或刪除檢視
  • 通過將檢視的hidden屬性設定為NO
  • 滾動消失的檢視再次需要出現在螢幕上
  • 檢視顯式呼叫setNeedsDisplaysetNeedsDisplayInRect:方法

檢視系統都會自動觸發重新繪製。對於自定義檢視,就必須重寫drawRect:方法去執行所有繪製。在方法中通過原生繪製API來繪製本身的形狀、文字、圖片、漸變或者任何你希望展示的部分。檢視第一次展示的時候,iOS系統會傳遞正方形區域來表示這個檢視繪製的區域,為了最大程度優化效能,重繪的時候最好只重繪受影響的部分

在呼叫drawRect:方法之後,檢視就會把自己標記為已更新,然後等待下一次檢視更新被觸發。靜態自定義檢視需要處理因為滾動而出現或者因為其他檢視出現而引起的檢視變化。(譯者吐槽:後半句啥意思?沒太懂)

如果想要改變檢視的內容,就必須觸發檢視重繪內容。通過呼叫setNeedsDisplay或者setNeedsDisplayInRect:方法來觸發更新。使用場景例如一秒內多次更新檢視,或者根據使用者的互動行為在檢視中出現新的內容。

重要:不要顯式呼叫drawRect:方法。這個方法應該只留給iOS需要重新繪製的時候留給系統呼叫。因為在其他時機圖形上下文是不存在的,所以也不能對螢幕進行繪製。(圖形上下文下一個小節會說明。)

座標系統及iOS中的繪製

當App需要在iOS系統中繪製圖形時,它必須繪製在一個二維座標系中。這看上去很簡單,但在某些繪製情況下需要處理另一種不同的座標系。

iOS的圖形繪製需要依靠圖形上下文來完成。理論上說,圖形上下文是用來描述在哪裡、如何畫上,包括顏色繪製、切割繪製區域、線條粗細和樣式等資訊。

另外,圖1-1中展示了每個圖形上下文都有一個座標系。更準確的說,每個圖形上下都三個座標系:

  • 繪製座標系。繪圖上下文通過使用指令繪製的座標。
  • 檢視座標系。相對於檢視的固定座標系。
  • 裝置座標系。物理螢幕的畫素展示座標。

圖1-1 繪製座標、檢視座標以及硬體座標關係

圖1-1

譯者注:CTM是Quartz中的一個概念,下面會介紹。

iOS的繪圖框架為繪製特定的目標(螢幕、點陣圖、PDF內容等)建立圖形上下文,這些圖形上下文為該目的地建立初始繪圖座標系。這個初始的座標系被稱為預設座標系,是1比1對映到檢視座標系上的。(譯者吐槽:後半句沒懂)

每個檢視都有一個自己的current transformation matrix(CTM),一個數字舉證對映當前繪製座標系到檢視座標系。App可以修改矩陣來影響後面發生的繪製操作。

iOS會在預設座標系的基礎上建立圖形上下文。在iOS中主要是兩種:

  • 左上原點座標系(ULO),從左上角為0,0座標,向右向下為正,UIKit和Core Animation都是基於ULO。
  • 左下原點座標系(LLO),從左下角為0,0座標,向右向上為正,Core Graphics是基於LLO。

兩種座標系展示如圖1-2

圖1-2iOS中預設座標系

圖1-2

提示:MacOS預設使用的是LLO。通過AppKit和CoreGraphics繪製都是基於此座標系,AppKit提供了左上原點座標系的轉換支援。

點和畫素

iOS系統中指定的座標系和底層裝置繪製畫素的之間有區別。當使用原生繪製列如Quartz,UIKit和Core Animation,繪製座標西和試圖座標系都是邏輯座標系(譯者注:這裡說的邏輯座標系也就是指的不與裝置畫素點對應),座標系數值表示的是,與裝置上的畫素並沒有一一對應的關係。

系統會自動根據檢視的點座標值去對映到裝置的畫素上,但並不一定是一對一對映,這點非常重要。

一個點不一定對映到物理的一個畫素。

使用點代替的畫素的主要目的還是為了讓檢視在裝置上呈現出合適的尺寸,不會因為螢幕畫素變高導致原本檢視變得很小。具體多少畫素對應一個點,是由系統根據當前裝置硬體來決定的。例如,在視網膜螢幕上,一條線的繪製對應像個畫素的線條寬度。這種對映關係讓普通屏視網膜屏和更高解析度的屏上展示檢視的大小基本保持一致。

提示:Core Graphics中渲染和列印PDF的時候一個點點對應1/72英寸。

在iOS中,UIScreen,UIView,UIImage和CALayer都提供屬性用於描述畫素和點之間的對映比例。例如,UIKit的View的contentScaleFactor屬性。在非視網膜屏中,該屬性值為1.0。在視網膜屏中,為2.0(譯者注:除了plus系列後應該還有3.0)。在未來也可能出現其他的值。(在iOS4之前一直都是1.0)。

因為自動對映的關係,在繪製檢視時通暢不需要關心畫素。只有在下載高解析度圖片在視網膜螢幕上展示的時候,需要關心圖片渲染的scale,避免高分圖被低分渲染而變大的問題。

在iOS中,當你在螢幕上繪製東西時,圖形子系統使用一種叫做反鋸齒的技術,在低解析度的螢幕上近似一個高解析度的影象。用一個例子來解釋下。繪製一條黑色的豎線在白色背景上,如果線正好落在畫素上,展現出來就如下圖左邊那樣是一系列黑色畫素排列。如果正好落在兩個畫素上,那就會出現灰色畫素繪製兩格如下圖1-3右側。

圖1-3

iOS UI繪製原理

整數值的點座標會落在兩個畫素的中間。例如,畫一條1個畫素寬度的直線(1,1)到(1,10),得到的是一條灰色的先。如果畫兩個畫素寬度的線,才會得到一條黑色的實線,因為兩個畫素正好落滿兩個畫素。一般來說,如果不調整它們的位置,使它們完全覆蓋畫素,那麼與寬度為偶數的物理畫素的寬度相比,奇數個物理畫素寬的線顯得更淺。

scale屬性就是為了表示一個點對映了多少畫素。

在非視網膜屏上scale永遠為1.0,一個點對應一個畫素。為了避免反鋸齒,當你繪製一個單點線時,如果佔了奇數整數寬度,那就需要偏移0.5個點,如果佔用偶數寬度則不必這麼做。

圖1-4 一個點寬度的線在非視網膜和視網膜屏上的展示

iOS UI繪製原理

在視網膜的scale為2.0,一點線也不會觸發抗鋸齒,因為本身就會撐滿兩個畫素。如果要畫一條一個畫素的線,就需要使用0.5個點的寬度並且偏移0.25個點。

直接按照scale去控制畫素繪製並不能得到最好的體驗。一個一畫素寬的線在非視網膜螢幕上看起來可能沒問題,但如果在視網膜螢幕上看起來就會覺得太細了。這取決於你如何去繪製。

獲取影象上下文

影象上下文可以在drawRect:方法中獲取到,並且立刻進行繪製。UIView為影象上下文提供了繪製的環境。

如果您想在檢視以外的地方繪製(例如,在一個PDF或點陣圖檔案中捕獲一系列繪圖操作),或者如果您需要呼叫需要上下文物件的核心圖形函式,那麼您必須採取額外的步驟來獲取圖形上下文物件。下面的章節解釋了為什麼。

更多關於修改影象上下文狀態和建立定製內容請參考 [ Quartz 2D Programming Guide ]。影象上下文的方法清單請參考 [ CGContext Reference ], [ CGBitmapContext Reference ], [ CGPDFContext Reference ]

在螢幕上繪製

想要在螢幕上繪製,就需要在drawRect:方法中獲取到影象上下文。(這一系列方法中的第一個引數都是一個CGContextRef物件。)可以通過呼叫UIGraphicsGetCurrtnContext方法,在drawRect:方法中獲取一個圖形上下文。(多次獲取也會得到同一個。)

在UIKit的view中,使用Core Graphics系列方法來繪製是基於ULO座標系的。或者,翻轉CTM來使用LLO座標系來繪製。詳細的請閱讀 [ Flipping the Default Coordinate System ]

UIGraphicsGetCurrentContext函式始終返回的是當前的上下文。例如,在建立PDF後獲取的上下文就是PDF上下文。只要是使用Core Graphics系列函式繪製,都必須使用這個方法來獲取上下文。

提示: 列印相關的函式放在了UIPrintPageRender類中。類似於drawRect:,UIKit在其中提供了列印相關的實現。並且預設也是基於ULO座標系。

繪製點陣圖和PDF

UIKit提供了繪製點陣圖和PDF的上下文和系列函式。兩種建立方式都需要分別呼叫一個函式來建立其對應的上下文。在通過上下文進行繪製,並在繪製完成後關閉上下文。

兩種上下文也是基於ULO座標系。Core Graphics提供了一系列方法用於在在點陣圖上下文中徐然和在PDF上下文中繪製。上下文從Core Graphics中直接呼叫函式獲得,並且基於LLO座標系繪製。

**提示:**在iOS中還是推薦使用UIKit的相關函式來獲取上下文來繪製。如果非要使用Core Graphics中的相關方法來繪製,則需要對座標系的差異做相容。(譯者:所以UIKit中的上下文相關方法其實是轉換了座標系的Core Graphics方法。)

詳細可以參考 建立繪製點陣圖建立PDF

顏色和色域

儘管Quartz在iOS系統中支援全色域;但是幾乎所有app中都只用到了RGB色域。畢竟iOS被設計在螢幕上繪製渲染,而RGB是最合適的。

UIColor物件通過提供一系列便捷方法通過RGB/HSB和灰度色值來建立顏色,且不需要關心色域問題,而由UIColor物件自動決定。

也可以使用Core Graphics框架中的CGContextSetRGBStrokeColorCGContextSetRGBFillColor函式來建立社設定顏色。儘管Core Graphics提供了可以指定色域和建立自定義色域的函式,但是並不推薦在程式碼中使用。(譯者:為啥不推薦?原因呢?)還是推薦始終使用RGB色域即可。

使用Quartz和UIKit繪製

我們把iOS中的繪圖技術統稱為Quartz。而Core Graphics框架則是Quartz心臟,並承擔大多數繪製內容的職責。框架提供了資料型別和函式支援以下能力:

  • 圖形上下文
  • 路徑
  • 圖片和點陣圖
  • 透明圖層
  • 顏色和色域
  • 漸變和陰影
  • 字型
  • PDF

UIKit在Quartz基礎上提供了一套圖形操作相關的類。目的並不是為了替代Core Graphics,相反,他們是為了給UIKit的其他類提供繪畫支援:

  • UIImage/UIColor/UIFont/UIScreen/UIBezierPath
  • 生成一個JPEG或PNG的圖片物件的函式
  • 獲取點陣圖上下文的函式
  • 獲取PDF上下文的函式
  • 繪製矩形和裁剪繪圖區域的函式
  • 獲取當前圖形上下文的函式

更多資訊參考,UIKit Framework Reference,還有 Core Graphics Reference

配置圖形上下文

在呼叫 drawRect: 方法之前,檢視物件已經建立了一個圖形上下文並且將其置為當前的上下文。它只存在於drawRect:方法呼叫期間。可以通過呼叫UIGraphicsGetCurrentContext函式來獲取圖形上下文的一個引用。方法返回一個CGContextRef型別物件的引用,該物件傳遞了Core Graphics函式修改當前圖形的狀態。表1-1列出了主要的幾個方法。需要檢視完整的請移步 CGContext Reference。下表還列出了UIKit替代方法。

表1-1 修改圖形狀態的Core Graphics方法

狀態 函式名 UIKit替代方法
Current transformation matrix (CTM) CGContextRotateCTM/CGContextScaleCTM/CGContextTranslateCTM/CGContextConcatCTM None
Clipping area CGContextClipToRect UIRectClip function
Line: Width, join, cap, dash, miter limit CGContextSetLineWidth/CGContextSetLineJoin/CGContextSetLineCap/CGContextSetLineDash/CGContextSetMiterLimit None
Accuracy of curve estimation CGContextSetFlatness None
Anti-aliasing setting CGContextSetAllowsAntialiasing None
Color: Fill and stroke settings CGContextSetRGBFillColor/CGContextSetRGBStrokeColor UIColor class
Alpha global value (transparency) CGContextSetAlpha None
Rendering intent CGContextSetRenderingIntent None
Color space: Fill and stroke settings CGContextSetFillColorSpace/CGContextSetStrokeColorSpace UIColor class
Text: Font, font size, character spacing, text drawing mode CGContextSetFont/CGContextSetFontSize/CGContextSetCharacterSpacing UIFont class
Blend mode CGContextSetBlendMode The UIImage class and various drawing functions let you specify which blend mode to use.

上下文中以堆疊形式儲存了圖形的裝填。上下文被Quartz建立時堆疊是空的。通過呼叫CGContextSaveGState函式將當前圖形狀態推入堆疊。此後圖形狀態的改變會影響後續的繪製操作,但不會影響之前已如堆疊的。當完成修改後可以通過呼叫CGContextRestoreGState函式來將其中堆疊中彈出。這種推入和彈出操作替代了逐個撤銷每個狀態的操作。這也是唯一能還原到之前狀態的方法。

更多資訊參考 Graphics ContextQuartz 2D

繪製路徑

路徑是一系列線和貝塞爾曲線組成的向量形狀。UIKit中包含了UIRectFrameUIRectFill等函式用於繪製簡單的路徑(類似矩形)。Core Graphics也提供了便捷函式用於繪製簡單路徑(例如矩形和橢圓)。

更多複雜路徑,就需要使用UIBezierPath類自己畫了,或者使用函式操作Core Graphics提供的CGPathRef。儘管可以脫離上下文繪製路徑,但最終底層還是使用了上下文,只是被封裝了。

--- 結束 ---

原文:iOS Drawing Concepts

延伸閱讀

CTM

相關文章