iOS 渲染原理總結

1. 圖像渲染流程


如圖所示,CPU 計算好顯示內(nèi)容提交到 GPU思犁,GPU 渲染完成后將渲染結果放入幀緩沖區(qū)代虾,隨后視頻控制器按照垂直同步信號逐行讀取幀緩沖區(qū)的數(shù)據(jù)进肯,經(jīng)過可能的數(shù)模轉換傳遞給顯示器顯示

2. 什么是離屏渲染


正常的渲染流程如圖所示激蹲,App 通過 CPU 與 GPU 合作,不停的將內(nèi)容渲染完成江掩,放入到 FrameBuffer 中学辱,顯示屏不斷的從 FrameBuffer 中讀取數(shù)據(jù),顯示到屏幕上



而離屏渲染則是先創(chuàng)建離屏渲染緩沖區(qū) OffscreenBuffer环形,將渲染好的內(nèi)容放入其中策泣,等到合適的時機再將 OffscreenBuffer 中的內(nèi)容進一步疊加、渲染抬吟,完成后才將內(nèi)容放入 FrameBuffer中萨咕。

3. 為什么要減少離屏渲染

  1. 離屏渲染需要 App 對內(nèi)容進行額外的渲染,并保存到 OffscreenBuffer 火本,
  2. 需要對 OffscreenBuffer 和 FrameBuffer 中的內(nèi)容進行切換(Buffer 切換的代價比較大)危队。
  3. OffscreenBuffer 本身需要額外的空間,大量的離屏渲染可能早晨過大的內(nèi)存壓力钙畔。

4. 離屏渲染的具體過程是怎樣的


圖層的疊加繪制茫陆,大體遵循“畫家算法”。如圖所示擎析,在這種算法下會按層繪制簿盅,先繪制距離較遠的場景,然后繪制距離較近的場景揍魂,覆蓋較遠的部分桨醋。因此在普通的layer繪制中,上層的 subLayer 會覆蓋下層 subLayer现斋,下層 subLayer 繪制完成后就可以拋棄了喜最,從而節(jié)約空間。所有的 subLayer 繪制完成后步责,整個繪制就完成了返顺,就放入 frameBuffer 準備呈現(xiàn)到顯示器上。

假設要繪制一個三層 layer蔓肯,不設置 cornerRadius 和 maskToBounds遂鹊,整個過程如圖所示

而當設置了 cornerRadius 和 maskToBounds = true 時,maskToBounds 會應用到所有的 subLayer 上蔗包。這也意味著所有的 subLayer 必須要重新應用一次圓角+裁切秉扑,因此所有的 subLayer 在第一次繪制結束后不能被丟棄,而必須保存在 OffscreenBuffer 中等待下一輪圓角+裁切,因此誘發(fā)了離屏渲染舟陆。

5. iOS 渲染具體過程


app 處理流程如下
app 本身不負責渲染误澳,渲染則是由一個獨立的進程負責,即 Render Server 進程秦躯。App 將渲染任務和數(shù)據(jù)提交給 Render Server忆谓,Render Server 處理完數(shù)據(jù)后再傳遞給 GPU,最后由 GPU 調用 iOS 的圖像設備進行顯示踱承。具體流程如下:
1. app 處理事件如用戶的點擊倡缠,在此過程中 app 可能需要更新時圖樹,相應的涂層樹也會更新茎活。
2. app 通過 CPU 完成對顯示內(nèi)容的計算昙沦,如視圖的創(chuàng)建、布局計算载荔、圖片解碼盾饮、文本繪制等。在完成對顯示內(nèi)容的計算后懒熙,app 對圖層進行打包丘损,并將其發(fā)送至 Render Server,即完成了一次 Commit Transaction 操作煌珊。
3. Render Server 執(zhí)行 OpenGL号俐、CoreGraphics 相關程序,并調用 GPU
4. GPU 則在物理層上完成對圖像的渲染定庵。
5. GPU 通過 frameBuffer 吏饿、視頻控制器等相關部件,將圖像顯示在屏幕上蔬浙。
上述步驟遠超過了 16.67ms猪落,因此為了滿足對屏幕 60FPS 的刷新率的支持,需要將這些步驟分解畴博,以流水線的形式笨忌,并行執(zhí)行,如下圖所示俱病。

