UIView 的渲染過程

  • setNeedsLayout

標(biāo)記為需要重新布局,異步調(diào)用layoutIfNeeded刷新布局,不立即刷新,在下一輪runloop結(jié)束前刷新,對于這一輪runloop之內(nèi)的所有布局和UI上的更新只會刷新一次,layoutSubViews一定會被調(diào)用。

  • layoutIfNeeded

如果有需要刷新的標(biāo)記,立即調(diào)用layoutSubViews進行布局(如果沒有標(biāo)記,不會調(diào)用layoutSubViews)

注意:

  • layoutIfNeeded不一定會調(diào)用layoutSubViews方法
  • setNeedsLayout一定會調(diào)用layoutSubViews方法(有延遲,下一輪runloop結(jié)束前)
  • 如果想在當(dāng)前runloop中立即刷新,調(diào)用順序是
[self setNeedsLayout];
[self layoutIfNeeded];
  • setNeedsDisplay

和setNeedsLayout一樣是異步執(zhí)行的,此方法會調(diào)用drawRect方法。

layoutSubviews 以下情況會調(diào)用
  • addsubview 會觸發(fā)layoutSubviews
  • 設(shè)置View的frame 會觸發(fā)layoutSubviews,當(dāng)然frame的前后得不一樣
  • 滾動一個uiscrollview會觸發(fā)layoutSubviews
  • 旋轉(zhuǎn)screen會觸發(fā)父UIView上的layoutSubviews
  • 改變一個UIView的大小時,會觸發(fā)父UIView上的layoutSubviews
drawRect 在以下情況下會被調(diào)用
  • 如果UIView在初始化時沒有設(shè)置rect大小,將直接導(dǎo)致drawRect不被自動調(diào)用。drawRect調(diào)用是在Controller-》loadview,Controller-》viewDidload兩方法之后調(diào)用的,所以不用擔(dān)心在控制器中,這些view的drawRect就開始畫了,這樣可以在控制器中設(shè)置一些值給view
  • 該方法在調(diào)用sizeToFit后唄調(diào)用,所以可以先調(diào)用sizeToFit計算出size。然后系統(tǒng)自動調(diào)用drawRect方法。
  • 通過設(shè)置contentModel屬性值為UIViewContentModeRedraw,那么將在每次設(shè)置更改frame的時候自動調(diào)用drawRect
  • 直接設(shè)置setNeedsDisplay,或者setNeedsDisplayInRect:觸發(fā)drawRect,但是有個前提條件是rect 不能為0;
drawRect方法使用注意點
  • 使用UIView繪圖,只能在drawRect方法中獲取相應(yīng)的contextRef并繪圖。如果在其他方法中獲取將會獲取到一個invalidate的ref并且不能用于畫圖。drawRect方法不能手動調(diào)用,必須通過調(diào)用setNeedsDisplay或者setNeedsDisplayInRect,讓系統(tǒng)自動調(diào)用該方法。
  • 若使用CALayer繪圖,只能在drawInContext:中(類似于drawRect)繪制,或者在delegate中的相應(yīng)方法繪制。同樣也是調(diào)用setNeedsDisplay等間接調(diào)用以上方法。
  • 若要實時畫圖,不能使用gestureRecognizer,只能使用touchbegin等方法來調(diào)用setNeedsDisplay實時刷新屏幕。
  • sizeToFit

這是一個UIView的方法。意思是調(diào)整和移動view 和view內(nèi)部子視圖的大小和位置。當(dāng)你想要調(diào)整當(dāng)前視圖,以便他使用最合適的空間的時候用這個方法。特定的視圖會根據(jù)內(nèi)部需要進行尺寸調(diào)整。在特定條件下,如果view沒有父視圖,他會根據(jù)屏幕的bounds來進行調(diào)整。如果你需要view根據(jù)父視圖來調(diào)整大小,就需要將view添加到父視圖中取。
其實說白了,就是讓當(dāng)前視圖以最適合的尺寸來調(diào)整大小,參照物是父視圖尺寸,沒有父視圖就用屏幕bounds。
你不可以直接對這個方法進行重寫,如果你需要改變視圖的默認(rèn)尺寸,你可以重寫- (CGSize)sizeThatFits:(CGSize)size這個方法。在這個方法里面可以進行一些必要的計算并在這個方法中對計算的結(jié)果進行返回。

  • - (CGSize)sizeThatFits:(CGSize)size

要求視圖去計算和返回一個最適合的指定尺寸的尺寸。
傳入:是一個指定的最適合的尺寸
返回:是view的子view最適合的尺寸
默認(rèn)實現(xiàn)是返回view自己的size
例如:

  • UISwitch 返回一個固定尺寸的值去顯示一個標(biāo)準(zhǔn)的切換視圖。
  • UIimageview 會返回當(dāng)前顯示image的尺寸。
應(yīng)用場景
  • 對navigationitem 的設(shè)置
  • 對uibarbuttonitem 的設(shè)置
  • UIlable
  • imageview

使用注意點:

  • sizeToFit不應(yīng)該再子類中被重寫,應(yīng)該重寫sizeThatFitssizeToFit會自動調(diào)用sizeThatFits方法
  • sizeThatFits傳入的參數(shù)是receiver當(dāng)前的size,返回一個合適的size
  • sizeToFit可以被手動直接調(diào)用,sizeToFitsizeThatFits方法都沒有遞歸,對subviews不負(fù)責(zé),只負(fù)責(zé)自己。
  • sizeThatFits 不會改變receiver的size,sizeToFit會改變receiver的size
view的繪制渲染機制和runloop有什么關(guān)系

堆棧信息:


image.png
底層實現(xiàn)

