iOS界面渲染流程分析

前言

本文閱讀建議
1.一定要辯證的看待本文.
2.本文所表達(dá)觀點(diǎn)并不是最終觀點(diǎn),還會(huì)更新,因?yàn)楸救诉€在學(xué)習(xí)過(guò)程中,有什么遺漏或錯(cuò)誤還望各位指出.
3.覺(jué)得哪里不妥請(qǐng)?jiān)谠u(píng)論留下建議~
4.覺(jué)得還行的話就點(diǎn)個(gè)小心心鼓勵(lì)下我吧~

在最近的面試中,我發(fā)現(xiàn)一道面試題,其考點(diǎn)是:圍繞iOS App中一個(gè)視圖從添加到完全渲染,在這個(gè)過(guò)程中,iOS系統(tǒng)都做了什么?

在進(jìn)行了大量的文章查閱以及學(xué)習(xí)以后,將所有較為可靠的資料總結(jié)一下供大家參考。


面試題

本文可為以下面試題提供參考:

  1. app從點(diǎn)擊屏幕(硬件)到完全渲染,中間發(fā)生了什么?越詳細(xì)越好 要求講到進(jìn)程間通信?出處
  2. 一個(gè)UIImageView添加到視圖上以后,內(nèi)部是如何渲染到手機(jī)上的,請(qǐng)簡(jiǎn)述其流程?
  3. 在一個(gè)表內(nèi)有很多cell,每個(gè)cell上有很多個(gè)視圖,如何解決卡頓問(wèn)題?
  4. UIView與CALayer的區(qū)別?

簡(jiǎn)答

iOS渲染視圖的核心是Core Animation
其渲染層次依次為:圖層樹(shù)->呈現(xiàn)樹(shù)->渲染樹(shù)

  1. CPU階段
    1. 布局(Frame)
    2. 顯示(Core Graphics)
    3. 準(zhǔn)備(QuartzCore/Core Animation)
    4. 通過(guò)IPC提交(打包好的圖層樹(shù)以及動(dòng)畫(huà)屬性)
  2. OpenGL ES階段
    1. 生成(Generate)
    2. 綁定(Bind)
    3. 緩存數(shù)據(jù)(Buffer Data)
    4. 啟用(Enable)
    5. 設(shè)置指針(Set Pointers)
    6. 繪圖(Draw)
    7. 清除(Delete)
  3. GPU階段
    1. 接收提交的紋理(Texture)和頂點(diǎn)描述(三角形)
    2. 應(yīng)用變換(transform)
    3. 合并渲染(離屏渲染等)

其iOS平臺(tái)渲染核心原理的重點(diǎn)主要圍繞前后幀緩存、Vsync信號(hào)、CADisplayLink

文字簡(jiǎn)答:

  1. 首先一個(gè)視圖由CPU進(jìn)行Frame布局,準(zhǔn)備視圖和圖層的層級(jí)關(guān)系,查詢是否有重寫(xiě)drawRect:drawLayer:inContext:方法,注意:如果有重寫(xiě)的話,這里的渲染是會(huì)占用CPU進(jìn)行處理的
  2. CPU會(huì)將處理視圖和圖層的層級(jí)關(guān)系打包,通過(guò)IPC(內(nèi)部處理通信)通道提交給渲染服務(wù),渲染服務(wù)由OpenGL ES和GPU組成。
  3. 渲染服務(wù)首先將圖層數(shù)據(jù)交給OpenGL ES進(jìn)行紋理生成和著色。生成前后幀緩存,再根據(jù)顯示硬件的刷新頻率,一般以設(shè)備的VSync信號(hào)CADisplayLink為標(biāo)準(zhǔn),進(jìn)行前后幀緩存的切換。
  4. 最后,將最終要顯示在畫(huà)面上的后幀緩存交給GPU,進(jìn)行采集圖片和形狀,運(yùn)行變換,應(yīng)用紋理和混合。最終顯示在屏幕上。

以上僅僅是對(duì)該題簡(jiǎn)單回答,其中的原理以及瓶頸和優(yōu)化,后面會(huì)詳細(xì)介紹。


