UIKit性能調優實戰講解

在使用UIKit的過程當中,性能優化是永恆的話題。不少人都看過度析優化滑動性能的文章,但其中很多文章只介紹了優化方法卻對背後的原理避而不談,或者是晦澀難懂並且讀者缺少實踐體驗的機會。不妨思考一下下面的問題本身是否有一個清晰的認識:ios

  1. 爲何要把控件儘可能設置成不透明的,若是是透明的會有什麼影響,如何檢測這種影響?
  2. 爲何cell中的圖片,儘量要使用正確的大小、格式,若是錯誤會有什麼影響,如何檢測這種影響?
  3. 爲何設置陰影和圓角有可能影響滑動時流暢度?
  4. shouldRasterize和離屏渲染的關係是什麼,什麼時候應該使用?

本文會結合Instrument分析影響性能的因素,提出優化方案並解釋背後的原理,項目初始demo的下載地址在個人Github,強烈建議每一位讀者下載下來隨着我一步一步調試、優化。若是以爲對本身有幫助,能夠給一個Star表示支持。後面的圖片較多,流量黨慎入。git

基本概念

打開項目後,只要CustomTableCell.swift文件便可,它實現了自定義的UITableViewCell以及內部的UI佈局,由於重點在於性能優化,代碼實現的就比較隨意。github

首先按下Command + I打開Instrument,本文主要用到的是Core Animation工具:swift

打開Core Animation調試

注意這個調試必須使用真機,點擊左上角的紅色圓圈就會開始錄製。新手可能不太熟悉,這裏簡單介紹一下調試界面:緩存

調試界面

咱們須要瞭解兩個兩個區域:性能優化

  1. 這裏記錄了實時的fps數值,有些地方是0是由於屏幕沒有滑動
  2. 這是重中之重,接下來我會帶你們逐個理解、體驗這些調試選項

有過遊戲經驗的人也許對fps這個概念比較熟悉。咱們知道任何屏幕老是有一個刷新率,好比iphone推薦的刷新率是60Hz,也就是說GPU每秒鐘刷新屏幕60次,所以兩次刷新之間的間隔爲16.67ms。這段時間內屏幕內容保持不變,稱爲一幀(frame),fps表示frames per second,也就是每秒鐘顯示多少幀畫面。對於靜止不變的內容,咱們不須要考慮它的刷新率,但在執行動畫或滑動時,fps的值直接反映出滑動的流暢程度。bash

調試、優化

圖層混合

首先咱們要明白像素的概念,屏幕上每個點都是一個像素,像素有R、G、B三種顏色構成(有時候還帶有alpha值)。若是某一塊區域上覆蓋了多個layer,最後的顯示效果受到這些layer的共同影響。舉個例子,上層是藍色(RGB=0,0,1),透明度爲50%,下層是紅色(RGB=1,0,0)。那麼最終的顯示效果是紫色(RGB=0.5,0,0.5)。這種顏色的混合(blending)須要消耗必定的GPU資源,由於實際上可能不止只有兩層。若是隻想顯示最上層的藍色,能夠把它的透明度設置爲100%,這樣GPU會忽略下面全部的layer,從而節約了不少沒必要要的運算。網絡

第一個調試選項"Color Blended Layers"正是用於檢測哪裏發生了圖層混合,並用紅色標記出來。所以咱們須要儘量減小看到的紅色區域。一旦發現應該想法設法消除它。開始調試後勾選這個選項,咱們在手機上能夠看到以下的場景:app

Color Blended Layers

不少文章裏說把控件設置爲opaque = true,其原理就是但願避免圖層混合,然而這種調優通常狀況下用處不大。由於UIViewopaque屬性默認值就是true,也就是說只要不是人爲設置成透明,都不會出現圖層混合。好比demo中就沒有任何透明的控件。iphone

對於UIImageView來講,不只它自身須要是不透明的,它的圖片也不能含有alpha通道,這就是爲何圖中第三個圖片是綠色,而前兩個圖片是紅色的緣由。因爲本人對PS和圖像幾乎一竅不通,恕我不能演示如何消除這些圖片的紅色。我從網上找了一個美女的頭像來講明,圖像自身的性質可能會對結果有影響,所以若是你肯定本身的代碼沒有問題,並且出現了圖層混合,請聯繫美工或後臺解決。

我的認爲比opaque屬性更重要的是backgroundColor屬性,若是不設置這個屬性,控件依然被認爲是透明的,因此咱們作的第一個優化是在CustomTableCell類的init方法中添加一行代碼:

label.backgroundColor = UIColor.whiteColor()
複製代碼

雖然在白色背景下,這行代碼沒法肉眼看到效果,但從新調試後咱們能夠發現label的紅色消失了。也正是由於對背景顏色的不重視,它成了影響滑動性能的第一個殺手。

