iOS 性能優化(一)

本文將從原理出發,解釋卡頓發生的原理,然後會講解項目中行之有效的幾個優化點,以此作爲日後的參考提醒。下面進入正題。

屏幕顯示圖像原理
這裏寫圖片描述

我們知道,CRT顯示器的顯示原理是用電子槍掃描熒光屏來發光。如上圖所示,電子槍按照從左到右,然後從上到下的順序掃描。當電子槍換到新的一行準備進行掃描時,顯示器會發出一個水平同步信號;而當一幀畫面繪製完成後,電子槍回覆到原位準備畫下一幀前,顯示器會發出一個垂直同步信號。垂直同步信號的作用一方面是通知顯示器回到第一行行首位置,另外一方面,也通知顯卡,準備輸出下一幀畫面。現在已經是液晶顯示器的時代了,不再使用電子槍掃描了,但是原理還是類似的,水平同步信號和垂直同步信號還是一樣被使用的。

計算機工作原理
這裏寫圖片描述
CPU中計算顯示內容,比如視圖的創建、佈局計算、圖片解碼、文本繪製等,將數據經過總線傳給GPU;
GPU進行變換、合成、渲染,將數據經過總線提交給幀緩衝區(FrameBuffer);
幀緩衝區(FrameBuffer)等待垂直同步信號 VSync,然後將緩衝區的內容顯示到屏幕上。

屏幕撕裂(Screen tearing)
這裏寫圖片描述
當資源同時發生讀寫操作時,就會產生數據錯亂,解決辦法就是添加線程安全鎖。同理,屏幕撕裂的產生原因及修復措施也類似。
上述的簡單的屏幕顯示原理其實會產生這樣一個問題:假設我們的顯卡速度很快,每秒生產的幀數肯定要超過顯示器刷新率。那麼在實際數據處理過程中,緩衝區的數據,在被輸出之前,就被顯卡不斷的刷新重寫。但是緩衝區並不是“先清空再寫入數據”,這太沒有效率,而是採用“新數據覆蓋老數據”的方式。
假設這樣一種情況,緩衝區已經有一副完整的幀畫面(A幀),然後顯卡生成了下一幀畫面(B幀),新一幀的數據開始寫入緩衝區,寫到一半的時候,垂直同步信號來 了,於是緩衝區的數據被輸出到顯示器。但問題是,這時緩衝區的數據,是由一半A幀和一半B幀數據合成的。因此最終顯示器上顯示出來的畫面就不是一副完整的 畫面,這就是“畫面撕裂”現象出現的原因。

解決屏幕撕裂
簡單來說只要讓幀緩衝區裏的數據始終保持一副完整的畫面就可以了。從技術角度出發,其實就是利用剛剛提到的垂直同步信號 VSync。
具體說起來就是,當顯卡生成了一副完整畫面並寫入了幀緩衝區之後,暫停!然後開始等待垂直同步信號,當得到垂直同步信號後,再繼續渲染下一幀寫入緩衝區。這樣就可以保證在緩衝區的數據始終是一副完整的畫面,不會出現前後幀混合的問題。

卡頓產生原因
這裏寫圖片描述
在 VSync 信號到來後,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,比如視圖的創建、佈局計算、圖片解碼、文本繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 信號到來時顯示到屏幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。

從上面的圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象。所以開發時,也需要分別對 CPU 和 GPU 壓力進行評估和優化。