知識(shí)點(diǎn)

  1. 重新認(rèn)識(shí)Core Animation
  2. CPU渲染職能
  3. OpenGL ES渲染職能
  4. GPU渲染職能
  5. IPC內(nèi)部通信(進(jìn)程間通信)
  6. 前后幀緩存&Vsync信號(hào)
  7. 視圖渲染優(yōu)化&卡頓優(yōu)化
  8. Metal渲染引擎
  9. 事件響應(yīng)鏈&Runloop原理
  10. CALayer的職能

重新認(rèn)識(shí)Core Animation

蘋(píng)果官方文檔-Core Animation
Core Animation并僅僅是字面意思的核心動(dòng)畫(huà),而是整個(gè)顯示核心都是圍繞QuartzCore框架中的Core Animation

image.png

Core Animation是依賴于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染,但在本文中,以及官方文檔都是將OpenGL與GPU分開(kāi)說(shuō)明。

image.png

Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer,監(jiān)聽(tīng)了 BeforeWaiting 和 Exit 事件。這個(gè) Observer 的優(yōu)先級(jí)是 2000000,低于常見(jiàn)的其他 Observer。當(dāng)一個(gè)觸摸事件到來(lái)時(shí),RunLoop 被喚醒,App 中的代碼會(huì)執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級(jí)、設(shè)置 UIView 的 frame、修改 CALayer 的透明度、為視圖添加一個(gè)動(dòng)畫(huà);這些操作最終都會(huì)被 CALayer 捕獲,并通過(guò) CATransaction 提交到一個(gè)中間狀態(tài)去(CATransaction 的文檔略有提到這些內(nèi)容,但并不完整)。當(dāng)上面所有操作結(jié)束后,RunLoop 即將進(jìn)入休眠(或者退出)時(shí),關(guān)注該事件的 Observer 都會(huì)得到通知。這時(shí) CA 注冊(cè)的那個(gè) Observer 就會(huì)在回調(diào)中,把所有的中間狀態(tài)合并提交到 GPU 去顯示;如果此處有動(dòng)畫(huà),CA 會(huì)通過(guò) DisplayLink 等機(jī)制多次觸發(fā)相關(guān)流程。

CPU渲染職能

在這里推薦大家去閱讀落影l(fā)oyinglin的文章iOS開(kāi)發(fā)-視圖渲染與性能優(yōu)化

  • 顯示邏輯
    • CoreAnimation提交會(huì)話,包括自己和子樹(shù)(view hierarchy)的layout狀態(tài)等;
    • RenderServer解析提交的子樹(shù)狀態(tài),生成繪制指令
    • GPU執(zhí)行繪制指令
    • 顯示渲染后的數(shù)據(jù)
image.png
  • 提交流程
    • 布局(Layout)
      • 調(diào)用layoutSubviews方法
      • 調(diào)用addSubview:方法
    • 顯示(Display)
      • 通過(guò)drawRect繪制視圖;
      • 繪制string(字符串);
    • 準(zhǔn)備提交(Prepare)
      • 解碼圖片;
      • 圖片格式轉(zhuǎn)換;
    • 提交(Commit)
      • 打包layers并發(fā)送到渲染server;
      • 遞歸提交子樹(shù)的layers;
      • 如果子樹(shù)太復(fù)雜,會(huì)消耗很大,對(duì)性能造成影響;

CPU渲染職能主要體現(xiàn)在以下5個(gè)方面:

布局計(jì)算
如果你的視圖層級(jí)過(guò)于復(fù)雜,當(dāng)視圖呈現(xiàn)或者修改的時(shí)候,計(jì)算圖層幀率就會(huì)消耗一部分時(shí)間。特別是使用iOS6的自動(dòng)布局機(jī)制尤為明顯,它應(yīng)該是比老版的自動(dòng)調(diào)整邏輯加強(qiáng)了CPU的工作。

