iOS 性能優(yōu)化-讓UITableView如絲般順滑(布局、離屏渲染、事務(wù)優(yōu)化)

UITableView 是 iOS 中最常用的組件之一,也是承載業(yè)務(wù)最重,交互最多的組件之一。對 UITableView 的優(yōu)化最能體現(xiàn)開發(fā)人員在性能優(yōu)化上的基礎(chǔ)水平,直接關(guān)系到我們的用戶體驗

還記得第一次創(chuàng)建空白的列表么?那如少女肌膚般的順滑令人終身難忘。可時間的推移、業(yè)務(wù)的增長,列表中展示的東西越來越多,少女變大媽,不再順滑,滿是褶皺...


???

這.........
我們絕不接受,我們要大媽返老還童!
快上車,來不及解釋了。


為什么會卡頓?

與游戲類似,游戲中的 FPS 通常可以用來代表當(dāng)前的流暢程度。當(dāng) FPS 小于30幀每秒時人眼就會感覺到畫面不夠連續(xù),有撕裂感。這也是所有游戲必須保證 FPS 在任何情況下都維持在30以上的原因,是不影響玩家游戲體驗的最低標(biāo)準(zhǔn)。我們的APP也是同理的。

無論是游戲,還是程序應(yīng)用,用戶看到的都是一張張連續(xù)的圖片?;谌搜鄣奶攸c,當(dāng)畫面達到一定刷新率時,人眼就會認為其是連續(xù)的,把這認為是欺騙我們的眼睛也不為過。
高刷新率對硬件和程序性能有著苛刻的要求,但是過低的刷新率會給用戶造成不適,當(dāng)刷新率降低到 24 以下時,人眼就能明顯的感覺到畫面的卡頓/不連貫。無論何時,刷新率都應(yīng)當(dāng)保持在 30 以上。

為什么我們的畫面幀率會降低到30以下?
蘋果的屏幕幀率很早就達到60了,單位時間內(nèi)幀率降低到30以下,說明某些幀繪制超時了。主要原因如下:

1.復(fù)雜的事務(wù)
2.低效的繪制

復(fù)雜的事務(wù)優(yōu)化起來需要因地制宜。例如網(wǎng)絡(luò)請求、復(fù)雜的任務(wù)隊列。這些問題大多可以使用多線程,優(yōu)化業(yè)務(wù)流程解決。

低效繪制

造成低效繪制的原因有很多,常見的有三點:

  1. 復(fù)雜的界面布局
  2. 離屏渲染
  3. 繁重的繪制
1.復(fù)雜的界面布局

在 iOS 中,界面布局無非 Autolayout 和 Frame 兩種方式,XIB,SB 都是對 Autolayout 的更高級的可視化封裝。
(SwiftUI未來一年還將處于試水的階段,這里暫不討論)。

Frame布局的優(yōu)點在于:程序不需要翻譯約束來計算視圖的位置,直接繪制即可。

采用Frame布局的方式時,視圖的位置大小信息由開發(fā)人員編寫計算完成。低效的計算代碼也可能會影響到布局的速度,盡可能采取簡單的計算方式,避免循環(huán)、迭代、事務(wù)等待的情況發(fā)生。
將視圖的布局信息提前到編碼階段,對開發(fā)人員的水平、工作量、及規(guī)范程度的要求都會極大的提高,容錯性也越低,若代碼不夠規(guī)范,維護起來簡直就是地獄。

Autolayout 會通過各種約束計算轉(zhuǎn)換成最終的視圖位置參數(shù),過程是機械式的,越是復(fù)雜的界面就越可能耗費更長的時間。但其勝在容錯性高、可視化、快速開發(fā)、易于調(diào)試等優(yōu)點。

(小結(jié)):
界面越是簡單,布局方式影響性能的幾率就越低,而程序只要在當(dāng)前幀完成繪制,就不存在任何問題。當(dāng)界面越來越復(fù)雜,布局的影響就必須重視。時間充裕的情況下,應(yīng)當(dāng)盡可能降低”翻譯約束“的發(fā)生。
在開發(fā)中,也需要權(quán)衡,花費大量的時間去提升0.01秒的速度是否值得。

2.離屏渲染

【什么是離屏渲染?】
程序在屏幕以外的地方生成了一塊用于繪制的緩沖區(qū)域,當(dāng)前幀在緩沖區(qū)域繪制完成后,直接將結(jié)果顯示到屏幕上,這整個過程稱作離屏渲染。離屏渲染可以防止畫面撕裂等情況發(fā)生,在游戲等視頻類領(lǐng)域大量使用。

【iOS 中為什么會有這項技術(shù)?】
在屏幕繪制的過程中,很多時候并不能一次性得到最終的顯示效果,如:有“透明度”的視圖,有“遮罩”效果的圖層,有“圓角”的圖片等等。
我們從屏幕上看到的這些效果都是計算機對各視圖或圖層的屬性混合后的最終效果,而這個 “計算混合“ 的過程就需要另開辟一塊緩沖區(qū)域進行繪制,即“離屏渲染”,離屏渲染完成后將結(jié)果復(fù)制到屏幕上。

(小結(jié)):
離屏渲染并不是什么Bug,它是計算機圖像繪制過程中非常重要的一項技術(shù)。之所以會被列入優(yōu)化的清單,因其會 創(chuàng)建緩沖區(qū) 域用于計算圖像的混合結(jié)果,比較耗費性能。
當(dāng)一個界面大量出現(xiàn)這樣需要使用離屏渲染技術(shù)時,消耗的系統(tǒng)資源也就越多。尤其是類似 UITableView 這類組件滾動時,離屏渲染所占用的系統(tǒng)資源會呈指數(shù)上升,影響用戶體驗。
在可能頻繁更新的組件或區(qū)域中盡可能的杜絕使用到離屏渲染,防止在某些的情況下(如:快速滾動)突破性能峰值。

3.大量的繪制

無論我們布局多么簡單,又或是我們避免了離屏渲染。我們還是會遇上渲染性能問題。例如:

  1. 圖片顯示問題
  2. 大量的繪制

關(guān)于圖片,可以分為大圖顯示,多圖加載等。關(guān)于繪制,又可以分為動效動畫,多圖形繪制。這里就不展開討論圖片的繪制原理,網(wǎng)上有很多講解的博客。

(小結(jié))
默認情況下,繪制都是由GPU負責(zé),但在圖形和圖片的繪制過程中,可以利用 CPU 幫忙負擔(dān)一部分開銷的。

優(yōu)化原理總結(jié):
性能優(yōu)化的本質(zhì)是防止超過性能峰值的情況發(fā)生,并盡可能的降低系統(tǒng)資源消耗的程度。


具體優(yōu)化手段

布局優(yōu)化

因布局的卡頓導(dǎo)致的問題主要出現(xiàn)在視圖初次顯示時,例如跳轉(zhuǎn)到新頁面時、Cell初次出現(xiàn)時。

在對性能或開發(fā)速度沒有明確的要求時,合理采用不同的布局手段,在效率和性能中做好平衡,例如,在復(fù)雜的頁面布局中適當(dāng)降低 Autolayout 的占比。
可以的話,將界面的子視圖延后加載,先讓界面顯示出來,再來加載子視圖。

離屏渲染

離屏渲染發(fā)生在圖層混合時,盡可能避免離屏渲染在高頻率顯示的地方出現(xiàn)。
可以將某些圖形繪制交由CPU完成,給GPU減負,降低性能峰值.

例如: 在 drawRect: 中使用 CoreGraphics 繪制圖形、圓角等。

有時候,不得不利用到離屏渲染。如我們的Cell中有一個陰影效果,可以將第一次混合出來的結(jié)果 緩存 起來給其他Cell使用,避免重復(fù)渲染。

