1. 程式人生 > 實用技巧 >iOS進階之頁面效能優化

iOS進階之頁面效能優化

前言

在軟體開發領域裡經常能聽到這樣一句話,“過早的優化是萬惡之源”,不要過早優化或者過度優化。我認為在編碼過程中時刻注意效能影響是有必要的,但凡事都有個度,不能為了效能耽誤了開發進度。在時間緊急的情況下我們往往採用“quick and dirty”的方案來快速出成果,後面再迭代優化,即所謂的敏捷開發。與之相對應的是傳統軟體開發中的瀑布流開發流程。

卡頓產生的原因

在 iOS 系統中,影象內容展示到螢幕的過程需要 CPU 和 GPU 共同參與。CPU 負責計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。之後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是介面卡頓的原因。

本人5年iOS開發經驗,曾就職於阿里巴巴。 善於把艱澀的iOS知識轉化為通俗易懂的白話文字,同時也歡迎大家加入小編的iOS交流群 834688868 ,群裡會提供相關面試資料,書籍歡迎大家入駐!

因此,我們需要平衡 CPU 和 GPU 的負荷避免一方超負荷運算。為了做到這一點,我們首先得了解 CPU 和 GPU 各自負責哪些內容。

上面的圖展示了 iOS 系統下各個模組所處的位置,下面我們再具體看一下 CPU 和 GPU 對應了哪些操作。

CPU 消耗型任務

佈局計算

佈局計算是 iOS 中最為常見的消耗 CPU 資源的地方,如果檢視層級關係比較複雜,計算出所有圖層的佈局資訊就會消耗一部分時間。因此我們應該儘量提前計算好佈局資訊,然後在合適的時機調整對應的屬性。還要避免不必要的更新,只在真正發生了佈局改變時再更新。

物件建立

物件建立過程伴隨著記憶體分配、屬性設定、甚至還有讀取檔案等操作,比較消耗 CPU 資源。儘量用輕量的物件代替重量的物件,可以對效能有所優化。比如 CALayer 比 UIView 要輕量許多,如果檢視元素不需要響應觸控事件,用 CALayer 會更加合適。

通過 Storyboard 建立檢視物件還會涉及到檔案反序列化操作,其資源消耗會比直接通過程式碼建立物件要大非常多,在效能敏感的介面裡,Storyboard 並不是一個好的技術選擇。

對於列表型別的頁面,還可以參考 UITableView 的複用機制。每次要初始化 View 物件時先根據 identifier 從快取池裡取,能取到就複用這個 View 物件,取不到再真正執行初始化過程。滑動螢幕時,會將滑出螢幕外的 View 物件根據 identifier 放入快取池,新進入螢幕可見範圍內的 View 又根據前面的規則來決定是否要真正初始化。

Autolayout

Autolayout 是蘋果在 iOS6 之後新引入的佈局技術,在大多數情況下這一技術都能大大提升開發速度,特別是在需要處理多語言時。比如阿拉伯語下佈局是從右往左,通過 Autolayout 設定 leading 和 trailing 即可。

但是 Autolayout 對於複雜檢視來說常常會產生嚴重的效能問題,對於效能敏感的頁面建議還是使用手動佈局的方式,並控制好重新整理頻率,做到真正需要調整佈局時再重新佈局。

文字計算

如果一個介面中包含大量文字(比如微博、微信朋友圈等),文字的寬高計算會佔用很大一部分資源,並且不可避免。

一個比較常見的場景是在 UITableView 中,heightForRowAtIndexPath這個方法會被頻繁呼叫,即使不是耗時的計算在呼叫次數多了之後也會帶來效能損耗。這裡的優化就是儘量避免每次都重新進行文字的行高計算,可以在獲取到 Model 資料後就根據文字內容計算好佈局資訊,然後將這份佈局資訊作為一個屬性儲存到對應的 Model 中,這樣在 UITableView 的回撥中就可以直接使用 Model 中的屬性,減少了文字的計算。

文字渲染

螢幕上能看到的所有文字內容控制元件,包括 UIWebView,在底層都是通過 CoreText 排版、繪製為 Bitmap 顯示的。常見的文字控制元件 (UILabel、UITextView 等),其排版和繪製都是在主執行緒進行的,當顯示大量文字時,CPU 的壓力會非常大。

這一部分的效能優化就需要我們放棄使用系統提供的上層控制元件轉而直接使用 CoreText 進行排版控制。

Wherever possible, try to avoid making changes to the frame of a view that contains text, because it will cause the text to be redrawn. For example, if you need to display a static block of text in the corner of a layer that frequently changes size, put the text in a sublayer instead.

上面這段話引用自 iOS Core Animation: Advanced Techniques,翻譯過來的意思就是說包含文字的檢視在改變佈局時會觸發文字的重新渲染,對於靜態文字我們應該儘量減少它所在檢視的佈局修改。

影象的繪製

影象的繪製通常是指用那些以 CG 開頭的方法把影象繪製到畫布中,然後從畫布建立圖片並顯示的過程。前面的模組圖裡介紹了 CoreGraphic 是作用在 CPU 之上的,因此呼叫 CG 開頭的方法消耗的是 CPU 資源。我們可以將繪製過程放到後臺執行緒,然後在主執行緒裡將結果設定到 layer 的 contents 中。程式碼如下:

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

圖片的解碼

Once an image file has been loaded, it must then be decompressed. This decompression can be a computationally complex task and take considerable time. The decompressed image will also use substantially more memory than the original.

圖片被載入後需要解碼,圖片的解碼是一個複雜耗時的過程,並且需要佔用比原始圖片還多的記憶體資源。

為了節省記憶體,iOS 系統會延遲解碼過程, 在圖片被設定到 layer 的 contents 屬性或者設定成 UIImageView 的 image 屬性後才會執行解碼過程,但是這兩個操作都是在主執行緒進行,還是會帶來效能問題。

如果想要提前解碼,可以使用 ImageIO 或者提前將圖片繪製到 CGContext 中,這部分實踐可以參考 iOS Core Animation: Advanced Techniques

這裡多提一點,常用的 UIImage 載入方法有 imageNamedimageWithContentsOfFile。其中 imageNamed 載入圖片後會馬上解碼,並且系統會將解碼後的圖片快取起來,但是這個快取策略是不公開的,我們無法知道圖片什麼時候會被釋放。因此在一些效能敏感的頁面,我們還可以用 static 變數 hold 住 imageNamed 載入到的圖片避免被釋放掉,以空間換時間的方式來提高效能。

GPU消耗型任務

相對於 CPU 來說,GPU 能幹的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合並渲染,然後輸出到螢幕上。寬泛的說,大多數 CALayer 的屬性都是用 GPU 來繪製。

以下一些操作會降低 GPU 繪製的效能,

大量幾何結構

所有的 Bitmap,包括圖片、文字、柵格化的內容,最終都要由記憶體提交到視訊記憶體,繫結為 GPU Texture。不論是提交到視訊記憶體的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片並且快速滑動時),CPU 佔用率很低,GPU 佔用非常高,介面仍然會掉幀。避免這種情況的方法只能是儘量減少在短時間內大量圖片的顯示,儘可能將多張圖片合成為一張進行顯示。

另外當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。

檢視的混合