視圖懶加載
iOS只會(huì)當(dāng)視圖控制器的視圖顯示到屏幕上時(shí)才會(huì)加載它。這對(duì)內(nèi)存使用和程序啟動(dòng)時(shí)間很有好處,但是當(dāng)呈現(xiàn)到屏幕上之前,按下按鈕導(dǎo)致的許多工作都會(huì)不能被及時(shí)響應(yīng)。比如控制器從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),或者視圖 從一個(gè)nib文件中加載,或者涉及IO的圖片顯示,都會(huì)比CPU正常操作慢得多。

Core Graphics繪制
如果對(duì)視圖實(shí)現(xiàn)了drawRect:drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在繪制任何東 西之前都會(huì)產(chǎn)生一個(gè)巨大的性能開(kāi)銷。為了支持對(duì)圖層內(nèi)容的任意繪制,Core Animation必須創(chuàng)建一個(gè)內(nèi)存中等大小的寄宿圖片。然后一旦繪制結(jié)束之后, 必須把圖片數(shù)據(jù)通過(guò)IPC傳到渲染服務(wù)器。在此基礎(chǔ)上,Core Graphics繪制就會(huì)變得十分緩慢,所以在一個(gè)對(duì)性能十分挑剔的場(chǎng)景下這樣做十分不好。

解壓圖片
PNG或者JPEG壓縮之后的圖片文件會(huì)比同質(zhì)量的位圖小得多。但是在圖片繪制到屏幕上之前,必須把它擴(kuò)展成完整的未解壓的尺寸(通常等同于圖片寬 x 長(zhǎng) x 4個(gè)字節(jié))。為了節(jié)省內(nèi)存,iOS通常直到真正繪制的時(shí)候才去解碼圖片。根據(jù)你加載圖片的方式,第一次對(duì) 圖層內(nèi)容賦值的時(shí)候(直接或者間接使用 UIImageView )或者把它繪制到 Core Graphics中,都需要對(duì)它解壓,這樣的話,對(duì)于一個(gè)較大的圖片,都會(huì)占用一定的時(shí)間。

圖層打包
當(dāng)圖層被成功打包,發(fā)送到渲染服務(wù)器之后,CPU仍然要做如下工作:為了顯示 屏幕上的圖層,Core Animation必須對(duì)渲染樹(shù)種的每個(gè)可見(jiàn)圖層通過(guò)OpenGL循環(huán) 轉(zhuǎn)換成紋理三角板。由于GPU并不知曉Core Animation圖層的任何結(jié)構(gòu),所以必須 要由CPU做這些事情。這里CPU涉及的工作和圖層個(gè)數(shù)成正比,所以如果在你的層 級(jí)關(guān)系中有太多的圖層,就會(huì)導(dǎo)致CPU沒(méi)一幀的渲染,即使這些事情不是你的應(yīng)用 程序可控的。

OpenGL ES渲染職能

這里推薦大家去看《OpenGL ES應(yīng)用開(kāi)發(fā)實(shí)踐指南:iOS卷》,因?yàn)槠^(guò)長(zhǎng),就不贅述OpenGL的原理。

image.png

簡(jiǎn)單來(lái)說(shuō),OpenGL ES是對(duì)圖層進(jìn)行取色,采樣,生成紋理,綁定數(shù)據(jù),生成前后幀緩存。

紋理的概念:紋理是一個(gè)用來(lái)保存圖像的顏色元??值的 OpenGL ES 緩存,可以簡(jiǎn)單理解為一個(gè)單位。

1)生成(Generate)— 請(qǐng) OpenGL ES 為圖形處理器制的緩存生成一個(gè)獨(dú)一無(wú)二的標(biāo)識(shí)符。 
2)綁定(Bind)— 告訴 OpenGL ES 為接下來(lái)的運(yùn)算使用一個(gè)緩存。 
3)緩存數(shù)據(jù)(Buffer Data)— 讓 OpenGL ES 為當(dāng)前定的緩存分配并初始化 夠的內(nèi)存(通常是從 CPU 制的內(nèi)存復(fù)制數(shù)據(jù)到分配的內(nèi)存)。 
4)啟用(Enable)或者(Disable)— 告訴 OpenGL ES 在接下來(lái)的渲染中是 使用緩存中的數(shù)據(jù)。 
5)設(shè)置指(Set Pointers)— 告訴 Open-GL ES 在緩存中的數(shù)據(jù)的類型和所有需 要的數(shù)據(jù)的內(nèi)存移值。 
6)繪圖(Draw) — 告訴 OpenGL ES 使用當(dāng)前定并啟用的緩存中的數(shù)據(jù)渲染 整個(gè)場(chǎng)景或者某個(gè)場(chǎng)景的一部分。 
7)刪除除(Delete)— 告訴 OpenGL ES 除以前生成的緩存并釋相關(guān)的資源。 

當(dāng)顯示一個(gè)UIImageView時(shí),Core Animation會(huì)創(chuàng)建一個(gè)OpenGL ES紋理,并確保在這個(gè)圖層中的位圖被上傳到對(duì)應(yīng)的紋理中。當(dāng)你重寫(xiě)-drawInContext方法時(shí),Core Animation會(huì)請(qǐng)求分配一個(gè)紋理,同時(shí)確保Core Graphics會(huì)將你在-drawInContext中繪制的東西放入到紋理的位圖數(shù)據(jù)中。

image.png

iOS 操作系統(tǒng)不會(huì)讓?xiě)?yīng)用直接向前幀緩存或者 后幀緩存繪圖,也不會(huì)讓?xiě)?yīng)用直接復(fù)制前幀緩存和后幀緩存之間的切換。操作系統(tǒng)為自 己保留了這些操作,以便它可以隨時(shí)使用 Core Animation 合成器來(lái)控制顯示的最終外觀

最終,生成前后幀緩存會(huì)再交由GPU進(jìn)行最后一步的工作。

GPU渲染職能

GPU會(huì)根據(jù)生成的前后幀緩存數(shù)據(jù),根據(jù)實(shí)際情況進(jìn)行合成,其中造成GPU渲染負(fù)擔(dān)的一般是:離屏渲染,圖層混合,延遲加載。

image.png
  • 普通的Tile-Based渲染流程
    • CommandBuffer,接受OpenGL ES處理完畢的渲染指令;
    • Tiler,調(diào)用頂點(diǎn)著色器,把頂點(diǎn)數(shù)據(jù)進(jìn)行分塊(Tiling);
    • ParameterBuffer,接受分塊完畢的tile和對(duì)應(yīng)的渲染參數(shù);
    • Renderer,調(diào)用片元著色器,進(jìn)行像素渲染;
      -RenderBuffer,存儲(chǔ)渲染完畢的像素;
  • 離屏渲染 —— 遮罩(Mask)
    • 渲染layer的mask紋理,同Tile-Based的基本渲染邏輯;
    • 渲染layer的content紋理,同Tile-Based的基本渲染邏輯;
    • Compositing操作,合并1、2的紋理;
  • 離屏渲染 ——UIVisiualEffectView
  • 渲染等待
  • 光柵化
  • 組透明度

GPU用來(lái)采集圖片和形狀,運(yùn)行變換,應(yīng)用文理和混合,最終把它們輸送到屏幕上。

太多的幾何結(jié)構(gòu)會(huì)影響GPU速度,但這并不是GPU的瓶頸限制原因,但由于圖層在顯示之前要通過(guò)IPC發(fā)送到渲染服務(wù)器的時(shí)候(圖層實(shí)際上是由很多小物體組成的特別重量級(jí)的對(duì)象),太多的圖層就會(huì)引起CPU的瓶頸。

重繪。主要由重疊的半透明圖層引起。GPU的填充比率(用顏色填充像素的比率)是有限的,所以要避免重繪。


IPC內(nèi)部通信(進(jìn)程間通信)

在研究這個(gè)問(wèn)題的過(guò)程中,我有想過(guò)去看一下源碼,試著去理解在視圖完全渲染之前,IPC是如何調(diào)度的,可惜蘋(píng)果并沒(méi)有開(kāi)源繪制過(guò)程中的代碼。這里推薦官方文章給大家了解一下iOS中IPC是如何運(yùn)作的。

蘋(píng)果官方文檔-Mach內(nèi)核編程 IPC通信

前后幀緩存&Vsync信號(hào)