Cell 的復(fù)用

在 UITableView 中,蘋果給我們提供了 “復(fù)用” 功能,讓程序僅生成剛好夠顯示數(shù)量的 Cell,通過復(fù)用 Cell 來避免創(chuàng)建無窮無盡 Cell 的慘案發(fā)生。

復(fù)用 Cell 的過程中要避免頻繁修改Cell布局的情況發(fā)生。復(fù)用Cell的過程中修改布局相當(dāng)于重新計算繪制一次,極易影響到滑動的流暢性。正確做法應(yīng)當(dāng)是對不同的布局注冊為不同的標(biāo)識符(Identifier).這種會動態(tài)更改布局的 Cell 不能進行提前注冊(register(nib:forCellReuseIdentifier:)),而是需要對不同的布局分別進行注冊,如下:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.row < 10 {
            var cell = tableView.dequeueReusableCell(withIdentifier: TestCellID)
            if cell == nil {
                cell = TestCell(style: .default, reuseIdentifier: TestCellID)
            }
            return cell
        } 
        var cell = tableView.dequeueReusableCell(withIdentifier: TestCellSmallID)
        if cell == nil {
            cell = TestCell(style: .default, reuseIdentifier: TestCellSmallID)
            //這里各種修改布局
        }
        return cell
    }

動態(tài)高度

Cell 不總是等高的,當(dāng)我們的高度為動態(tài)高度時,在 Cell 的復(fù)用過程中,高度的更新意味著復(fù)用 Cell 的約束需要更新,進而 Cell 的繪制也需要更新。這里面就還牽扯到了Cell 本身的布局方式,以及高度的獲取效率。

在 iOS8 之前,開發(fā)者常會將Cell的布局信息緩存起來,不用每次重新計算。

從 iOS8 開始,蘋果在 UITableView 中加入了基于 Autolayout 的自動高度屬性

rowHieght = UITableViewAutomaticDimension

嗯哼~
在前面布局的部分提到過,Autolayout 在復(fù)雜的布局中性能會明顯低于 Frame。更何況是在會頻繁復(fù)用的動態(tài)高度的 Cell 中。

所以,動態(tài)高度時我們需要做到兩點:

  1. Cell 越是復(fù)雜,越需要注意因布局導(dǎo)致的性能問題(Frame > Autolayout)
  2. 將高度計算結(jié)果緩存起來進行復(fù)用,避免不必要的重復(fù)計算。
耗時的操作(圖形繪制、資源加載等)

UITableView 展示過程中不可避免的會加載某些資源,如圖片、音頻、視頻。這些資源從加載到解碼再到顯示出來都比較耗費時間,單純的加載顯示我們可以通過多種方式減緩性能峰值:

  1. 對資源進行壓縮,降低分辨率、碼率等方式,畢竟列表中大多時候只是一個預(yù)覽效果,不需要全長全質(zhì)加載顯示。
  2. 異步執(zhí)行事務(wù),在子線程中處理,防止負責(zé)繪制的主線程出現(xiàn)等待狀態(tài)。
  3. 分解單個事務(wù), 以時間換取空間(性能),降低性能峰值。

例1:在滾動時,我們并不需要 cell 立刻顯示出內(nèi)容,尤其是在高速滾動時,我們可能根本就不需要 cell 顯示。這時候通過維護一個事務(wù)隊列來管理cell的狀態(tài)很有必要。
例2:上面提到的大圖加載,同樣可以將單次加載分解為多個部分.


性能優(yōu)化沒有最好,只有最適合,具體情況具體分析,奇淫技巧各顯神通~
時刻留意性能,是一個優(yōu)秀的開發(fā)者必備的特質(zhì),哪怕是在硬件性能越來越強的今天。


“還想要返老還童的大媽?還是覺得不夠順滑?
Too young too native!”
帥氣的筆者已經(jīng)焊死了車門,并撕開了自己的襯衣...

最后編輯于
?著作權(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ù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

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