iOS性能優化系列篇之「列表流暢度優化」

這一篇文章是iOS性能優化系列文章的的第二篇,主要內容是關於列表流暢度的優化。在具體內容的闡述過程當中會結合性能優化的整體原則進行闡述,因此建議你們在閱讀這篇文章前先閱讀一下上一篇文章:iOS性能優化系列篇之「優化整體原則」ios

因爲平時工做比較忙,兩篇之間的間隔有點久。但這兩篇文章出乎我意料地受到了你們的喜歡,因此我但願後面有時間能把這個系列更新下去,下一步準備寫一篇關於iOS內存相關的優化文章。也但願這篇列表流暢度優化的文章可以給你們帶來一點點啓示。算法

和上一篇綜述性質的文章不一樣,這一篇文章工程實用性更強一些,更多的是一些優化技術細節。文中討論了許多可能影響列表流暢度的因素,因爲2018 WWDC裏面講述了大量的關於性能優化相關的內容,所以本文也在相關的內容裏面加入2018 WWDC的性能優化部分。緩存

讀者可將本體說起的優化手段或者原理應用到本身的項目中去。可是但願你們在優化過程當中,要結合本身的項目具體問題具體分析,由於本文討論的影響流暢度的因素,可能並非你的應用流暢性不佳的瓶頸,根據個人經驗,大部分流暢的問題都是業務邏輯致使的,反倒什麼離屏渲染啊之類你們耳熟能詳的流暢度的影響因素在實際項目中並無想象的那麼大。若是不經實地測量就盲目應用一些優化手段,可能會致使過分優化,事倍功半。安全

卡頓產生的緣由

在整體原則篇中提到,五大原則中的其中一個就是要理解優化任務的底層運行機制,由於只有深刻了解底層機制才能更好的有針對性的提出更優的解決方案,因此在進行列表流暢度優化前,咱們必定要弄清楚一個view從建立到顯示到屏幕上都經歷了那些過程,在這些過程當中那些方面可能會致使性能瓶頸,以及形成卡頓的底層緣由是什麼。性能優化

咱們知道iOS設備大部分狀況下,屏幕刷新頻率是60hz(ProMotion下是120hz),也就是每隔16.67ms會進行一次屏幕刷新。每次刷新時,須要CPU和GPU配合完成一次圖像顯示。其主要流程以下:服務器

應用內:網絡

  • 佈局。CPU建立view,設置其屬性(frame、background color等等)
  • 建立backing images。setContents將一個image傳給layer或者經過 drawRect:或 drawLayer:inContext繪製
  • 準備。Core Animation將layer發送到render server前的一些準備工做,好比圖片解碼等。
  • 提交。Core animation將layers打包經過 IPC (Inter-Process Communication) 發送到render server

應用外(render server):多線程

  • 設置用來渲染的OpenGL triangles(若是是有動畫,還需計算動畫layer的屬性的中間值)。
  • 渲染這些可見的triangles,將結果提交到視頻緩衝區
  • 視頻控制器以60hz頻率讀取緩衝區內容顯示到顯示器,若是在16.67ms內沒有完成提交,則會被丟棄。

從上面的圖中能夠看到,在view顯示的過程當中,CPU和GPU都各自承擔了不一樣的任務,CPU和GPU不論哪一個阻礙了顯示流程,都會形成掉幀現象。因此優化方法也須要分別對CPU和GPU壓力進行評估和優化,在CPU和GPU壓力之間找到性能最優的平衡點, 不管過分優化哪一方致使另外一方壓力過大都會形成總體FPS性能的降低。而尋找平衡點的過程則因項目特色不一樣而不一樣,並無一套通用的方法,須要咱們用instrument等性能評測工具,根據實際app的性能度量結果去作優化,不能憑空亂猜。併發

CPU優化

咱們先看table view在滑動過程當中CPU佔用的狀況。app

instruments 截圖