其中官疲,app 調用 Render Server 前的最后一步 Commit Transaction 可以分為4個步驟:Layout、Display亮隙、Prepare途凫、Commit

  • Layout 階段:進行視圖構建,包括 LayoutSubviews 方法重載溢吻、addSubView 方法填充子視圖等

  • Display 階段:主要進行視圖繪制维费,這里的繪制是指設置最終要成像的圖元數(shù)據(jù)。重載視圖的 drawRect: 方法,可以自定義 UIView 的顯示犀盟,其原理是在 drawRect: 方法內(nèi)部繪制寄宿圖而晒,該過程使用 CPU 和內(nèi)存

  • Prepare 階段:屬于附加步驟,一般處理圖片的解碼與轉換操作

  • Commit 階段:主要將圖層打包阅畴,發(fā)送到 Render Server倡怎。該過程會遞歸進行,因為圖層是以樹形結構存在的

    GPU 渲染流程如下


    上圖是一個三角形被渲染的過程中恶阴,GPU 所負責的渲染流水線诈胜。
    GPU 獲取到的圖像信息稱為圖元豹障,通常是三角形冯事、頂點、線段等血公。主要分為以下幾個階段

    1. Geometry 幾何處理階段:包含頂點著色器昵仅、形狀裝配、集合著色器
      • 頂點著色器:這個階段會將圖元中的頂點信息進行視角轉換累魔、添加光照摔笤、增加紋理等操作
      • 形狀(圖元)裝配:根據(jù)上一步中的頂點位置,裝配成基本的圖元形狀垦写,例如三角形
      • 集合著色器:增加額外的頂點吕世,將原始圖元轉化成新的圖元,以構建一個不一樣的模型梯投∶剑總的來說就是基于三角形、線段分蓖、點構建更復雜的幾何圖形
    2. Rasterization 光柵化階段:圖元轉換為像素

      光柵化的目的是將集合渲染后的圖元信息轉換為一系列的像素尔艇,同時會裁掉屏幕顯示范圍之外的內(nèi)容,以便后續(xù)顯示在屏幕上么鹤。這個階段會根據(jù)圖元信息终娃,計算出每個圖元所覆蓋的像素信息,從而將像素劃分成不同的部分蒸甜。
      一種簡單的方法是根據(jù)像素的中心點棠耕。如果一個像素的中心點在圖元的內(nèi)部,那這個像素就屬于這個圖元柠新。如圖所示窍荧,深藍色的線是圖元信息構建出的三角形,而根據(jù)是否覆蓋中心點就可以遍歷出屬于該圖元的所有像素登颓,即淡藍色的部分搅荞。
    3. Pixel 像素處理階段:處理像素,得到位圖(bitmap)
      經(jīng)過了光柵話階段,得到了圖元對應的像素咕痛,此時需要給這些像素填充顏色和效果痢甘。所以最后這個階段就是給像素填充正確的內(nèi)容,最終顯示在屏幕上茉贡。這些經(jīng)過處理塞栅、包含大量信息的像素點稱為位圖(bigmap)。即 Pixel 階段輸出的就是位圖腔丧。具體如下
    • 片段著色器(Fragment Shader):這個階段的目的是給每一個像素賦予正確的顏色放椰,顏色的來源就是之前得到的頂點、紋理愉粤、光照之類的3D數(shù)據(jù)等信息砾医,由于需要處理紋理、光照等復雜信息衣厘,這通常是整個系統(tǒng)的性能瓶頸如蚜。
    • 測試與混合(Tests and Blending或Merging)階段:因為每個屏幕像素點上可能堆疊了多個顏色數(shù)據(jù),所以要根據(jù)深度值 z 坐標影暴、透明度 alpha 值错邦,從而進行片段的混合,得到最終的顏色型宙。

6. GPU 渲染流程反映到具體的 iOS 代碼上都有哪些步驟

  • 設置 layer 的類型為 CAEAGLLayer
  • 新建 EAGLContext 類型的上下文撬呢,并設置為當前的上下文
  • 清除掉之前的舊的 renderBuffer 和 frameBuffer
  • 創(chuàng)建新的 renderBuffer 和 frameBuffer
  • 分配 renderBuffer 的內(nèi)存空間,并綁定到 layer 上
  • 構造頂點著色器并編譯
  • 構造片段著色器并編譯
  • 構造著色器程序妆兑,并關聯(lián)頂點魂拦、片段著色器
  • 創(chuàng)建頂點對象,鏈接頂點屬性
  • 設置背景色
  • 清空顏色緩沖數(shù)據(jù)
  • 設置渲染窗口
  • 激活著色器程序
  • 關聯(lián)數(shù)據(jù)
  • 最終繪制

7. 為何要圖片解碼箭跳,直接顯示圖片有什么問題晨另?

圖片格式
  1. 位圖:位圖是一個像素數(shù)組,數(shù)組中的每個像素就代表著圖片中的一個點
  2. JPEG谱姓、PNG: 一種壓縮的位圖圖形格式借尿。其中 PNG 圖片是無損壓縮,并且支持 alpha 通道屉来,JPED 圖片是有損壓縮路翻,可以指定 0~100% 的壓縮比
解碼

通過網(wǎng)絡下載的圖片或者本地的圖片,都是 JPEG茄靠、PNG這些格式的壓縮圖片茂契。在將這些圖片渲染到屏幕之前,首先要得到圖片的原始像素數(shù)據(jù)慨绳,才能執(zhí)行后續(xù)的繪制操作
iOS 默認在主線程對圖像進行解碼掉冶,解壓縮后的圖片大小與原始文件大小無關真竖,只與圖片像素有關
解壓縮后的圖片大小 = 圖片的像素寬 * 圖片的像素高 * 每個像素所占的字節(jié)數(shù)

8. render server 具體是啥,負責什么工作厌小,為何需要單獨的 RenderServer 進程

iOS Render Server 是 OpenGL ES & Core Graphics恢共。Render Server 將與 GPU 通信把數(shù)據(jù)經(jīng)過處理之后傳遞給 GPU。主要為

  1. 負責解析圖層樹璧亚,反序列化為渲染樹:使用這個樹狀結構讨韭,渲染服務隊動畫的每一幀做出如下工作:對所有的圖層屬性計算中間值,設置 OpenGL 幾何形狀(紋理化的三角形)來執(zhí)行渲染癣蟋。
  2. 調用繪制指令透硝,并提交到 GPU
    這兩個階段在動畫過程中不停的重復。階段 1 在軟件層面通過CPU 處理疯搅,階段 2 被 GPU 執(zhí)行濒生。
圖層樹

屏幕上的可視內(nèi)容倍分解成為獨立的圖層(CALayer),這些圖層被存儲在一個叫做圖層樹的體系中秉撇。

App 并不是完全占有 Screen甜攀,還有狀態(tài)欄、通知欄琐馆、上拉菜單等,需要一個統(tǒng)一的 Server 來負責

9. layer 為啥可以顯示內(nèi)容恒序?layer 的backing store 與 frameBuffer(幀緩存或顯存瘦麸、顯示存儲器) 都是啥,里面存的都是位圖歧胁?關系如何滋饲?

CALayer 包含一個 contents 屬性指向一塊緩存區(qū),稱為 backing store喊巍,可以存放位圖屠缭。iOS 中將該緩存區(qū)保存的圖片稱為寄宿圖(CGImageRef)
GPU 包含著一部分顯存空間(VRAM),在設備啟動的時候崭参,作為 PCI 設備的 GPU呵曹,其顯存空間中的一部分地址,會被映射到 PCI 地址中何暮,然后再把 PCI 總線上的地址映射到 CPU 地址中奄喂。這樣 CPU 就能通過對這段映射后的地址的訪問,訪問到 GPU 的存儲空間海洼。此外還可以通過內(nèi)存空間進行數(shù)據(jù)交互跨新。首先系統(tǒng)為 GPU 動態(tài)分配一些不連續(xù)的空間(GTT),用于映射到 GPU 顯存空間中坏逢。然后 CPU 通過對這段空間的訪問域帐,進行對 GPU 顯存空間的訪問赘被。對于 GPU 指令來說,則是直接由 CPU 通過 PCI 總線推送到 GPU 中肖揣,或是由 GPU 自己從指令流中獲取指令帘腹。
layer 的backing store和 frameBuffer 存儲的都是位圖。frameBuffer 中的位圖是最終顯示到屏幕上的许饿。

image.png

以顯示圖片為例阳欲。圖片數(shù)據(jù)是以紋理形式,進入 OpenGL 渲染流程的陋率,就像上面的流程圖球化。
參照問題5(iOS 渲染具體過程)layer 中的位圖數(shù)據(jù)會與其他的位圖(如果還有其他 layer的話)經(jīng)過片段著色器、測試與混合階段瓦糟,變成新的位圖數(shù)據(jù)筒愚,輸出到 frameBuffer 顯示到屏幕上

