主要从三个方面进行优化:
- 引用计数(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(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
转载请注明出处,谢谢~