從上圖能夠看出,在滑動過程當中CPU佔用特色是:

  • 滑動時CPU佔用率高、空閒時CPU佔用率底
  • 主線程CPU佔用高、子線程CPU佔底

根據上述特色咱們能夠作以下優化:

預加載,空間換時間

爲何要預加載:

  • 滑動時CPU佔用太高,16.67ms內沒法完成內容提交—>致使卡頓
  • 滑動時CPU佔用率高,但空閒時CPU佔用率底—>CPU佔用分佈特色
  • 利用CPU空閒時間預加載,下降滑動時CPU佔用峯值—>解決卡頓

經過預加載咱們但願達到的CPU理想佔用效果以下:

預加載內容:

靜態資源預加載

  • 如何預加載:建立列表前找時機加載。如啓動時、viewDidLoad、runloop空閒時等等
  • 加載內容:緩存在磁盤的網絡數據、圖片、其餘滑動時須要的耗時的資源
  • 注意事項:在預加載帶來的滑動性能提高和內存佔用增長之間權衡

動態資源預加載

  • 如何預加載:

    • 在iOS10之後,UITableView和UICollectionView提供了預加載機制,iOS12開始prefeatching作了優化,再也不與cell的加載同時併發進行,而是cell加載完成以後串行開始prefeatch,從而優化了流暢度
    • iOS10之前,也能夠本身實現相似機制,主要利用的機制有:
      • UIScrollViewDelegate 提供滑動開始、結束、速度時機回調
      • indexPathsForRowsInRect 和layoutAttributesForElementsInRect 提供預加載的indexPath
      • 可根據滑動速度動態調整加載的量
  • 加載內容:

    • Cell的高度、subView的佈局計算
    • 拉取網絡數據
    • 網絡圖片
    • 其餘耗時的資源
  • 注意事項:

    • 在預加載帶來的滑動性能提高和內存佔用增長之間權衡
    • 注意數據過時的問題

WWDC 2018中講到了一個iOS12的底層優化點,蘋果工程師在性能調優的時候發現一個致使丟幀的奇怪case,在沒有其餘後臺線程運行、只有滑動的狀況下,會比有少許的後臺線程的狀況更容易掉幀。經過調研CPU的調度算法發現,在僅有滑動的狀況下,爲了省電,CPU佔用會保持比較底,可是這樣CPU會花更多的時間來計算,就會致使可能錯過這一幀。因此iOS12中,會把UIKit框架上全部的信息(滑動信息以及滑動frame的關鍵時間點)傳遞給底層CPU性能控制器,這樣CPU能夠更智能調度以在frame截止的時機內完成CPU計算。這部分屬於系統底層的優化,對於應用開發者只要應用運行在iOS12就能夠得到這部分優化。

多線程

爲何要多線程:

  • UIKit 大部分API只能在主線程調用, 特別是一些耗時的操做,如view的建立,佈局和渲染默認都是在主線程上完成
  • 主線程任務過多,16.67ms內沒法完成,致使卡頓
  • 將非主線程必須的任務,移到子線程中,減輕主線程負擔
  • 多核處理器,多線程能夠發揮多核併發優點,提升性能

最終經過多線程,咱們但願CPU佔用達到以下效果:

使用多線程注意事項:

  • 主線程最大程度上減小非主線程必須的任務
  • 控制子線程數量在合理的範圍內,防止線程爆炸,必定要根據項目實際CPU佔用特色,有針對的使用多線程。