雖然我們不能看到蘋(píng)果內(nèi)部是如何實(shí)現(xiàn)的,但是蘋(píng)果官方也提供了我們可以參考的對(duì)象,也就是VSync信號(hào)CADisplayLink對(duì)象。

iOS 的顯示系統(tǒng)是由 VSync 信號(hào)驅(qū)動(dòng)的,VSync 信號(hào)由硬件時(shí)鐘生成,每秒鐘發(fā)出 60 次(這個(gè)值取決設(shè)備硬件,比如 iPhone 真機(jī)上通常是 59.97)。iOS 圖形服務(wù)接收到 VSync 信號(hào)后,會(huì)通過(guò) IPC 通知到 App 內(nèi)。App 的 Runloop 在啟動(dòng)后會(huì)注冊(cè)對(duì)應(yīng)的 CFRunLoopSource 通過(guò) mach_port 接收傳過(guò)來(lái)的時(shí)鐘信號(hào)通知,隨后 Source 的回調(diào)會(huì)驅(qū)動(dòng)整個(gè) App 的動(dòng)畫(huà)與顯示。

image.png

幀緩存:接收渲染結(jié)果的緩沖區(qū),為GPU指定存儲(chǔ)渲染結(jié)果的區(qū)域

幀緩存可以同時(shí)存在多個(gè),但是屏幕顯示像素受到保存在前幀緩存(front frame buffer)的特定幀緩存中的像素顏色元素的控制。
程序的渲染結(jié)果通常保存在后幀緩存(back frame buffer)在內(nèi)的其他幀緩存,當(dāng)渲染后的后幀緩存完成后,前后幀緩存會(huì)互換。(這部分操作由操作系統(tǒng)來(lái)完成)

前幀緩存決定了屏幕上顯示的像素顏色,會(huì)在適當(dāng)?shù)臅r(shí)候與后幀緩存切換。

Core Animation的合成器會(huì)聯(lián)合OpenGL ES層和UIView層、StatusBar層等,在后幀緩存混合產(chǎn)生最終的顏色,并切換前后幀緩存;
OpenGL ES坐標(biāo)是以浮點(diǎn)數(shù)來(lái)存儲(chǔ),即使是其他數(shù)據(jù)類型的頂點(diǎn)數(shù)據(jù)也會(huì)被轉(zhuǎn)化成浮點(diǎn)型;


視圖加載

那么在了解iOS視圖渲染流程以后,再來(lái)看一下第二題:
一個(gè)UIImageView添加到視圖上以后,內(nèi)部是如何渲染到手機(jī)上的,請(qǐng)簡(jiǎn)述其流程?

圖片的顯示分為三步:加載、解碼、渲染。
通常,我們操作的只有加載,解碼和渲染是由UIKit進(jìn)行。
以UIImageView為例。當(dāng)其顯示在屏幕上時(shí),需要UIImage作為數(shù)據(jù)源。
UIImage持有的數(shù)據(jù)是未解碼的壓縮數(shù)據(jù),能節(jié)省較多的內(nèi)存和加快存儲(chǔ)。
當(dāng)UIImage被賦值給UIImage時(shí)(例如imageView.image = image;),圖像數(shù)據(jù)會(huì)被解碼,變成RGB的顏色數(shù)據(jù)。
解碼是一個(gè)計(jì)算量較大的任務(wù),且需要CPU來(lái)執(zhí)行;并且解碼出來(lái)的圖片體積與圖片的寬高有關(guān)系,而與圖片原來(lái)的體積無(wú)關(guān)。
此處引用-->iOS性能優(yōu)化——圖片加載和處理

