【iOS 印象】效能優化梳理(Swift)

Dylan_王興彬發表於2018-08-10

效能監控

  • 業務效能監控:在 App 中業務的開始與結束打點上報,以達到後臺統計監控效能;
  • 卡頓監控:
    • 主執行緒卡頓監控,通過子執行緒監測主執行緒的 runLoop,判斷兩個區域狀態之間的耗時是否達到一定閾值。
    • FPS監控。要保持流暢的UI互動,App 重新整理率應該當努力保持在 60fps。監控實現原理比較簡單,通過記錄兩次重新整理時間間隔,就可以計算出當前的 FPS。

記憶體分配與釋放

  • 基於棧(stack-based)的記憶體分配

棧是一種非常簡單的資料結構; 資料從棧的頂部推入(push)與彈出(pop); 由於只能夠修改棧的末端,因此可以通過維護一個指向棧末端的指標來實現這種資料結構;

  • 基於堆(heap-based)的記憶體分配

記憶體分配具備更加動態的生命週期,更復雜的資料結構 在堆上進行記憶體分配,需要為物件開闢並鎖定堆當中的一個空閒塊(free block) 為了執行緒安全,必須進行鎖定和同步

引用計數

引用計數(reference counting)操作本身相對不耗費效能,但由於使用次數足夠多,因此它帶來的效能影響比較大。引用計數是 Objective-C 和 Swift 中用於確定何時該釋放物件的安全機制。Swift 當中的引用計數是強制自動管理(ARC, Auto Reference Counting),因此容易被開發者所忽略。

排程

Swift 中有三種型別的排程(Dispatch)方式:

  • Swift 會盡可能將函式內聯(inline),這樣的話,該函式就可以直接呼叫,不會有額外的效能開銷。
  • 靜態排程(static dispatch)本質是通過 V-table 進行的查詢和跳轉,這個操作消耗大概 1nm。
  • 動態排程(dynamic dispatch)消耗大概 5nm。只有幾個方法進行這樣的動態排程的話,問題不大。應該避免在巢狀迴圈或執行次數較多的操作中採用動態排程的方式。

V-table: virtual table,虛擬函式表,記錄了類中所有虛擬函式的函式指標,也就是說是個函式指標陣列的起始位置。

物件

Swift 中有兩種型別的物件:

  • 類(Class)
class Index {
	let section: Int
	let item: Int
}

let i = Index(section: 1, item: 2)
let i2 = i
複製程式碼

類當中的資料是在堆上分配記憶體。 Index 這個類包含了兩個屬性,sectionitem,當該物件被建立時,堆上便開闢了sectionitem 的資料空間,並生成一個指向該物件的指標 i。如果對其新增一個引用,則 i2 指向堆上相同區域,這兩個物件指標之間是共用同一塊記憶體空間的,同時會對該物件自動插入持有操作,引用計數+1。

  • 結構體(Struct)
struct Index {
	let section: Int
	let item: Int
}

let i = Index(section: 1, item: 1)
let i2 = i
複製程式碼

通常我們會說,要編寫效能優異的 Swift 程式碼,最簡單的方式儘可能使用結構體。

結構體儲存在棧上,基於棧進行記憶體分配,並且通常使用靜態排程或內聯排程。如果將其賦值給另一個變數 i2,這會將儲存在棧上的值複製一遍產生新的物件,而不是引用。

另:。列舉也是值型別,採用列舉改進資料模型,避免使用大量的字串。

抽象型別,協議會導致效能下降

協議內部會有儲存值的快取區、後設資料,並且要支援動態排程派發,有一個協議記錄表(protocol witness table,也稱作虛擬函式表),因此協議型別所佔記憶體要比具體的類、結構體或列舉要更大。這個可以簡單瞭解一下,之後考慮另起一篇文章單獨記錄下這個問題。“過早的優化是萬惡之源”,相比面向協議帶來的好處,一般來說可以不用太過考慮使用協議帶來的細微效能消耗。

即便如此,在面向協議開發的過程中,也可以適當地注意以下幾點,在利用協議帶來的好處的同時,避免協議帶來的效能損耗:

  • 若某種協議只用於類,可新增: class作為約束,採用類協議,如代理(delegate)協議的使用。
  • 將協議作為泛型約束來使用,而不是單獨作為型別引數,這樣編譯器可以對其進行優化。

GCD 進行多執行緒效能優化

通過 GCD 將一些耗時操作派發到非主執行緒,提高 UI 流暢度,響應更及時,優化使用者體驗。

I/O 效能優化

  • 使用快取減少 I/O 次數,NSCache 是專門用來管理快取的一個類,合理利用快取可極大提高執行效率。
    • 併發訪問快取時資料一致性問題
    • 執行緒安全問題,防止一邊修改一邊遍歷
    • 查詢快取時的效能問題
    • 快取的釋放與重建,避免無用的快取佔用過多空間
  • 化零為整進行寫入操作
  • 選用適合的 API
  • 選用適合的操作執行緒

減少離屏渲染

離屏渲染:GPU在當前螢幕緩衝區以外新開闢一個緩衝區進行渲染操作。 離屏渲染需要多次切換上下文環境:先是從當前螢幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到螢幕上又需要將上下文環境從離屏切換到當前螢幕,而上下文環境的切換是一項高開銷的動作。

UITableView 效能優化

  • 正確設定 reuseIdentifierUITableViewCell 進行重用
  • 設定統一規格的 Cell
  • 儘量減少不必要的透明檢視
  • 儘量避免漸變、圖片拉伸與離屏渲染
  • Cell 是動態高度時計算並快取行高
  • 非同步請求載入 Cell 展示資料,並進行預處理,包括圖片的載入、壓縮,富文字的顯示
  • 減少子檢視的層級關係

必要時使用 Autorelease Pool

使用迴圈體時,考慮是否有必要採用 Autorelease Pool 對臨時物件進行釋放,避免佔用過多記憶體空間。

合理進行執行緒分配

合理的執行緒分配,最終目的就是保證主執行緒儘量少地處理非 UI 操作,同時將整個 App 中子執行緒數量控制在合理範圍,以避免不必要子執行緒開啟與切換消耗。

  • UI 與資料來源操作在主執行緒
  • 資料庫操作、日誌記錄、網路回撥在相應的固定執行緒
  • 不同業務,通過建立佇列保證資料一致性

預載入與延遲載入

  • 預處理:耗時操作提前在後臺執行緒進行處理
  • 延遲載入:必要可視內容優先載入,其他內容稍後或需要展示時再載入

參考與更多

相關文章