可在子線程中進行的任務

  • 圖片解碼
  • 文本渲染,UILabel和UITextview都是在主線程渲染的,當顯示大量文本時,CPU的壓力會很是大。特別是對於一些資訊類應用,這部分耗時至關大,對流暢度的影響也十分明顯。對此能夠自定義文本控件,用TextKit或最底層的CoreText對文本異步繪製。儘管這實現起來很是麻煩,但其帶來的優點也很是大,CoreText對象建立好後,能直接獲取文本的寬高等信息,避免了屢次計算(調整 UILabel 大小時算一遍、UILabel 繪製時內部再算一遍);CoreText對象佔用內存較少,能夠緩存下來以備稍後屢次渲染。用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪製文本。儘管這兩個方法性能不錯,但仍舊須要放到後臺線程進行以免阻塞主線程。
  • UIView的drawRect, 因爲 CoreGraphic 方法一般都是線程安全的,因此圖像的繪製能夠很容易的放到後臺線程進行
  • 耗時的業務邏輯

緩存

緩存的內容能夠是

  • UIView。 view的建立代價很大,一些能夠複用的view能夠cache。例如UITableView爲咱們實現的了cell的複用。
  • 圖片。 圖片涉及磁盤IO和解碼,十分耗時,能夠考慮緩存。
  • 佈局。其實不只僅是cell的高度能夠緩存,若是cell裏面有大量的文字圖片等複雜元素,cell的subView的佈局也能夠在第一次計算好,用Model的key來緩存。避免頻繁屢次的調整佈局屬性。在滑動列表(UITableView和UICollectionView)中強烈不建議使用Autolayout。隨着視圖數量的增加,Autolayout帶來的 CPU 消耗會呈指數級上升。具體數據能夠看這個文章:pilky.me/36/。在WWDC20…
  • 數據, 網絡拉取的數據或者db中的數據
  • 其餘建立耗時,可重複利用的資源。 如NSDateFormatter等

更優的實現方式

這裏說的更優的實現方式,主要是指爲了實現同一功能或者效果,CPU佔用更小的實現方式。這部分包括的內容其實很是多,也很雜。受限於篇幅和水平有限,這裏筆者僅羅列一些比較常見的點,並針對其中比較重要的drawRect優化和圖片優化內容作進一步的講解。

  • drawRect優化
  • 圖片優化
  • 算法的時間複雜度優化。咱們知道算法的時間複雜 O(1) < O(log n) < O (n) < O(n^2)... 你們可能以爲iOS開發過程當中使用的算法並很少,算法對性能影響並不明顯。其實否則,舉iOS中的一個例子:IGListDiff採用空間換時間的方式,使得比較的算法複雜度從 O(n^2) 變成 O(n)。IGListKit-diff-實現簡析 。還好比不一樣容器的選擇,會帶來不一樣的查找、插入、刪除的時間複雜度,在大的數據量下也會帶來不一樣的性能表現。
  • storyboard VS 代碼建立view
  • frame VS autolayout autolayout性能度量iOS12優化了autolayout的性能,耗時由指數變爲線性耗時
  • UIView VS CAlayer 後者更輕量,在不須要處理觸摸事件的場景能夠考慮使用CAlayer。UIView層級太多,會致使建立、佈局等較耗時,能夠儘可能扁平化,甚至能夠異步在子線程畫到一個Image上。
  • UIImageView animationImages VS CAAnimation
  • NSDateFormatter dateFromString VS NSDate dateWithTimeIntervalSince1970:
  • 更優的業務邏輯。你們平時在性能優化的時候,已經要優先去排查業務邏輯這塊,仔細梳理。我的經驗不少性能問題都是由不合理的業務邏輯致使的。使用Instruments的time profiler工具仔細觀察耗時的業務邏輯,作好梳理和優化工做。
  • 其餘

下面詳細講下drawRect優化和圖片優化

drawRect優化

  • 首選使用CAShapeLayer替代drawRect,在大多數場景下,均可以使用CAShapeLayer替代drawRect。兩者對比:

    • CAShapeLayer使用GPU硬件加速,更快。GPU對高度並行的浮點運算作了優化。而drawRect使用CPU繪圖,相比之下會很慢,並且十分耗CPU
    • CAShapeLayer佔用內存更少。由於不會建立寄宿圖,所以不管多大都不會佔用太多內存。而drawRect圖層每次重繪的時候都須要從新抹掉內存而後從新分配,十分佔用內存。詳見內存惡鬼drawRect
    • CAShapeLayer不會被圖層邊界剪裁掉
    • CAShapeLayer不會出現像素化,經過矢量圖繪製而不是bitmap
    • CAShapeLayer有不少屬性能夠方便的作動畫,好比使用strokeStart和strokeEnd能夠作出了很漂亮的動畫
  • 異步繪製。可使用異步繪製的方式,在子線程繪製好得到image,而後交給主線程。

  • Dirty Rectangles: 可使用setNeedsDisplayInRect標記Dirty Rectangles,僅重繪指定區域,也會極大提高性能。

圖片優化

在大多數app中,圖片絕對是使用最頻繁的資源之一,咱們知道磁盤和網絡的加載速度和內存比要慢不少,而通常圖片都比較大,I/O十分耗時。並且圖片還涉及解碼,也是一項十分消耗CPU的工做,所以圖片的優化對app的性能有着十分關鍵的做用。談談iOS中圖片的解壓縮

在以前將的優化整體原則的時候,咱們說過須要理解優化對象的運行機制,咱們先了解下圖片顯示原理:

  • 從磁盤或者網絡加載一張圖片,此時圖片未解碼
  • 圖片賦值給UIImageView
  • 在主線程中解碼,很是耗時的 CPU 操做
  • CATransaction捕捉到layer tree的變化
  • 在main run loop, 提交transaction:
    • 若是圖片數據沒對齊,Core Animation會拷貝一份數據,進行字節對齊
    • GPU處理位圖數據,進行渲染

針對上面的過程,咱們的優化手段主要有:

  • 異步下載/讀取圖片,這樣能夠防止這項十分耗時的操做阻塞主線程。
  • 預處理圖片大小。若是UIImage大小和UIImageview的size不一樣的話,CPU須要提早預處理,這是一項十分消耗CPU的工做,特別是在一些縮略圖的場景下,若是使用了十分大的圖片,不只會帶來很大的CPU性能問題,還會致使內存問題。咱們能夠用instruments Core Animation 的Misaligned Image debug選項來發現此問題。這裏可使用ImageIO中的CGImageSourceCreateThumbnailAtIndex等相關方法進行後臺異步downsample,能夠在CPU和內存上得到很好的性能。
  • UIImageView frame取整。視圖或圖片的點數(point),不能換算成整數的像素值(pixel),致使顯示視圖的時候須要對沒對齊的邊緣進行額外混合計算,影響性能。藉助ceilf()、floorf()、CGRectIntegral()等將小數點後數據除去便可。咱們能夠用instruments Core Animation 的Misaligned Image debug選項來發現此問題
  • 使用mmap,避免mmcpy。解碼圖片 iOS從磁盤加載一張圖片,使用UIImageVIew顯示在屏幕上,須要通過如下步驟:從磁盤拷貝數據到內核緩衝區、從內核緩衝區複製數據到用戶空間。使用mmap內存映射,省去了上述第2步數據從內核空間拷貝到用戶空間的操做,具體能夠參考FastImageCache的實現
  • 子線程解碼。若是咱們使用imgView.image = img; 若是圖片沒有解碼,則會在主線程進行解碼等操做,會極大影響滑動的流暢性。
  • 字節對齊,若是數據沒有字節對齊,Core Animation會再拷貝一份數據,進行字節對齊,也是十分消耗CPU。
  • iOS 12引入了Automatic Backing Store這項技術。經過在保證色彩不失真的基礎上,使用更少的數據量,去表達一個像素的顏色。在UIView.draw()、UIGraphicsImageRenderer、UIGraphicsImageRenderer.Range中是默認開啓的。其實咱們本身能夠針對圖片的特色,採用更少的byte來標示一個像素佔用的空間,FastImageCache就是使用這種優化手段,有興趣的讀者能夠去了解一下。
  • 咱們平常開發中可使用一些比較經典的圖片緩存庫,好比SDWebImage、 FastImageCache、YYImage等。這些第三方庫替咱們完成的大部分優化的工做,並且接口也十分友好。咱們可也使用這些第三方庫幫助咱們得到更好的性能體驗。

GPU優化

CPU和GPU之因此大不相同,是因爲其設計目標的不一樣,它們分別針對了兩種不一樣的應用場景。CPU須要很強的通用性來處理各類不一樣的數據類型,同時又要邏輯判斷又會引入大量的分支跳轉和中斷的處理。這些都使得CPU的內部結構異常複雜。而GPU面對的則是類型高度統一的、相互無依賴的大規模數據和不須要被打斷的純淨的計算環境。因此CPU擅長邏輯控制,串行的運算。和通用類型數據運算不一樣,GPU擅長的是大規模併發計算,這也正是密碼破解等所須要的。因此GPU除了圖像處理,也愈來愈多的參與到計算當中來。參考

iOS中GPU在顯示方面的工做主要是:接收提交的紋理(Texture)和頂點描述(三角形),進行變換(transform)、混合並渲染,而後輸出到屏幕上。屏幕上的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。通常來講,CALayer的大多數屬性都是使用GPU來繪製的。雖然GPU在處理圖像等渲染是速度很快,但若是開發過程當中使用不當,仍會致使GPU佔用太高,渲染速度跟不上屏幕刷新致使卡頓。

對GPU消耗比較高的操做有:

  • 紋理的渲染

    全部的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定爲 GPU Texture。不管是提交到顯存的過程,仍是 GPU 調整和渲染 Texture 的過程,都要消耗很多 GPU 資源。當在較短期顯示大量圖片時(好比 TableView 存在很是多的圖片而且快速滑動時),CPU 佔用率很低,GPU 佔用很是高,界面仍然會掉幀。避免這種狀況的方法只能是儘可能減小在短期內大量圖片的顯示,儘量將多張圖片合成爲一張進行顯示。

    圖片過大,超過 GPU 的最大紋理尺寸時,圖片須要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來講,iPhone 4S 以上機型,紋理尺寸上限都是 4096x4096,更詳細的資料能夠看這裏:iosres.com。因此,儘可能不要讓圖片和視圖的大小超過這個值。

  • 視圖的混合 (Composing)

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

  • 圖形的生成

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

經常使用優化手段

  • 減小視圖數量和層次,可把多個視圖預先渲染爲一張圖片

  • 不要讓圖片和視圖超過GPU可渲染的最大尺寸

  • 視圖不透明

  • 防止離屏渲染 OpenGL 中,GPU 屏幕渲染有如下兩種方式:

    • On-Screen Rendering 意爲當前屏幕渲染,指的是 GPU 的渲染操做是在當前用於顯示的屏幕緩衝區中進行。
    • Off-Screen Rendering 意爲離屏渲染,指的是 GPU 在當前屏幕緩衝區之外新開闢一個緩衝區進行渲染操做。

    相比於當前屏幕渲染,離屏渲染的代價是很高的,主要體如今兩個方面:

    • 建立新緩衝區 要想進行離屏渲染,首先要建立一個新的緩衝區。
    • 上下文切換 離屏渲染的整個過程,須要屢次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束之後,將離屏緩衝區的渲染結果顯示到屏幕上有須要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。

    因此在圖形生成的步驟咱們要儘量的避免離屏渲染

優化工具

