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. 為什么要減少離屏渲染
- 離屏渲染需要 App 對內(nèi)容進行額外的渲染,并保存到 OffscreenBuffer 火本,
- 需要對 OffscreenBuffer 和 FrameBuffer 中的內(nèi)容進行切換(Buffer 切換的代價比較大)危队。
- OffscreenBuffer 本身需要額外的空間,大量的離屏渲染可能早晨過大的內(nèi)存壓力钙畔。
4. 離屏渲染的具體過程是怎樣的
圖層的疊加繪制茫陆,大體遵循“畫家算法”。如圖所示擎析,在這種算法下會按層繪制簿盅,先繪制距離較遠的場景,然后繪制距離較近的場景揍魂,覆蓋較遠的部分桨醋。因此在普通的layer繪制中,上層的 subLayer 會覆蓋下層 subLayer现斋,下層 subLayer 繪制完成后就可以拋棄了喜最,從而節(jié)約空間。所有的 subLayer 繪制完成后步责,整個繪制就完成了返顺,就放入 frameBuffer 準備呈現(xiàn)到顯示器上。
而當設置了 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 獲取到的圖像信息稱為圖元豹障,通常是三角形冯事、頂點、線段等血公。主要分為以下幾個階段- Geometry 幾何處理階段:包含頂點著色器昵仅、形狀裝配、集合著色器
- 頂點著色器:這個階段會將圖元中的頂點信息進行視角轉換累魔、添加光照摔笤、增加紋理等操作
- 形狀(圖元)裝配:根據(jù)上一步中的頂點位置,裝配成基本的圖元形狀垦写,例如三角形
- 集合著色器:增加額外的頂點吕世,將原始圖元轉化成新的圖元,以構建一個不一樣的模型梯投∶剑總的來說就是基于三角形、線段分蓖、點構建更復雜的幾何圖形
-
Rasterization 光柵化階段:圖元轉換為像素
光柵化的目的是將集合渲染后的圖元信息轉換為一系列的像素尔艇,同時會裁掉屏幕顯示范圍之外的內(nèi)容,以便后續(xù)顯示在屏幕上么鹤。這個階段會根據(jù)圖元信息终娃,計算出每個圖元所覆蓋的像素信息,從而將像素劃分成不同的部分蒸甜。
- 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 值错邦,從而進行片段的混合,得到最終的顏色型宙。
- Geometry 幾何處理階段:包含頂點著色器昵仅、形狀裝配、集合著色器
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. 為何要圖片解碼箭跳,直接顯示圖片有什么問題晨另?
圖片格式
- 位圖:位圖是一個像素數(shù)組,數(shù)組中的每個像素就代表著圖片中的一個點
- 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。主要為
- 負責解析圖層樹璧亚,反序列化為渲染樹:使用這個樹狀結構讨韭,渲染服務隊動畫的每一幀做出如下工作:對所有的圖層屬性計算中間值,設置 OpenGL 幾何形狀(紋理化的三角形)來執(zhí)行渲染癣蟋。
- 調用繪制指令透硝,并提交到 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 中的位圖是最終顯示到屏幕上的许饿。
以顯示圖片為例阳欲。圖片數(shù)據(jù)是以紋理形式,進入 OpenGL 渲染流程的陋率,就像上面的流程圖球化。
參照問題5(iOS 渲染具體過程)layer 中的位圖數(shù)據(jù)會與其他的位圖(如果還有其他 layer的話)經(jīng)過片段著色器、測試與混合階段瓦糟,變成新的位圖數(shù)據(jù)筒愚,輸出到 frameBuffer 顯示到屏幕上
思考
- 想顯示漸變帶邊框帶陰影的 View 有幾種方式?各有什么優(yōu)缺點菩浙?
- 在 TableViewCell 中顯示圓形頭像有幾種方式巢掺?各有什么優(yōu)缺點?
- maskToBounds 與 cornerRadius 組合:離屏渲染
- 上方疊加一個中間圓形鏤空的圖片:多了一層 ImageView
- UIBezierPath 設置的 CAShapeLayer 作為 UIImageView.layer.mask:離屏渲染
- 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
}