CPU資源消耗原因及解決方式

  1. 提前佈局

    提前佈局可以說是最重要的優化點了。其實在從服務端拿到 JSON 數據的時候,關於視圖的佈局就已經確定了,包括每個控件的frame、cell的高度以及文本排版結果等等,在這個時候完全可以在後臺線程計算並封裝爲對應的佈局對象XXXTableViewCellLayout,每個cellLayout的內存佔用並不是很多,所以直接全部緩存到內存中。當列表滾動到某個cell的時候,直接拿到對應的cellLayout配置這個cell的對應屬性即可。當然,該有的計算是免不了的,只是提前算好並緩存,免去了在滾動的時候計算和重複的計算。

  2. 對象的創建

    對象的創建會分配內存、設置屬性等,會消耗CPU資源。所以儘量使用輕量對象代替,比如能用CALayer的時候儘量不用UIView,敏感位置能不用IB儘量使用純代碼手寫。推遲同一時間創建對象,推薦使用懶加載在需要使用時候創建對象。

  3. 對象的調整

    對象的調整也經常是消耗 CPU 資源的地方。這裏特別說一下 CALayer:CALayer 內部並沒有屬性,當調用屬性方法時,它內部是通過運行時 resolveInstanceMethod 爲對象臨時添加一個方法,並把對應屬性值保存到內部的一個 Dictionary 裏,同時還會通知 delegate、創建動畫等等,非常消耗資源。UIView 的關於顯示相關的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調整時,消耗的資源要遠大於一般的屬性。對此你在應用中,應該儘量減少不必要的屬性修改。當視圖層次調整時,UIView、CALayer 之間會出現很多方法調用與通知,所以在優化性能時,應該儘量避免調整視圖層次、添加和移除視圖。

  4. 對象的銷燬

    當前類持有大量對象時候,其銷燬時候的資源消耗就非常明顯。建議創建銷燬的異步隊列,將需要銷燬的對象放到隊列中銷燬。

  5. AutoLayout

    Autolayout 對於複雜視圖來說常常會產生嚴重的性能問題,AutoLayout相對低效的原因是隱藏在底層的命名爲”Cassowary“的約束求解系統,隨着視圖數量的增長,Autolayout 帶來的 CPU 消耗會呈指數級上升,當Cell內約束超過25個的時候,會降低滑動的幀率。

  6. 文本的計算以及渲染

    UI中存在大量的對於文本高度的適配,可以參考:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪製文本。儘管這兩個方法性能不錯,但仍舊需要放到後臺線程進行以避免阻塞主線程。常見的文本控件 (UILabel、UITextView 等),其排版和繪製都是在主線程進行的,當顯示大量文本時,CPU 的壓力會非常大。解決辦法是利用TextKit或者是CoreText自定義文本控件。

  7. 圖片解碼以及圖像的繪製

    當你用 UIImage 或 CGImageSource 的那幾個方法創建圖片時,圖片數據並不會立刻解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去,並且 CALayer 被提交到 GPU 前,CGImage 中的數據纔會得到解碼。這一步是發生在主線程的,並且不可避免。如果想要繞開這個機制,常見的做法是在後臺線程先把圖片繪製到 CGBitmapContext 中,然後從 Bitmap 直接創建圖片。目前常見的網絡圖片庫都自帶這個功能。一個最常見的地方就是 [UIView drawRect:] 裏面了。由於 CoreGraphic 方法通常都是線程安全的,所以圖像的繪製可以很容易的放到後臺線程進行。

  8. 文件系統的調用

    NSFileManager獲取一個目錄獲取文件信息,進行多次遞歸計算,stat幾乎瞬間完成,NSFileManager耗時較長且消耗CPU。

GPU 資源消耗原因和解決方式

  1. 紋理的渲染

    所有的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定爲 GPU Texture。不論是提交到顯存的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片並且快速滑動時),CPU 佔用率很低,GPU 佔用非常高,界面仍然會掉幀。避免這種情況的方法只能是儘量減少在短時間內大量圖片的顯示,儘可能將多張圖片合成爲一張進行顯示。當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096×4096,更詳細的資料可以看這裏:iosres.com。所以,儘量不要讓圖片和視圖的大小超過這個值。

  2. 視圖的混合

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

  3. 紋理的渲染

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

  4. 其他
    數據分頁加載、做好數據緩存、異步處理、避免離屏渲染

總結

性能優化這個東西其實很難形成一個具體的方案,爲什麼這麼說?因爲之所以稱之爲優化,是因爲要在原有的代碼基礎上進行優化,原有的代碼又有各式各樣的原因導致需要依照現有代碼來優化,而很難完全脫離現有的情況完全參照某一種的既定方案進行優化。假如說是完全參照某一種的方案優化的話,建議還是將某一個性能敏感的頁面利用Texture進行完全重寫,這樣才能算是整體化一的利用了某一種方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章