我查看了較為流行的第三方庫(kù)源碼,例如YYImage、SDWebImage、FastImageCache,其中加載一個(gè)圖片的流程大致為:

  1. 查看UIImageView的API我們可以發(fā)現(xiàn),UIImage封裝了一個(gè)CoreGraphics/CGImage的對(duì)象。
    1.+[UIImage imageWithContentsOfFile:]使用Image I/O創(chuàng)建CGImageRef內(nèi)存映射數(shù)據(jù)。此時(shí),圖像尚未解碼。
  2. 返回的圖像被分配給UIImageView。
  3. 如果圖像數(shù)據(jù)為未解碼的PNG/JPG,解碼為位圖數(shù)據(jù)
  4. 隱式CATransaction捕獲到UIImageView layer樹(shù)的變化
  5. 在主運(yùn)行循環(huán)的下一次迭代中,Core Animation提交隱式事務(wù),這會(huì)涉及創(chuàng)建已設(shè)置為層內(nèi)容的所有圖像的副本,根據(jù)圖像:
    1. 緩沖區(qū)被分配用于管理文件IO和解壓縮操作。
    2. 文件數(shù)據(jù)從磁盤(pán)讀入內(nèi)存。
    3. 壓縮的圖像數(shù)據(jù)被解碼成其未壓縮的位圖形式
    4. Core Animation使用未壓縮的位圖數(shù)據(jù)來(lái)渲染圖層。

再看一下YYImage的源碼,其流程也大致為:

  1. 獲取圖片二進(jìn)制數(shù)據(jù)
  2. 創(chuàng)建一個(gè)CGImageRef對(duì)象
  3. 使用CGBitmapContextCreate()方法創(chuàng)建一個(gè)上下文對(duì)象
  4. 使用CGContextDrawImage()方法繪制到上下文
  5. 使用CGBitmapContextCreateImage()生成CGImageRef對(duì)象。
  6. 最后使用imageWithCGImage()方法將CGImage轉(zhuǎn)化為UIImage。

當(dāng)然YYImage不止做了這些,還有解碼器編碼器,支持webP等多種格式,并且還寫(xiě)了自定義的操作隊(duì)列,對(duì)網(wǎng)絡(luò)加載圖片進(jìn)行了優(yōu)化。在此不贅述。

推薦文章:
蘋(píng)果官方文檔-CGImage位圖
iOS圖片加載速度極限優(yōu)化—FastImageCache解析
Image I/O詳解的文章
在這里同時(shí)推薦Y大的兩篇文章
移動(dòng)端圖片格式調(diào)研
iOS 處理圖片的一些小 Tip

視圖渲染優(yōu)化&卡頓優(yōu)化

接下來(lái)我們看一下第三題:在一個(gè)表內(nèi)有很多cell,每個(gè)cell上有很多個(gè)視圖,如何解決卡頓問(wèn)題?

什么是卡頓?蘋(píng)果官方文章-顯示幀率

image.png

當(dāng)你的主線程操作卡頓超過(guò)16.67ms以后,你的應(yīng)用就會(huì)出現(xiàn)掉幀,丟幀的情況。也就是卡頓。

一般來(lái)說(shuō)造成卡頓的原因,就是CPU負(fù)擔(dān)過(guò)重,響應(yīng)時(shí)間過(guò)長(zhǎng)。主要原因有以下幾種:

  • 隱式繪制 CGContext
  • 文本CATextLayer 和 UILabel
  • 光柵化 shouldRasterize
  • 離屏渲染
  • 可伸縮圖片
  • shadowPath
  • 混合和過(guò)度繪制
  • 減少圖層數(shù)量
  • 裁切
  • 對(duì)象回收
  • Core Graphics繪制
  • -renderInContext: 方法

其中最常見(jiàn)的問(wèn)題就是離屏渲染

離屏渲染:離屏繪制發(fā)生在基于CPU或者是GPU的渲染,或者是為離屏圖 片分配額外內(nèi)存,以及切換繪制上下文,這些都會(huì)降低GPU性能。對(duì)于特定圖 層效果的使用,比如圓角,圖層遮罩,陰影或者是圖層光柵化都會(huì)強(qiáng)制Core Animation提前渲染圖層的離屏繪制。

如果視圖繪制超出GPU支持的2048x2048或者4096x4096尺寸的 紋理,就必須要用CPU在圖層每次顯示之前對(duì)圖片預(yù)處理,同樣也會(huì)降低性能。

