iOS 渲染

整理一些老生常談的問題.

一.顯示流程

1.繪制
當(dāng)然是邏輯上的繪制,此時各種框架的代碼開始工作,iOS并不完全由GPU負(fù)責(zé)渲染,比如當(dāng)需要Core Graphics參與時,CPU會做更多的事,這一點很重要;比如當(dāng)重寫drawRect方法時使用了很多Core Graphics的api,此時就由CPU來繪制bitmap,并且還需要內(nèi)存來存儲bitmap,當(dāng)然只是繪制一張圖并不會消耗什么,但是如果頻繁的繪制就不一樣了.
繪制包含很多動作,比如UIKit中UIView的布局,Core Animation中CALayer的顯示和動畫,還有上面說的core Graphics繪制,以及OpenGL,Metal的繪制,這部分可能是CPU的工作也可能直接由GPU處理.

框架

2.畫面渲染
從渲染流程的角度,不同的框架控制不同的步驟,除了OpenGL或者Metal更多設(shè)計GPU的工作,其他框架基本都是在編程CPU的工作.編程以外的事,就是GPU渲染管線,計算過程,緩存策略等等了.
cpu和gpu處理完成之后,得到一張bitmap,也就是像素點陣,接下來視頻控制器將bitmap顯示到屏幕上.
但是由于計算需要時間不一,視頻控制器不能去等bitmap生成出來,因此需要一個緩存來存儲已經(jīng)生成好的bitmap,也就是幀緩沖Framebuffer,

渲染過程

3.掃描顯示
當(dāng)cpu和GPU的工作完成時,視頻控制器就應(yīng)該將bitmap轉(zhuǎn)換為模擬信號,給顯示元件發(fā)送指令了.
iOS屏幕的顯示仍然類似CRT電子掃描顯示,從左上角開始,從左到右以此控制顯示元件,一行顯示完成后開始第二行,這樣從上到下的刷新屏幕.

GPU隨時可能會進行渲染,視頻控制器隨時可能會給顯示元件發(fā)送指令,那么自然就會有這種情況,當(dāng)掃描顯示進行到一半的時候收到了新的指令,如果畫面變化不大,那就感覺不出來什么區(qū)別,如果畫面變化很大,就產(chǎn)生了畫面撕裂現(xiàn)象.

4.垂直同步和二級緩沖
iOS采用使用垂直同步信號 Vsync 與雙緩沖機制 Double Buffering來解決這個問題.

當(dāng)元件開始讀取幀數(shù)據(jù)的時候,加一個鎖,在當(dāng)前幀發(fā)送完畢之前,不發(fā)送下一幀的顯示指令;當(dāng)完成一行的顯示時,發(fā)送一個水平同步信號(horizonal synchronization),當(dāng)完成一幀的顯示時,發(fā)送一個垂直同步信號(vertical synchronization).當(dāng)視頻控制器收到垂直信號時,再從幀緩沖中取出需要顯示的下一張bitmap,進行顯示.

5.卡頓
解決畫面撕裂的方案其實就一個字,等.但是這會引起別的問題.
視圖控制器有預(yù)定的發(fā)送頻率,也就是預(yù)定幀率,在支持高刷(ProMotion displays)的iPhone13之前,是60fps,之后是120fps.如果是60Fps,那就是16.7毫秒發(fā)送一次bitmap,假如cpu和gpu的復(fù)載很大,收到Vsync的時候framebuffer還沒有新的一幀,就發(fā)送舊的,畫面就沒有變化,對應(yīng)到屏幕刷新,就是卡頓現(xiàn)象.
二級緩沖就是GPU 會預(yù)先渲染一幀放入一個緩沖區(qū)中妓美,用于視頻控制器的讀取。當(dāng)下一幀渲染完畢后,GPU 會直接把視頻控制器的指針指向第二個緩沖器帖旨。

二.離屏渲染

1.Core Animation的工作流程

image.png

這張有名的圖是Core Animation的工作流程,可以看到既參與Application的部分,又要負(fù)責(zé)Rander server的部分.

Commit Transaction階段指的是對布局進行計算,構(gòu)建和繪制,以及圖片解碼等等工作;

首先是構(gòu)建視圖(layout),包括初始化各種UIView對象,addsubveiw, 計算frame布局和autolayout等;在這一階段,影響性能的就是圖層數(shù)量,布局關(guān)系,減少視圖層級,避免不必要的初始化可以提升性能.
其次是繪制(display),構(gòu)建視圖時,用于交付 draw calls的bitmap還沒有生成,但是如果在drawrect中使用core Graphics的api繪制圖形,CPU就會直接開始生成bitmap,并且還要申請內(nèi)存來存儲.此時就需要注意不能頻繁的繪制.

Rander server階段, Core Animation對圖層解碼,得到每個layer的bitmap;
當(dāng)完成解碼后,等待下一次runloop,core Animation調(diào)用OpenGL或者Metal的接口,開始draw calls階段;
OpenGL或者Metal拿到具體的任務(wù),此時,工作已經(jīng)到了GPU這邊,開始執(zhí)行渲染;
當(dāng)渲染完成,下一次runloop循環(huán)開始,就是顯示元件的工作了.

2.offscreen buffer
前面說到GPU的frame buffer緩存將要顯示的bitmap,但是有些情況下,core Animation交付的bitmap并不是最終版本,GPU需要暫存一些中間狀態(tài)的bitmap,當(dāng)這些中間bitmap都到位之后,再生出最終的bitmap放到frame buffer,而offscreen buffer就負(fù)責(zé)管理這些中間狀態(tài)的bitmap.也就是常說的離屏渲染.
因此offscreen是GPU的工作部分產(chǎn)生的,所以CPU負(fù)責(zé)的部分,雖然也可能申請內(nèi)存去存儲中間狀態(tài),比如core Graphics繪制就是典型的開辟一個cgcontext來畫,除此之外還有文字繪制,圖片解碼視頻軟解等,但是這些不屬于離屏渲染,重寫drawrect并不會被Color offscreen rendered標(biāo)記黃色.

3.畫家算法
OpenGL/Metal輸出的bitmap會像畫油畫一樣一層層覆蓋,bitmap作為位圖,像素點陣,包含的信息很有限,一個位置的像素信息,被后來的信息覆蓋,前面的就丟失了,當(dāng)修改完一整張bitmap后,再想回頭修改之前的bitmap是做不到的.
對于一個layer,如果它能夠確定自己的內(nèi)容,一步到位,就可以直接到frame buffer中去疊加, 如果不能一步到位,比如說cornerRadius+clipsToBounds,就得把自己和子layer都先疊加畫好好,然后再裁剪,再然后才能去frame buffer,在畫子layer之前,自己就會先在offscreen buffer中渲染.

4.離屏渲染的場景
那么什么時候會產(chǎn)生離屏渲染呢,模擬器打開Color offscreen rendered觀察一下.

  • 圓角

這是一個UIImageView,它的clipsToBounds為true,layer.cornerRadius是10.0,backgroundcolor是clear,
此時并沒有產(chǎn)生離屏渲染
使用layer的contents也是一樣的

        let l = CALayer.init()
        l.frame = .init(x: 0, y: 0, width: 20, height: 20)
        l.masksToBounds = true
        l.contents = UIImage.init(named:"avatar")?.cgImage
        l.contentsGravity = .resizeAspectFill
        l.cornerRadius = 5
//        l.backgroundColor = UIColor.blue.cgColor
        contentView.layer.addSublayer(l)

image.png

但是如果backgroundcolor不是clear,比如設(shè)置成black,就會產(chǎn)生離屏渲染


image.png

此外border也會影響離屏渲染,即便背景是透明,設(shè)置了border和圓角和clipstobound,也會產(chǎn)生離屏渲染

avatar.backgroundColor = .clear
avatar.layer.borderWidth = 1
avatar.layer.borderColor = UIColor.black.cgColor
image.png

主要是因為layer的三層結(jié)構(gòu)


layer的結(jié)構(gòu)

這是一個UIView,同樣設(shè)置了clipsToBounds=true,layer.cornerRadius=10.0,backgroundcolor是灰色,它沒有離屏渲染,
當(dāng)給他添加一個subView的時候,就產(chǎn)生了離屏渲染,此時如果把backgroundcolor設(shè)置為clear,又沒有離屏渲染了.


image.png

image.png

此時如果改成超出父視圖的這種布局,會發(fā)現(xiàn)離屏渲染又回來了;
這是由core Animation的算法決定的,子層沒有超過父層的部分,那么就可以按照畫家算法正常的去畫.


超出了父視圖

image.png

