在使用UIKit的過程當中,性能優化是永恆的話題。不少人都看過度析優化滑動性能的文章,但其中很多文章只介紹了優化方法卻對背後的原理避而不談,或者是晦澀難懂並且讀者缺少實踐體驗的機會。不妨思考一下下面的問題本身是否有一個清晰的認識:ios
shouldRasterize
和離屏渲染的關係是什麼,什麼時候應該使用?本文會結合Instrument分析影響性能的因素,提出優化方案並解釋背後的原理,項目初始demo的下載地址在個人Github,強烈建議每一位讀者下載下來隨着我一步一步調試、優化。若是以爲對本身有幫助,能夠給一個Star表示支持。後面的圖片較多,流量黨慎入。git
打開項目後,只要CustomTableCell.swift文件便可,它實現了自定義的UITableViewCell
以及內部的UI佈局,由於重點在於性能優化,代碼實現的就比較隨意。github
首先按下Command + I
打開Instrument,本文主要用到的是Core Animation工具:swift
注意這個調試必須使用真機,點擊左上角的紅色圓圈就會開始錄製。新手可能不太熟悉,這裏簡單介紹一下調試界面:緩存
咱們須要瞭解兩個兩個區域:性能優化
有過遊戲經驗的人也許對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
不少文章裏說把控件設置爲opaque = true
,其原理就是但願避免圖層混合,然而這種調優通常狀況下用處不大。由於UIView
的opaque
屬性默認值就是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」,它表示若是命中緩存則顯示爲綠色,不然顯示爲紅色,顯然綠色越多越好,紅色越少越好。勾選這個選項後咱們看到以下的場景:
光柵化的核心在於緩存的思想。咱們本身動手把玩一下,能夠發現如下幾個有意思的現象:
這是由於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主要處理兩件事:
好比應用中有一些從網絡下載的圖片,而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」會把須要離屏渲染的地方標記爲黃色,大部分狀況下咱們須要儘量避免黃色的出現。離屏渲染可能會自動觸發,也能夠手動觸發。如下狀況可能會致使觸發離屏渲染:
- 重寫drawRect方法
- 有mask或者是陰影(layer.masksToBounds, layer.shadow*),模糊效果也是一種mask
- 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」用於標記發生重繪的區域。一個典型的例子是系統的時鐘應用,絕大多數時候只有顯示秒針的區域須要重繪:
若是你一步一步作到了這裏,我想必定會有很多收益。不過,學而不思則罔,思而不學則殆。動手實踐後仍是應該總結提煉,優化滑動性能主要涉及三個方面:
opaque
屬性設置爲true
,確保backgroundColor
和父視圖顏色一致且不透明alpha
值UIImage
沒有alpha通道frame
一致,不要在滑動時縮放圖片drawRect
方法,設置圓角、陰影、模糊效果,光柵化都會致使離屏渲染本文的demo能夠在個人Github上下載,而後一步一步本身體驗優化過程。但demo畢竟是刻意搭建的一個環境,我會在我本身的仿寫的簡書app上不斷進行實戰優化,歡迎共同窗習交流。
還有一些高質量的問答: