天天看點

iOS頁面性能優化

前言

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

卡頓産生的原因

iOS頁面性能優化

在 iOS 系統中,圖像内容展示到螢幕的過程需要 CPU 和 GPU 共同參與。CPU 負責計算顯示内容,比如視圖的建立、布局計算、圖檔解碼、文本繪制等。随後 CPU 會将計算好的内容送出到 GPU 去,由 GPU 進行變換、合成、渲染。之後 GPU 會把渲染結果送出到幀緩沖區去,等待下一次 VSync 信号到來時顯示到螢幕上。由于垂直同步的機制,如果在一個 VSync 時間内,CPU 或者 GPU 沒有完成内容送出,則那一幀就會被丢棄,等待下一次機會再顯示,而這時顯示屏會保留之前的内容不變。這就是界面卡頓的原因。

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

iOS頁面性能優化

上面的圖展示了 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 中的屬性,減少了文本的計算。

文本渲染

iOS頁面性能優化

螢幕上能看到的所有文本内容控件,包括 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 加載方法有 imageNamed 和 imageWithContentsOfFile。其中 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 使用

iOS頁面性能優化

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

iOS頁面性能優化

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 選項後顯示如下:

iOS頁面性能優化

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

總結

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

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

參考資料

  • http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/