iOS開發中,在GPU優化上,咱們通常使用instruments中的Core Animation工具來進行滑動流暢度優化,在Core Animation中咱們可也看到列表滑動過程當中的FPS,其中有一些頗有用的debug選項,幫助咱們找到代碼中有性能問題的代碼。下面是一些經常使用的選項:

  • Color Blended Layers

    Color Blended Layers是用來檢測個半透明圖層的混合區,渲染程度對屏幕中的混合區域進行綠到紅的高亮。由於計算混合區的顏色時,致使overdraw,消耗必定的GPU資源,是致使滑動性能的一個因素。因此儘可能要儘可能避免

    在開發過程當中,避免Blended Layers的手段有:

    • 設置opaque屬性YES
    • View背景顏色不透明
    • Image不含有透明通道
    • 須要特別注意的是,在iOS8以後,UILabel使用的是CALayer做爲底圖層,而在iOS8開始,UILabel的底圖層變成了_UILabelLayer,繪製文本也有所改變。UILabel顯示中文時,還需masksToBounds = YES。
  • Color Hits Green and Misses Red Color Hits Green and Misses Red用來檢測是否正確使用shouldRasterize,當緩存須要從新生成時,紅色高亮rasterized layers,當設置shouldRasterize=YES,會將layer預先渲染成位圖,並緩存。以提升性能。可是若是cache頻繁重複地生成,表示shouldRasterize可能帶來的是負面的性能影響。所以shouldRasterize適用於渲染耗時、圖像內容不變的狀況,在列表中因爲內容要頻繁變化,所以不推薦使用此屬性

  • Color Copied Images

    大多數時,Core Animation只須要提交原始圖片的指針到render server,不涉及內存copy。可是一些狀況下,Core Animation不得不copy一份圖片發送到render server。蘋果的GPU只解析32bit的顏色格式,若是圖片顏色格式不對,CPU會預先格式轉換。copy images是很是耗CPU的操做,必定要避免。

  • Color Misaligned Images 被拉伸縮放的圖片、沒法正確對齊到像素的圖片(可能有不是整數的的座標)。是耗CPU的操做

  • Color Offscreen-Rendered Yellow

    GPU在當前屏幕緩衝區外開闢新的緩衝區進行渲染, 屏幕外緩衝區和當前屏幕緩衝區上下文切換是十分耗時的操做

    引發Offscreen-Rendered的操做有:

    - 圓角 cornerRadius masksToBounds同時設置
      - 設置shadow
      - 開啓光柵化 shouldRasterize=YES.CALayer 有一個 shouldRasterize 屬性,將這個屬性設置成 true 後就開啓了光柵化。開啓光柵化後會將圖層繪製到一個屏幕外的圖像,而後這個圖像將會被緩存起來並繪製到實際圖層的 contents 和子圖層,對於有不少的子圖層或者有複雜的效果應用,這樣作就會比重繪全部事務的全部幀來更加高效。可是光柵化原始圖像須要時間,並且會消耗額外的內存。光柵化也會帶來必定的性能損耗,是否要開啓就要根據實際的使用場景了,圖層內容頻繁變化時不建議使用。最好仍是用 Instruments 比對開啓先後的 FPS 來看是否起到了優化效果。
      - 圖層蒙板
    複製代碼

避免Offscreen-Rendered的方式能夠其餘方式實現圓角、shadow + shadowPath等。

總結

本文的講了一些形成卡頓的緣由,以及CPU和GPU優化的經常使用技巧和工具,你們在優化的時候能夠做爲參考。但不要把優化手段侷限在這些方面,不一樣的應用有各自不一樣的特色,必定要具體問題具體分析。甚至能夠跳出技術範疇,在交互方面作一些文章,好比在減小列表每次從服務器獲取的數據數量、採用用戶手動點擊觸發獲取更多數據而不是滑動過程當中自動獲取、使用交互動畫等均可以極大改善用戶的滑動體驗。

最後仍是要強調一下我上一篇文章講的優化時候須要注意的幾大原則,這樣才能在優化過程當中有更好的全局觀,儘可能少走彎路,但願你們可以在優化過程當中時刻牢記。

相關文章
相關標籤/搜索