那么如何在需要渲染大量視圖的情況下,還能保證流暢度,也就是保證FPS。
在這里推薦閱讀郭曜源前輩的iOS 保持界面流暢的技巧
以及indulge_in的YYAsyncLayer剖析
我參考了YYAsyncLayer,他其中的原理大致是這樣的:

YYAsyncLayer原理

YYAsyncLayer 是 CALayer 的子類,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時(shí),它會(huì)向 delegate,也就是 UIView 請(qǐng)求一個(gè)異步繪制的任務(wù)。在異步繪制時(shí),Layer 會(huì)傳遞一個(gè) BOOL(^isCancelled)() 這樣的 block,繪制代碼可以隨時(shí)調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消。

當(dāng) TableView 快速滑動(dòng)時(shí),會(huì)有大量異步繪制任務(wù)提交到后臺(tái)線程去執(zhí)行。但是有時(shí)滑動(dòng)速度過(guò)快時(shí),繪制任務(wù)還沒(méi)有完成就可能已經(jīng)被取消了。如果這時(shí)仍然繼續(xù)繪制,就會(huì)造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無(wú)法完成。我的做法是盡量快速、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前,我都會(huì)調(diào)用 isCancelled() 來(lái)進(jìn)行判斷,保證被取消的任務(wù)能及時(shí)退出,不至于影響后續(xù)操作。

AsyncDisplayKit原理

ASDK 在此處模擬了 Core Animation 的這個(gè)機(jī)制:所有針對(duì) ASNode 的修改和提交,總有些任務(wù)是必需放入主線程執(zhí)行的。當(dāng)出現(xiàn)這種任務(wù)時(shí),ASNode 會(huì)把任務(wù)用 ASAsyncTransaction(Group) 封裝并提交到一個(gè)全局的容器去。ASDK 也在 RunLoop 中注冊(cè)了一個(gè) Observer,監(jiān)視的事件和 CA 一樣,但優(yōu)先級(jí)比 CA 要低。當(dāng) RunLoop 進(jìn)入休眠前、CA 處理完事件后,ASDK 就會(huì)執(zhí)行該 loop 內(nèi)提交的所有任務(wù)。

Tips

優(yōu)化方案圍繞著 使用多線程調(diào)用,合理利用CPU計(jì)算位置,布局,層次,解壓等,再合理調(diào)度GPU進(jìn)行渲染,GPU負(fù)擔(dān)常常要比CPU大,合理調(diào)度CPU進(jìn)行計(jì)算可以減輕GPU渲染負(fù)擔(dān),使應(yīng)用更加流暢。


Metal渲染引擎

當(dāng)你現(xiàn)在再去查閱官方文檔時(shí),你會(huì)發(fā)現(xiàn)蘋(píng)果官方已經(jīng)使用Metal去替代OpenGL ES作為Core Animation的渲染。
看一下蘋(píng)果官方文檔-Metal可以發(fā)現(xiàn),早在

image.png

蘋(píng)果將Metal作為新的渲染引擎,更好的利用了GPU的性能,同時(shí)保證了低內(nèi)存占用和省電,但我個(gè)人并沒(méi)有深入研究Metal,這里可以有興趣的同學(xué)可以看一下落影前輩的文章:
Metal入門教程總結(jié)
Metal入門教程(八)Metal與OpenGL ES交互
OpenGL 專題


事件響應(yīng)鏈&原理

最后一題:UIView和CALayer的區(qū)別?

如果你已經(jīng)做了幾年iOS開(kāi)發(fā),相比對(duì)于這道題可能已經(jīng)很熟悉。
最直接的回答就是UIView可以響應(yīng)用戶事件,而CALayer不能處理事件

首先要講一下App中的事件響應(yīng)鏈,它分為兩部分:Hit-Testing事件傳遞 & Runloop原理

當(dāng)用戶對(duì)屏幕進(jìn)行了操作,產(chǎn)生了一個(gè)用戶事件。

蘋(píng)果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來(lái)接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()

當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收。這個(gè)過(guò)程的詳細(xì)情況可以參考這里。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程。隨后蘋(píng)果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue()進(jìn)行應(yīng)用內(nèi)部的分發(fā)。

