主要從三個方面進行優化:
- 引用計數(Reference Counting)
- 泛型(Generics)
- 動態分發(Dynamic Dispatch)
最后,也會介紹一下如何使用Time Profiler檢測耗時操作。
引用計數(Reference Counting)
使用類定義一個數據類型,在對這個數據類型進行大量重復操作時,引用計數對于性能的影響將會變得很明顯:
class Point {
var x, y: Float
}
var array: [Point] = ...
for p in array {
increment
...
decrement
}
使用struct來解決引用計數導致的性能問題:
struct Point {
var x, y: Float
}
當struct中也有引用類型時,比如:
可以用一個類來包裹這個struct,這樣就可以避免對struct中的引用對象進行單獨的引用計數:
泛型(Generics)
觀察以下泛型代碼:
func min<T: Comparable>(x: T, y: T) -> T {
return y < x ? y : x
}
在編譯時,以上泛型代碼其實是被翻譯成了類似這種結構的代碼:
當泛型函數在被調用時,編譯器會將泛型具體化(Generic Specialization):
最后被具體化為如下形式:
但是泛型定義的可見性會影響泛型的具體化,比如在同一個module下的不同文件中:
如何解決泛型定義的可見性問題?
打開項目設置,Build Settings - Optimization Level,將Release模式的選項改為 Whole Module Optimization。此時,同個Module下不同文件中的泛型函數,在開啟Whole Module Optimization之后可以一起編譯,此時泛型定義對于同個Module中的不同文件是可見的。
動態分發(Dynamic Dispatch)
這里涉及到兩個概念:
- 繼承
- 訪問控制
繼承
請觀察以下類型的定義:
在獲取name屬性的值和調用noise()方法時,都需要進行額外的操作,因為編譯器不確定屬性和方法有沒有被子類重寫。
如何消除額外的操作?
使用final關鍵字,明確地告訴編譯器不存在重寫操作。
訪問控制
再來觀察另一個例子:
—— 如果調用noiseImpl()方法,編譯器能不能直接調用Pet中的noiseImpl()方法?
—— 當然不能!
在這里,編譯器會假設子類中有可能重寫這個方法,所以它不會直接調用Pet中的noiseImpl()方法。
這時候,可以通過將noiseImpl()方法定義為private級別來告訴編譯器,直接調用Pet中的noiseImpl()方法。
對于通過module中的類繼承,同樣可以開啟Whole Module Optimization來避免多余的間接調用。編譯器可以自行判定當前Module中的繼承鏈關系。
使用Time Profiler檢測耗時操作
新建一個iOS項目,將Point定義為class類型,然后在ViewController中放置一個按鈕,點擊按鈕即可執行iteratePoints操作。
class Point {
var x, y: Float
init(x: Float, y: Float) {
self.x = x
self.y = y
}
}
class ViewController: UIViewController {
let points = [Point].init(repeating: Point.init(x: 0, y: 0), count: Int(1e7))
var countX: Float = 0
@IBAction func iteratePoints(_ sender: Any) {
for _ in 0..<5 {
for point in points {
let p = point
countX += p.x
}
}
}
}
代碼添加完畢,然后在Xcode中按下Command + I即可啟動Profile,或者可以點擊Xcode菜單欄Product - Profile。彈出Instruments后,雙擊啟動Time Profiler。
等待CPU占用率變為0,防止其他操作對待檢測的操作造成影響。如下圖紅色區域:
然后,點擊ViewController中的iteratePoints按鈕,即可看到Time Profiler中有明顯的藍色柱出現。
點擊該區域的藍色柱,然后按下Command和+按鈕,對該區域進行放大。
拖動選中想要檢查的部分:
可以發現底部有部分視圖有變化,726 @objc ViewController.iteratePoints會變為白色,點擊它!
點擊展開@objc ViewController.iteratePoints(:),會看到主要的耗時操作為retain/release
現在,關閉當前的Time Profiler(一定要關閉,不需要保存)
接下來請修改代碼,將Point由class類型改為struct類型。
然后再次按下Command + I,Xcode會用最新的代碼更新Build內容,然后啟動Profile。
按照之前的操作流程進行操作后,你會發現,耗時操作不再是retain/release。
參考文章頁面內有視頻,視頻結尾部分還有很多關于使用Time Profiler進行Swift代碼優化的示例(雖然并沒有提供示例代碼),建議親自觀看。
總結
-
使用final關鍵字和訪問控制
- 幫助編譯器理解你的類繼承結構;
- 已有的代碼可能需要因此而更新;
開啟Whole Module Optimization
使用Instruments - Time Profiler來檢測耗時操作,針對性地進行優化
參考文章:
Optimizing Swift Performance
轉載請注明出處,謝謝~