當多個檢視(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果檢視結構過於複雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當儘量減少檢視數量和層次,並且減少不必要的透明檢視。

離屏渲染

離屏渲染是指圖層在被顯示之前是在當前螢幕緩衝區以外開闢的一個緩衝區進行渲染操作。

離屏渲染需要多次切換上下文環境:先是從當前螢幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到螢幕上又需要將上下文環境從離屏切換到當前螢幕,而上下文環境的切換是一項高開銷的動作。

會造成 offscreen rendering 的原因有:

  • 陰影(UIView.layer.shadowOffset/shadowRadius/…)
  • 圓角(當 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時)
  • 圖層蒙板
  • 開啟光柵化(shouldRasterize = true)

使用陰影時同時設定 shadowPath 就能避免離屏渲染大大提升效能,後面會有一個 Demo 來演示;圓角觸發的離屏渲染可以用 CoreGraphics 將圖片處理成圓角來避免。

CALayer 有一個 shouldRasterize 屬性,將這個屬性設定成 true 後就開啟了光柵化。開啟光柵化後會將圖層繪製到一個螢幕外的影象,然後這個影象將會被快取起來並繪製到實際圖層的 contents 和子圖層,對於有很多的子圖層或者有複雜的效果應用,這樣做就會比重繪所有事務的所有幀來更加高效。但是光柵化原始影象需要時間,而且會消耗額外的記憶體。

光柵化也會帶來一定的效能損耗,是否要開啟就要根據實際的使用場景了,圖層內容頻繁變化時不建議使用。最好還是用 Instruments 比對開啟前後的 FPS 來看是否起到了優化效果。

注意:
shouldRasterize = true 時記得同時設定 rasterizationScale

Instruments 使用

Instruments 是一系列工具集,我們這裡只演示 Core Animation 的使用。在 Core Animation 選項右下方會看到如下選項,

Color Blended Layers

這個選項選項基於渲染程度對螢幕中的混合區域進行綠到紅的高亮顯示,越紅表示效能越差,會對幀率等指標造成較大的影響。紅色通常是由於多個半透明圖層疊加引起。

Color Hits Green and Misses Red

當 UIView.layer.shouldRasterize = YES 時,耗時的圖片繪製會被快取,並當做一個簡單的扁平圖片來呈現。這時候,如果頁面的其他區塊(比如 UITableViewCell 的複用)使用快取直接命中,就顯示綠色,反之,如果不命中,這時就顯示紅色。紅色越多,效能越差。因為柵格化生成快取的過程是有開銷的,如果快取能被大量命中和有效使用,則總體上會降低開銷,反之則意味著要頻繁生成新的快取,這會讓效能問題雪上加霜。

Color Copied Images

對於 GPU 不支援的色彩格式的圖片只能由 CPU 來處理,把這樣的圖片標為藍色。藍色越多,效能越差。

Color Immediately

通常 Core Animation Instruments 以每毫秒 10 次的頻率更新圖層除錯顏色。對某些效果來說,這顯然太慢了。這個選項就可以用來設定每幀都更新(可能會影響到渲染效能,而且會導致幀率測量不準,所以不要一直都設定它)。

Color Misaligned Images

這個選項檢查了圖片是否被縮放,以及畫素是否對齊。被放縮的圖片會被標記為黃色,畫素不對齊則會標註為紫色。黃色、紫色越多,效能越差。

Color Offscreen-Rendered Yellow

這個選項會把那些離屏渲染的圖層顯示為黃色。黃色越多,效能越差。這些顯示為黃色的圖層很可能需要用 shadowPath 或者 shouldRasterize 來優化。

Color OpenGL Fast Path Blue

這個選項會把任何直接使用 OpenGL 繪製的圖層顯示為藍色。藍色越多,效能越好。如果僅僅使用 UIKit 或者 Core Animation 的 API,那麼不會有任何效果。

Flash Updated Regions

這個選項會把重繪的內容顯示為黃色。不該出現的黃色越多,效能越差。通常我們希望只是更新的部分被標記完黃色。

演示

上述幾個選項中常用來檢測效能的是 Color Blended Layers、Offscreen-Rendered Yellow 和 Color Hits Green and Misses Red。下面我重點演示一下離屏渲染和光柵化的檢測,寫了一個簡單的 Demo 設定了陰影效果,程式碼如下:

    view.layer.shadowOffset = CGSizeMake(1, 1);
    view.layer.shadowOpacity = 1.0;
    view.layer.shadowRadius = 2.0;
    view.layer.shadowColor = [UIColor blackColor].CGColor;
//    view.layer.shadowPath = CGPathCreateWithRect(CGRectMake(0, 0, 50, 50), NULL);

shadowPath 沒有設定時用 Instruments 檢測 FPS 基本在 20 以下(iPhone6裝置),設定了 shadowPath 後基本維持在 55 左右,效能提升十分明顯。

下面來看一下光柵化的檢測,程式碼如下,

    view.layer.shouldRasterize = YES;
    view.layer.rasterizationScale = [UIScreen mainScreen].scale;

勾選 Color Hits Green and Misses Red 選項後顯示如下:

我們可以看到在靜止時快取都生效了,在快速滑動時快取基本不起作用,因此是否要開啟光柵化還是得根據具體場景,用 Instruments 檢測開啟前後的效能來決定。

總結

本文主要總結了效能調優的一些理論知識,後面還介紹了 Instruments 中 Core Animation 的一些效能檢測指標用法。效能優化最重要的是要使用工具來檢測而不是猜測,先檢視是否有離屏渲染等問題,再用 Time Profiler 分析一下耗時的函式呼叫。修改後再用工具分析是否有改善,一步一步執行,小心仔細。

建議大家也實際動手分析一下自己的應用,加深一下印象,enjoy~

參考資料

作者:hi_xgb
連結:https://www.jianshu.com/p/1b5cbf155b31