思考

  1. 想顯示漸變帶邊框帶陰影的 View 有幾種方式?各有什么優(yōu)缺點菩浙?
  2. 在 TableViewCell 中顯示圓形頭像有幾種方式巢掺?各有什么優(yōu)缺點?
    1. maskToBounds 與 cornerRadius 組合:離屏渲染
    2. 上方疊加一個中間圓形鏤空的圖片:多了一層 ImageView
    3. UIBezierPath 設置的 CAShapeLayer 作為 UIImageView.layer.mask:離屏渲染
    4. CoreGraphics 設置處理 UIImage 劲蜻,注意是直接將 UIImage 繪制成圓形陆淀,而不是調用 UIImageView drawRect::推薦
func ny_image(byRoundCornerRadius radius: CGFloat,
                                     corners: UIRectCorner,
                                 borderWidth: CGFloat,
                                 borderColor: UIColor,
                              borderLineJoin: CGLineJoin) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        let context = UIGraphicsGetCurrentContext()
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        // 根據(jù)坐標系旋轉圖片
        context?.scaleBy(x: 1, y: -1)
        context?.translateBy(x: 0, y: -rect.size.height)

        // 剪成圓形
        let minSize = min(size.width, size.height)
        if borderWidth < minSize / 2 {
            let path = UIBezierPath.init(roundedRect: rect.insetBy(dx: borderWidth, dy: borderWidth), byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
            path.close()
            context?.saveGState()
            path.addClip()
            context?.draw(cgImage!, in: rect)
            context?.restoreGState()
        }

        // 畫邊框
        if (borderColor != .clear) && (borderWidth < minSize / 2) && (borderWidth > 0) {
            let strokeInset = (floor(borderWidth * scale) + 0.5) / scale
            let strokeRect = rect.insetBy(dx: strokeInset, dy: strokeInset)
            let strokeRadius = radius > scale / 2 ? radius - scale / 2 : 0
            let path = UIBezierPath.init(roundedRect: strokeRect, byRoundingCorners: corners, cornerRadii: CGSize(width: strokeRadius, height: strokeRadius))
            path.close()

            path.lineWidth = borderWidth
            path.lineJoinStyle = borderLineJoin
            borderColor.setStroke()
            path.stroke()
        }

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }

參考

  1. iOS 保持界面流暢的技巧
  2. 繪制像素到屏幕上
  3. iOS 渲染原理解析
  4. iOS 圖像渲染原理
  5. 圖像顯示、OpenGL先嬉、離屏渲染轧苫、濾鏡等等的一些小事
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市疫蔓,隨后出現(xiàn)的幾起案子含懊,更是在濱河造成了極大的恐慌,老刑警劉巖衅胀,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岔乔,死亡現(xiàn)場離奇詭異,居然都是意外死亡滚躯,警方通過查閱死者的電腦和手機雏门,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哀九,“玉大人剿配,你說我怎么就攤上這事≡氖” “怎么了呼胚?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長息裸。 經(jīng)常有香客問我蝇更,道長沪编,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任年扩,我火速辦了婚禮蚁廓,結果婚禮上,老公的妹妹穿的比我還像新娘厨幻。我一直安慰自己相嵌,他們只是感情好,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布况脆。 她就那樣靜靜地躺著饭宾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪格了。 梳的紋絲不亂的頭發(fā)上看铆,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音盛末,去河邊找鬼弹惦。 笑死,一個胖子當著我的面吹牛悄但,可吹牛的內(nèi)容都是我干的棠隐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼算墨,長吁一口氣:“原來是場噩夢啊……” “哼宵荒!你這毒婦竟也來了?” 一聲冷哼從身側響起净嘀,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎侠讯,沒想到半個月后挖藏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡厢漩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年膜眠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溜嗜。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡宵膨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出炸宵,到底是詐尸還是另有隱情辟躏,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布土全,位于F島的核電站捎琐,受9級特大地震影響会涎,放射性物質發(fā)生泄漏。R本人自食惡果不足惜瑞凑,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一末秃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧籽御,春花似錦练慕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至零截,卻和暖如春麸塞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涧衙。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工哪工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弧哎。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓雁比,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撤嫩。 傳聞我的和親對象是個殘疾皇子偎捎,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354