PS:若是label文字有中文,依然會出現圖層混合,這是由於此時label多了一個sublayer,若是有好的解決辦法歡迎告訴我。

光柵化

光柵化是將一個layer預先渲染成位圖(bitmap),而後加入緩存中。若是對於陰影效果這樣比較消耗資源的靜態內容進行緩存,能夠獲得必定幅度的性能提高。demo中的這一行代碼表示將label的layer光柵化:

label.layer.shouldRasterize = true
複製代碼

Instrument中,第二個調試選項是「Color Hits Green and Misses Red」,它表示若是命中緩存則顯示爲綠色,不然顯示爲紅色,顯然綠色越多越好,紅色越少越好。勾選這個選項後咱們看到以下的場景:

Color Hits Green and Misses Red

光柵化的核心在於緩存的思想。咱們本身動手把玩一下,能夠發現如下幾個有意思的現象:

  1. 上下微小幅度滑動時,一直是綠色
  2. 上下較大幅度滑動,新出現的label一開始是紅色,隨後變成綠色
  3. 若是靜止一秒鐘,剛開始滑動時會變紅。

這是由於layer進行光柵化後渲染成位圖放在緩存中。當屏幕出現滑動時,咱們直接從緩存中讀取而沒必要渲染,因此會看到綠色。當新的label出現時,緩存中沒有個這個label的位圖,因此會變成紅色。第三點比較關鍵,緩存中的對象有效期只有100ms,即若是在0.1s內沒有被使用就會自動從緩存中清理出去。這就是爲何停留一下子再滑動就會看到紅色。

光柵化的緩存機制是一把雙刃劍,先寫入緩存再讀取有可能消耗較多的時間。所以光柵化僅適用於較複雜的、靜態的效果。經過Instrument的調試發現,這裏使用光柵化常常出現未命中緩存的狀況,若是沒有特殊須要則能夠關閉光柵化,因此咱們作的第二個優化是註釋掉下面這行代碼:

// label.layer.shouldRasterize = true
複製代碼

光柵化會致使離屏渲染,這一點待會兒會講。

顏色格式

像素在內存中的佈局和它在磁盤中的存儲方式並不相同。考慮一種簡單的狀況:每一個像素有R、G、B和alpha四個值,每一個值佔用1字節,所以每一個像素佔用4字節的內存空間。一張1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600個像素,所以佔用了超過8Mb的內存。可是一張一樣分辨率的PNG格式或JPEG格式的圖片通常狀況下不會有這麼大。這是由於JPEG將像素數據進行了一種很是複雜且可逆的轉化。

當咱們打開JPEG格式的圖片時,CPU會進行一系列運算,將JPEG圖片解壓成像素數據。顯然這個工做會消耗很多時間,因此不該該在滑動時進行,咱們應該預先處理好圖片。借用WWDC上的一頁PPT來講明:

顯示流程

Commit Transaction和Decode在同一幀內進行,若是這兩個操做的耗時超過16.67ms,Draw Calls就會延遲到下一幀,從而致使fps值的下降。下面是Commit Transaction的詳細流程:

解碼與轉換

在第三步的Prepare中,CPU主要處理兩件事:

  1. 把圖片從PNG或JPEG等格式中解壓出來,獲得像素數據
  2. 若是GPU不支持這種顏色各式,CPU須要進行格式轉換

好比應用中有一些從網絡下載的圖片,而GPU剛好不支持這個格式,這就須要CPU預先進行格式轉化。第三個選項「Color Copied Images」就用來檢測這種實時的格式轉化,若是有則會將圖片標記爲藍色。

遺憾的是因爲我對圖片格式不太瞭解,也不會使用相關工具,並無能模擬出觸發這個選項的場景。咱們要記住的是,若是調試時發現有圖片被標記爲藍色,說明圖片格式出現了一些問題。

圖片大小

第四個選項的使用場景很少,咱們直接看一下第五個選項「Color Misaligned Images」。它表示若是圖片須要縮放則標記爲黃色,若是沒有像素對齊則標記爲紫色。勾選上這個選項並進行調試,能夠看到以下場景:

圖片縮放

在demo中,每一個UIImageView的大小都是180x180,而只有第二張圖片的像素大小是360x360。所以除了第二張圖片,其餘的圖片都須要被縮放。圖片的縮放須要佔用時間,所以咱們要儘量保證不管是本地圖片仍是從網絡或取得圖片的大小,都與其frame保持一致。

第三個優化是調整全部圖片的像素大小以免沒必要要的縮放。

離屏渲染

離屏渲染表示渲染髮生在屏幕以外,你可能認爲這是一句廢話。爲了真正解釋清楚什麼是離屏渲染,咱們先來看一下正常的渲染通道(Render-Pass):

正常渲染通道