在操作UI時,比如改變了frame,更新了UIView/CALayer的層次時,或者手動調(diào)用了UIView/CALayer的setNeedsLayout/setNeedsdisplay 方法后,這個UIView/CALayer就被標(biāo)記為待處理,并被提交到一個全局的容器中去。蘋果注冊了一個Observer 來監(jiān)聽runloop的BeforeWaiting(即將進入休眠)和Exit(即將退出)事件,回調(diào)一個函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),這個函數(shù)會遍歷所有待處理的UIView/CAlayer,以執(zhí)行實際的繪制和調(diào)整,并更新UI界面。

列表卡頓,到底是什么引起的

iOS的mainrunloop 是一個60fps(每秒刷新60次)的回調(diào),也就是說每16.7ms 會繪制一次屏幕,這個時間段內(nèi)要完成view的緩沖區(qū)創(chuàng)建,view內(nèi)容的繪制(如果重寫了drawRect),這些CPU的工作。然后將這個緩沖區(qū)交給GPU渲染,這個過程又包括多個view的拼接(compositing)和紋理的渲染(Texture)等,最終顯示在屏幕上。因此,如果16.7ms 內(nèi)完不成這些操作,比如CPU做了太多的工作,或者view層次過于多,圖片過于大,導(dǎo)致GPU壓力太大,就會導(dǎo)致’卡‘的顯現(xiàn),也就是丟幀。

我們經(jīng)常在drawRect方法里繪制代碼,該方法是誰調(diào)用的 何時調(diào)用的?
  • 在[ZYYView drawRect:]方法之前,調(diào)用了[CALayer drawInContext:] 和 [UIView(CALayerDelegate) drawLayer:inContext:],這幾個方法是在 addSubView:方法之后 才會調(diào)用的,所以drawRect方法是在addSubView:函數(shù)觸發(fā)的。
    image.png
drawrect方法內(nèi)為何第一行代碼總要獲取圖形的上下文

每一個UIView都有一個layer,每一個layer都有個content,這個content指向的是一塊緩存,叫做backing store,當(dāng)UIView被繪制時,CPU執(zhí)行drawRect,通過context 將數(shù)據(jù)寫入backing store,當(dāng)backing store寫完后,通過render server 交給GPU去渲染,將backing store中的bitmap數(shù)據(jù)顯示在屏幕上,所以在drawRect方法中,要首先獲取context。

image.png
視圖繪制過程
  • 創(chuàng)建自定義視圖,初始化坐標(biāo),確認(rèn)是否設(shè)置了frame
  • 沒有設(shè)置frame 就不會調(diào)用 drawRect
  • 設(shè)置frame,調(diào)用addsubview 方法添加視圖
  • 將此view的layer(在初始化的時候就已創(chuàng)建)的delegate 設(shè)置成此view
  • 首先CPU 會為layer 分配一塊內(nèi)存來繪制bitmap,叫做backing store,然后layer創(chuàng)建指向這塊bitmap緩沖區(qū)的指針,叫做CGContextRef
  • 調(diào)用此view的layer的 drawInContext:方法
  • 如果代理實現(xiàn)了drawLayer:inContext: 方法,調(diào)用代理的 drawLayer:inContext:方法,也就是view的 drawLayer:inContext:方法
  • 執(zhí)行drawRect方法
  • 通過CoreGfraphic 的API,也叫做Quartz2D,繪制bitmap。
  • 將layer的content 指向生成的bitmap
    到此CPU的工作已經(jīng)完成,剩下的就是GPU的工作。
view的渲染機制 和GPU之間的關(guān)系

GPU處理的單位是texture
基本上我們控制GPU都是通過open GL來完成,但是從bitmap到texture之間需要一座橋梁,core animation正好充當(dāng)了這個角色,Core Animation對open GL 的api 有一層封裝,當(dāng)我們的要渲染的layer已經(jīng)有了bitmap content的時候,這個content一般來說是一個CGImageRef,CoreAnimation會創(chuàng)建一個open GL的texture,并將CGImageRef(bitmap)和這個texture綁定,通過textureID來標(biāo)識。
這個對應(yīng)關(guān)系建立起來之后,剩下的任務(wù)就是GPU如何將texture渲染到屏幕上了。

從硬件的角度來看

GPU的工作模式:CPU將準(zhǔn)備好的bitmap放到ram里,GPU去搬這塊內(nèi)存到VRAm(顯存的一種)中處理。而這個過程GPU所能承受的極限大概在16.7ms完成一幀的處理。所以最開始提到的60fps其實就是GPU能處理的最高頻率。

總結(jié)視圖的渲染過程
  • 創(chuàng)建視圖,初始化過程(內(nèi)部創(chuàng)建layer)
  • 將視圖添加到父視圖,調(diào)用addSubViews 方法
  • 添加后,將layer的delegate 賦值為此view
  • CPU 創(chuàng)建一塊內(nèi)存緩沖區(qū),名為backing store,用來盛放繪制的bitmap 數(shù)據(jù)。
  • layer 會創(chuàng)建一個指向這個內(nèi)存的指針,CGContextRef 類型(也就是當(dāng)前的上下文對象)
  • 調(diào)用layer的drawInContext: 方法
  • 調(diào)用layer delegate的 drawLayer:Incontext:方法
  • 調(diào)用 drawRect 方法
  • 以上三個方法將繪制的內(nèi)容(bitmap)送到backing store
  • 將繪制好的bitmap 給layer的content ,到此 CPU工作完成
  • GPU將ram 中的bitmap(使用open GL api)渲染到vram 中。
    參考鏈接
    https://blog.csdn.net/yangyangzhang1990/article/details/52452707
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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