因此在clipsToBounds圓角方面,視圖層級,視圖布局,背景色,contents,都是會影響離屏渲染的因素.

  • 陰影
    給一個UIView添加陰影,結(jié)果整個都是黃色的.
singleV.layer.shadowColor = UIColor.black.cgColor
        singleV.layer.shadowOffset = .init(width: 2, height: 2)
        singleV.layer.shadowRadius = 0.8
        singleV.layer.shadowOpacity = 1.0
image.png

甚至當(dāng)設(shè)置clipsToBounds= true時,關(guān)閉圓角,此時陰影被裁剪了,依然是離屏渲染


陰影被裁了

而clearcolor的情況下陰影不會生效,也不會有離屏渲染,不過如果有子視圖,陰影是會對子視圖生效的,此時就有離屏渲染


clearcolor

子視圖
  • mask
    這個都不用看效果了,mask的本質(zhì)就是層的合并,只要layer本身不是透明的,用了肯定離屏渲染.
let be = UIBezierPath.init()
        be.move(to: .init(x: 10, y: 10))
        be.addLine(to: .init(x: 50, y: 10))
        be.addLine(to: .init(x: 50, y: 50))
        be.addLine(to: .init(x: 10, y: 50))
        be.close()
        let layer = CAShapeLayer.init()
        layer.path = be.cgPath
        layer.backgroundColor = UIColor.red.cgColor
        singleV.layer.mask = layer
image.png
let l = CALayer.init()
        l.backgroundColor = UIColor.black.cgColor
        l.opacity = 0.5
        l.frame = .init(x: 0, y: 0, width: 80, height: 80)
        avatar.layer.mask = l
image.png
  • 光柵化
    shouldRasterize默認(rèn)是false, 設(shè)置為true時會緩存該layer的bitmap在offscreen buffer,因此屬于離屏渲染
  • 組透明度
    layer.allowsGroupOpacity默認(rèn)是true,開啟和關(guān)閉時的渲染效果不同,并且開啟時如果存在子layer,會引起離屏渲染.
    它指的是不單獨對層進行透明度處理,等層和子層渲染完了之后,再應(yīng)用透明度.
    另外透明度是是渲染比較靠后的一步,光柵化緩存的bitmap也不包含透明度信息.


    true

    false

    image.png
  • 文本
    CATextLayer和帶有contents的普通layer性質(zhì)相同
let l = CATextLayer.init()
        l.frame = .init(x: 320, y: 10, width: 100, height: 40)
        l.foregroundColor = UIColor.white.cgColor
        l.contentsScale = UIScreen.main.scale
        let font = UIFont.systemFont(ofSize: 14)
        l.font = CGFont.init(font.fontName as CFString)
        l.fontSize = font.pointSize
        l.string = "text layer"
        l.backgroundColor = UIColor.black.cgColor
        l.masksToBounds = true
        l.cornerRadius = 10
        contentView.layer.addSublayer(l)
CATextLayer

UILabel具有其特殊性,不管是普通的繪制文字,clipsToBounds,背景顏色,圓角,都不會產(chǎn)生離屏渲染;
UILabel的layer,類型是_UILabelLayer,并非CATextLayer. _UILabelLayer繼承自CALayer.

label.textColor = .white
        label.backgroundColor = .black
        label.clipsToBounds = true
        label.layer.cornerRadius = 10
UILabel

三:關(guān)于性能優(yōu)化

1.CPU和GPU都要關(guān)照
通常大部分的渲染工作由GPU來完成,從框架角度來說基本是由Core Animation來做,Core Animation實現(xiàn)硬件加速,也就是把渲染工作轉(zhuǎn)換成適合GPU處理的形式.但是也有一些工作Core Animation做不到的,他們屬于core Graphics的工作范圍,比如繪制文字,圖形以及ImageIO的圖片解碼等等,這些必須由CPU完成,然后再把數(shù)據(jù)傳給GPU.

CPU的有些渲染其實也可以叫做CPU專屬的"離屏渲染",畢竟表現(xiàn)還是很像的,就是更多的消耗性能和占用內(nèi)存
比如要避免頻繁的core Graphics繪制,大量的解碼圖片會引起內(nèi)存暴漲等這些情況要及時釋放內(nèi)存,core Graphics和core image的api都有對應(yīng)的release方法,要合理使用.
前面說到CPU還需要負(fù)責(zé)視圖的構(gòu)建和布局計算,因此頻繁的調(diào)整圖層結(jié)構(gòu),頻繁的重置布局都是需要注意的地方.