首先,OpenGL提交一個命令到Command Buffer,隨後GPU開始渲染,渲染結果放到Render Buffer中,這是正常的渲染流程。可是有一些複雜的效果沒法直接渲染出結果,它須要分步渲染最後再組合起來,好比添加一個蒙版(mask):

離屏渲染

在前兩個渲染通道中,GPU分別獲得了紋理(texture,也就是那個相機圖標)和layer(藍色的蒙版)的渲染結果。但這兩個渲染結果沒有直接放入Render Buffer中,也就表示這是離屏渲染。直到第三個渲染通道,才把二者組合起來放入Render Buffer中。離屏渲染意味着把渲染結果臨時保存,等用到時再取出,所以相對於普通渲染更佔用資源。

第六個選項「Color Offscreen-Rendered Yellow」會把須要離屏渲染的地方標記爲黃色,大部分狀況下咱們須要儘量避免黃色的出現。離屏渲染可能會自動觸發,也能夠手動觸發。如下狀況可能會致使觸發離屏渲染:

  1. 重寫drawRect方法
  2. 有mask或者是陰影(layer.masksToBounds, layer.shadow*),模糊效果也是一種mask
  3. layer.shouldRasterize = true

前二者會自動觸發離屏渲染,第三種方法是手動開啓離屏渲染。

開始調試並勾選「Color Offscreen-Rendered Yellow」,會看到這樣的場景:

離屏渲染

若是沒有進行第二步優化,你會發現label也是黃色。能夠看到tabbar和statusBar也是黃色,這是由於它們使用了模糊效果。圖片也是黃色,這說明它也進行了離屏渲染,觀察源碼後發現主要緣由是它使用了陰影,接下來咱們進行第四個優化,在設置陰影效果的四行代碼下面添加一行:

imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
複製代碼

這行代碼制定了陰影路徑,若是沒有手動指定,Core Animation會去自動計算,這就會觸發離屏渲染。若是人爲指定了陰影路徑,就能夠免去計算,從而避免產生離屏渲染。

設置cornerRadius自己並不會致使離屏渲染,但不少時候它還須要配合layer.masksToBounds = true使用。根據以前的總結,設置masksToBounds會致使離屏渲染。解決方案是儘量在滑動時避免設置圓角,若是必須設置圓角,可使用光柵化技術將圓角緩存起來:

// 設置圓角
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
複製代碼

快速路徑

還記得以前將離屏渲染和渲染路徑時的示意圖麼,離屏渲染的最後一步是把此前的多個路徑組合起來。若是這個組合過程能由CPU完成,就會大量減小GPU的工做。這種技術在繪製地圖中可能用到。

第七個選項「Color Compositing Fast-Path Blue」用於標記由硬件繪製的路徑,藍色越多越好。

變化區域

刷新視圖時,咱們應該把須要重繪的區域儘量縮小。對於未發生變化的內容則不該該重繪,第八個選項「Flash updated Regions」用於標記發生重繪的區域。一個典型的例子是系統的時鐘應用,絕大多數時候只有顯示秒針的區域須要重繪:

重繪區域

總結

若是你一步一步作到了這裏,我想必定會有很多收益。不過,學而不思則罔,思而不學則殆。動手實踐後仍是應該總結提煉,優化滑動性能主要涉及三個方面:

避免圖層混合

  1. 確保控件的opaque屬性設置爲true,確保backgroundColor和父視圖顏色一致且不透明
  2. 如無特殊須要,不要設置低於1的alpha
  3. 確保UIImage沒有alpha通道

避免臨時轉換

  1. 確保圖片大小和frame一致,不要在滑動時縮放圖片
  2. 確保圖片顏色格式被GPU支持,避免勞煩CPU轉換

慎用離屏渲染

  1. 絕大多數時候離屏渲染會影響性能
  2. 重寫drawRect方法,設置圓角、陰影、模糊效果,光柵化都會致使離屏渲染
  3. 設置陰影效果是加上陰影路徑
  4. 滑動時若須要圓角效果,開啓光柵化

實戰

本文的demo能夠在個人Github上下載,而後一步一步本身體驗優化過程。但demo畢竟是刻意搭建的一個環境,我會在我本身的仿寫的簡書app上不斷進行實戰優化,歡迎共同窗習交流。

參考資料:

  1. 繪製像素到屏幕上,原文:Getting Pixels onto the Screen
  2. Advanced Graphics and Animations for iOS Apps:這是2014年WWDC Session 419,強烈建議看一遍。
  3. 如何正確地寫好一個界面
  4. Mastering UIKit Performance

還有一些高質量的問答:

  1. What triggers 「Color Copied Images」 and 「Color Hits Green and Misses Red」 in Instruments?
  2. UILabel is marked as red when Color Blended Layers is selected
  3. What triggers offscreen rendering, blending and layoutSubviews in iOS?
相關文章
相關標籤/搜索