UI技術總結--界面性能優化

前言

在iOS開發中,保持界面流暢和良好的用戶體驗是非常重要的,那麼界面的優化對我們來說就是一個老生常談的話題,這裏總結了網上的一些資料和自己的一些開發經驗,講講界面性能的一些技巧.

UI卡頓、掉幀原因


借用網上一張圖來了解一下圖像內容展示到屏幕的過程.最上面是VSync垂直信號,我們一般說頁面滑動的流暢是60FPS,指的就是每秒鐘會有60 幀的畫面更新,我們在人眼上看到的就是流暢的效果,那基於此呢,每個16.7ms(1/60s)就要產生一幀畫面,那麼在這16.7ms中就需要由CPU和GPU共同協同完成產生最終的一幀的數據並通過數模轉換傳遞給顯示器顯示.
其中CPU負責計算顯示內容,比如視圖的創建、佈局計算、圖片解碼、文本繪製等,隨後把產生的位圖提交給GPU,再由GPU進行圖層的合成、紋理渲染等.然後GPU將渲染結果提交到幀緩衝區去,等待下一次 VSync 信號到來時顯示到屏幕上.而如果這個時候CPU或GPU處理的時間過長,當下一次VSync到來的時候,GPU還沒有將渲染結果提交到幀緩衝區中去,那麼這一幀就會被丟棄,畫面繼續保持之前的內容,等下下次VSync信號到來的時候(如果此時GPU已將渲染結果提交到了幀緩衝區中),再顯示並刷新界面.所以本來16.7ms後就該更新一幀畫面的,現在用了33.4ms(可能更多),這就是卡頓產生的原因.
那麼界面優化就主要從CPU和GPU兩個層面來優化了.

CPU

  • 佈局計算、文本計算

視圖的佈局計算是界面優化的一個重點,一般對於視圖佈局的優化可以通過提前計算好視圖佈局和對視圖佈局進行緩存.比如cell的高度、label的行高等涉及到計算的.對於比較複雜的界面,如果使用Autolayout也會帶來性能上的問題,Masonry這個庫就是基於Autolayout.因爲過多的約束帶來的計算量是非常大的,給CPU的消耗非常大.那麼這就容易造成CPU運算時間超時產生卡頓.如果用Frame的話會好很多.這篇文章比較了Frame和Autolayout對性能的影響. 可以使用SDAutoLayout這個庫,它的佈局是基於Frame的,或者使用 ComponentKit、AsyncDisplayKit 等框架.同樣Ulabel的寬高和繪製方法[NSAttributedString boundingRectWithSize:options:context:][NSAttributedString drawWithRect:options:context:]也可以放在後臺線程進行以避免阻塞主線程.

  • 對象創建

對象的創建會分配內存、調整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源,所以將對象的創建放到子線程中去做會提高部分性能.通過 Storyboard 創建視圖對象還會涉及到文件反序列化操作,比起代碼創建的視圖,Storyboard要消耗更多的資源,所以對於一些對性能比較高的界面最好是通過代碼來進行創建和佈局.如果沒有涉及到觸摸事件等操作,可以用CALayer替代UIView,因爲CALayer相對於UIView來說要輕量很多.

  • 文本渲染

如上圖所示,屏幕上能看到的所有文本內容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪製爲 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪製都是在主線程進行的,當顯示大量文本時,CPU 的壓力會非常大.所以可以自定義控件,直接使用 CoreText 進行排版控制,不過這樣太麻煩了,反正我不會這麼做,哈哈哈.

  • 圖像的繪製

圖像的繪製是需要消耗CPU資源的,將繪製過程放在後臺線程中進行,然後再在主線程中將結果設置到layer的contents中,這樣會提高CPU的效率.代碼如下:

- (void)display {
    dispatch_async(backgroundQueue, ^{
        CGContextRef ctx = CGBitmapContextCreate(...);
        CGImageRef img = CGBitmapContextCreateImage(ctx);
        CFRelease(ctx);
        dispatch_async(mainQueue, ^{
            layer.contents = img;
        });
    });
}

圖像的繪製通常是指用那些以 CG 開頭的方法把圖像繪製到畫布中,然後從畫布創建圖片並顯示的過程。前面的模塊圖裏介紹了 CoreGraphic 是作用在 CPU 之上的,因此調用 CG 開頭的方法消耗的是 CPU 資源。我們可以將繪製過程放到後臺線程,然後在主線程裏將結果設置到 layer 的 contents 中.

GPU

相對於 CPU 來說,GPU 能幹的事情比較單一:主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。

  • 紋理的渲染

所有的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定爲 GPU Texture。不論是提交到顯存的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片並且快速滑動時),CPU 佔用率很低,GPU 佔用非常高,界面仍然會掉幀.

  • 視圖的混合

當多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結構過於複雜,混合的過程也會消耗很多 GPU 資源。爲了減輕這種情況的 GPU 消耗,應用應當儘量減少視圖數量和層次,並在不透明的視圖裏標明 opaque 屬性以避免無用的 Alpha 通道合成。當然,這也可以用上面的方法,把多個視圖預先渲染爲一張圖片來顯示.

  • 離屏渲染

CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發離屏渲染(offscreen rendering),而離屏渲染通常發生在 GPU 中。當一個列表視圖中出現大量圓角的 CALayer,並且快速滑動時,可以觀察到 GPU 資源已經佔滿,而 CPU 資源消耗很少。這時界面仍然能正常滑動,但平均幀數會降到很低。爲了避免這種情況,可以嘗試開啓 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉嫁到 CPU 上去。對於只需要圓角的某些場合,也可以用一張已經繪製好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在後臺線程繪製爲圖片.設置了以下屬性時,都會觸發離屏渲染:

  • layer.shouldRasterize,光柵化

  • layer.mask,遮罩

  • layer.allowsGroupOpacity爲YES,layer.opacity的值小於1.0

  • layer.cornerRadius,並且設置layer.masksToBoundsYES。可以使用剪切過的圖片,或者使用layer畫來解決

  • layer.shadows,(表示相關的shadow開頭的屬性),使用shadowPath代替

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章