_UIApplicationHandleEventQueue()會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點(diǎn)擊、touchesBegin/Move/End/Cancel事件都是在這個(gè)回調(diào)中完成的。
此處引用-->深入理解Runloop-事件響應(yīng)

image.png

當(dāng)前前臺(tái)運(yùn)行中應(yīng)用接收到UIEvent以后,當(dāng)用戶對(duì)屏幕進(jìn)行了操作,系統(tǒng)先循環(huán)調(diào)用Hit-test遍歷視圖棧里的視圖,順序?yàn)橐晥D層次的逆順序,用Responder Chain響應(yīng)鏈傳遞一層層給根視圖AppDelegate處理。-->蘋(píng)果官方文檔-使用響應(yīng)者和響應(yīng)者鏈來(lái)處理事件
image.png

推薦兩篇文章:
iOS 事件處理機(jī)制與圖像渲染過(guò)程
iOS事件響應(yīng)鏈中Hit-Test View的應(yīng)用

CALayer的職能

CALayer 并不清楚具體的響應(yīng)鏈,所以不能直接處理觸摸事件或者手勢(shì)。但是它提供了-containsPoint:-hitTest:來(lái)判斷是否一個(gè)觸點(diǎn)在圖層的范圍之內(nèi)。

與UIView不同,CALayer著重于圖層的繪制,大致為以下職能:

  • 陰影、圓角、邊框、蒙版、拉伸、transform、動(dòng)畫(huà)。
  • 寄宿圖:你可以給CALayer.contents傳遞一個(gè)CGImage來(lái)進(jìn)行渲染,也可以調(diào)用- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;方法進(jìn)行繪制。但通常我們會(huì)使用UIView的drawRect方法
  • CATextLayer:直接將字符串使用Core Graphics寫(xiě)入圖層
  • CATransformLayer:能夠用于構(gòu)造一個(gè)層級(jí)的3D結(jié)構(gòu)

CALayer 內(nèi)部并沒(méi)有屬性,當(dāng)調(diào)用屬性方法時(shí),它內(nèi)部是通過(guò)運(yùn)行時(shí) resolveInstanceMethod為對(duì)象臨時(shí)添加一個(gè)方法,并把對(duì)應(yīng)屬性值保存到內(nèi)部的一個(gè) Dictionary 里,同時(shí)還會(huì)通知 delegate、創(chuàng)建動(dòng)畫(huà)等等,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實(shí)際上都是 CALayer 屬性映射來(lái)的,所以對(duì) UIView 的這些屬性進(jìn)行調(diào)整時(shí),消耗的資源要遠(yuǎn)大于一般的屬性。對(duì)此你在應(yīng)用中,應(yīng)該盡量減少不必要的屬性修改。

當(dāng)視圖層次調(diào)整時(shí),UIView、CALayer 之間會(huì)出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時(shí),應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。

使用圖層關(guān)聯(lián)的視圖而不是單獨(dú)使用 CALayer 的好處在于,你能在使用所
有 CALayer 底層特性的同時(shí),也可以使用 UIView 的高級(jí)API(比如自動(dòng)排版, 布局和事件處理)。做一些對(duì)性能特別挑剔的工作,比如對(duì) UIView 一些可忽略不計(jì)的操作都會(huì)引 起顯著的不同

關(guān)于UIView動(dòng)畫(huà)以及CALayer的動(dòng)畫(huà)這里推薦兩篇文章:
iOS-UIView與CALayer動(dòng)畫(huà)原理
CALayer與iOS動(dòng)畫(huà) 講解及使用

參考

本文大量借助了引用文章的文字描述,在此感謝各位作者的文章對(duì)本問(wèn)題的理解起了很大的幫助。也希望各位能去原文發(fā)表自己的看法。謝謝~

總結(jié)

iOS開(kāi)發(fā)要學(xué)的東西還有很多,因?yàn)闀r(shí)間的推移,每年的iOS崗位要求都在提高,導(dǎo)致我們?cè)趇OS開(kāi)發(fā)崗位的同學(xué)要學(xué)習(xí)很多知識(shí)。例如Runtime、Runloop、音視頻處理、視圖渲染等,各位一起加油吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容