對于GPU方面,要用color offscreen rendered來觀察,而不是靠經(jīng)驗和猜測.

2.處理圓角
處理圓角要考慮前面的那些情況,如果子視圖能單獨處理,就單獨處理,分別對層進行切圓角,同時背景能clear的就clear;
另外可以考慮用腦洞來實現(xiàn)圓角效果,比如用圖片遮擋.

3.處理陰影
使用shadowPath可以避免離屏渲染

        singleV.layer.shadowColor = UIColor.black.cgColor
        singleV.layer.shadowOffset = .init(width: 3, height: 3)
        singleV.layer.shadowRadius = 0.8
        singleV.layer.shadowOpacity = 1.0
   //singleV.layer.shadowPath = UIBezierPath.init(rect: singleV.bounds).cgPath
使用shadowpath
沒有使用shadowpath

那么為什么呢:
首先陰影是添加在其他層的效果,A的陰影顯然不會在A自己身上,會在下面的BCD...上,從畫家算法思考,B畫了一半,畫A,然后在B上畫A的陰影,這才算完.
shadowPath會預(yù)先定義好陰影的位置,在core Animation的階段就可以確定陰影畫在哪,現(xiàn)在當(dāng)畫B的時候,直接把陰影畫上,即使A的bitmap還沒有被發(fā)送到GPU,因為在生成B的bitmap時core Animation已經(jīng)預(yù)先計算好了陰影.

4.使用光柵化的情況
光柵化是計算機圖形學(xué)的必要流程,是把采樣結(jié)果隱射到bitmap上,所以這里的shouldRasterize并不是這個意思;
shouldRasterize指的是會把層以及子層整個生成好的bitmap緩存在offscreen buffer,之后如果能夠重用,就可以直接發(fā)送到frame buffer.

當(dāng)界面上的內(nèi)容在頻繁的快速的變化時,比如滑動一個列表,對于60fps的機型,理論上渲染流程每秒要走60次;
但是一秒內(nèi)一個單元格可能從最底下到最上面,它的內(nèi)容并沒有發(fā)生變化;
這種情況下使用shouldRasterize就可以提升性能,當(dāng)然前提是單元格本身就存在不可避免的離屏渲染,或者存在復(fù)雜繁多的圖層結(jié)構(gòu),因為shouldRasterize本身就會產(chǎn)生離屏渲染,簡單的布局使用了它之后得不償失.

除此之外,CPU的部分還要考慮多線程問題,UI的刷新只會在主隊列進行,主隊列只有一個線程就是主線程Thread 0,耗時的操作放在主隊列會影響UI的刷新.
這是apple的解決方案,單線程雖然效率低,但是線程安全;
不過實際上UIKit和core Graphics的繪制也是可以在主線程之外進行的,相關(guān)的庫也有一些比如Facebook的Texture

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末司恳,一起剝皮案震驚了整個濱河市益老,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逆趣,老刑警劉巖荆几,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吓妆,死亡現(xiàn)場離奇詭異,居然都是意外死亡吨铸,警方通過查閱死者的電腦和手機行拢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诞吱,“玉大人舟奠,你說我怎么就攤上這事竭缝。” “怎么了沼瘫?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵抬纸,是天一觀的道長。 經(jīng)常有香客問我耿戚,道長湿故,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任膜蛔,我火速辦了婚禮坛猪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘皂股。我一直安慰自己墅茉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布呜呐。 她就那樣靜靜地躺著就斤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蘑辑。 梳的紋絲不亂的頭發(fā)上洋机,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音以躯,去河邊找鬼槐秧。 笑死啄踊,一個胖子當(dāng)著我的面吹牛忧设,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颠通,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼址晕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了顿锰?” 一聲冷哼從身側(cè)響起谨垃,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎硼控,沒想到半個月后刘陶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡牢撼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年匙隔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片熏版。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡纷责,死狀恐怖捍掺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情再膳,我是刑警寧澤挺勿,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站喂柒,受9級特大地震影響不瓶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜湃番,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吭露。 院中可真熱鬧吠撮,春花似錦、人聲如沸讲竿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽题禀。三九已至鞋诗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間迈嘹,已是汗流浹背削彬。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秀仲,地道東北人融痛。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像神僵,于是被迫代替她去往敵國和親雁刷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容