学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

【译】Improve Performance in Your iOS Applications 三

翻译几篇iOS性能优化的文章 ⅠⅠⅠ,这是本系列的第三篇文章,重点介绍如何提高代码的可读性和性能。 请根据您的需要谨慎选择。通常,更改或改进体系结构和代码重构需要更多的时间和精力。

尽管现代iOS设备能够处理大量密集而复杂的任务,但如果你不密切关注应用程序的运行方式,设备可能会显得很慢。

开启Swift快速优化

第一步总是启用优化。Swift有三个优化级别:

  • One:常规开发。它只进行最低限度的优化,并保留所有调试信息。

-o:用于大多数生产代码。它的极端优化可能会极大地改变输出代码的性质和数量。调试数据将丢失发出。

-Osize:在这种模式下,编译器更倾向于代码大小而不是速度。
1
当前的优化级别可以通过Xcode UI来改变:
在项目导航器(Project Navigator)中选择项目编辑器图标( Project Editor)。要访问项目设置编辑器,请单击项目标题下面的图标。更改“生成设置”(Build Settings)标题下的“优化级别”(Optimization Level)框,以便对项目中的所有目标应用优化设置。

在“项目编辑器”(Project Editor)中的“目标”(Targets)下选择目标(target),并覆盖“生成设置”(Build Settings)标题下的“优化级别”(Optimization Level)框。

理解自动引用计数

如果你想构建一个高性能的iOS应用程序,你必须分析你的组件如何使用内存以及如何优化它。循环引用问题是典型的内存管理问题。但首先,让我们看看iOS是如何处理自己的内存的。

苹果的自动内存管理系统被称为自动引用计数(ARC)。引用计数用于确定内存块是否应该被释放。新对象的引用计数从1开始。这个引用计数可能会随着时间的推移而改变。最后,当引用计数接近0时,对象被释放。

强引用和弱引用

根据上面的想法,在声明变量时还应该理解强引用和弱引用。默认情况下,变量之间保持强引用。

1
2
3
4
struct Vehicle {
var strongCar = UIView()
weak var weakCar = UIView()
}

随着变量变得更强大,引用的数量也会增加。当一个引用计数为2的对象被一个新的变量强引用时,对象的引用计数增加到3。

另一方面,弱引用对引用计数的增加没有影响。如果一个引用计数为2的对象被赋值给一个引用计数已经为3的对象,那么该对象的引用计数将保持为2。

此外,当强变量处于活动状态时,强变量的引用组件保证同时保留在内存中。另一方面,这种确定性不适用于弱变量。

避免内存泄漏

一个实体Vehicle包含另一个实体Car的许多实例,并且一个实体Car的每个实例都与一个实体Vehicle相连接的模型很可能是您以前见过的。在一个非常简化的实现中,它看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Vehicle {
private var cars = [Car]()
func add(_ car : Car) {
cars.append(car)
}
}

class Car {
private var vehicle : Vehicle
required init(vehicle : Vehicle) {
self.vehicle = vehicle
}
}

let vehicle = Vehicle()
let car = Car(vehicle: Vehicle)
vehicle.add(car)

在上面的例子中,一切看起来都很好,但事实并非如此。观察VehicleCars之间的相似之处:它们彼此之间有着密切的联系。接下来,你要猜。内存泄漏是罪魁祸首。

当一段数据在其生命期结束后仍然存在于内存中时,就出现了内存泄漏。当两个强变量相互强引用时,它们会产生内存泄漏。循环引用是这个问题的技术术语。然后,让我们看看几个选项。

使用面向协议编程

当涉及到在应用程序中存储数据和建模行为时,结构(structures)和类(classes)都是很好的选择。然而,由于它们的相似之处,可能很难决定哪个更好。

仔细检查标准Swift库中的代码,就会发现协议使用得相当频繁。Apple喜欢面向协议编程(Protocol Oriented Programming),如果你正开始创建继承连接的新项目,建议你使用它。

多态是OOP范式中最有帮助的部分之一。它决定要调用的运行时参数或函数。动态调度是一个决策过程。下面是OOP的一个基本示例。Car类有一个带有override关键字的echo方法,因为它是在超类Vehicle中定义的。调用类Car中的echo方法,而不是类Vehicle中的echo方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
import UIKit

class Vehicle {
func echo() {
print("Improving iOS App performance.")
}

class Car: Vehicle {
override fun echo() {
// your code
}
}
}

那不是很好吗?不,正如前面的例子所示,每个运行时作业都会减慢我们的执行时间。那么解决办法是什么呢?

1
2
3
4
5
6
7
8
9
10
11
import UIKit

class Vehicle {
func echo() {}
}

class Car: Vehicle {
func echo {
// your code
}
}

POP是面向协议编程的缩写,现在可以使用了。它只需要一个小小的调整就可以显著减少运行时计算。POP术语如此熟悉是不是很有趣?它是苹果UIKit中最常用的委派模式。

使用静态调用

在参考Apple的Swift标准库文档时,你会发现与类相反,struct是值类型,而class是引用类型。因此,它们可以互换使用。一开始似乎有一点变化。这比我想象的要小得多!

Structs是静态分配的,而动态构造的类是动态分配的。但是,如果类具有结构类型参数,会发生什么情况呢?那么,你打算怎么做?即使struct参数是一个结构,也仍然需要堆栈分配和结构构造。根本不是这样的!因此,即使实参是struct类型的,存储它的类也允许它在堆中分配和动态调用,而不依赖于实参的类型。

对于前面提到的例子,你可以继续使用你之前创建的类,但是使用变量的弱引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Vehicle {
private var cars = [Car]()

func add(_ car : Car) {
cars.append(car)
}
}

class Car {
private weak var vehicle : Vehicle?

required init(vehicle : Vehicle) {
self.vehicle = vehicle
}
}

let vehicle = Vehicle()
let car = Car(vehicle: vehicle)
vehicle.add(Car)

最后,不要在代码中大范围使用类继承,而是尝试使用结构体(structs)和协议(protocols)。

限制变量的作用域

包含privatefileprivate关键字的声明将该声明的可见性限制在包含这些关键字的文件中。这使编译器能够确定代码中是否有任何其他可能的覆盖声明。

因此,由于没有任何这样的声明,编译器就可以自动推导出最终关键字,并在过程中根据需要删除对方法和字段访问的间接调用。使用以下示例,vehicle.doSomething()car. dosomething()myPrivateVar将能够直接使用,提供VehicleCar不包含任何覆盖声明在同一文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private class Vehicle {
func doSomething() { ... }
}

class Car {
fileprivate var myPrivateVar: Int
}

func usingVehicle(_ vehicle: Vehicle) {
vehicle.doSomething() // The compiler will remove calls to this method
}

func usingCar(_ car: Car) -> Int {
return car.myPrivateVar
}

使用值类型

Swift有两种类型:值类型(结构、枚举和元组)和引用类型(类)。重要的是要注意NSArrays 不能包含值类型。因此,当使用值类型时,优化器可以避免大部分与处理由NSArray支持的Array的可能性相关的成本。

1
2
3
4
5
6
7
// Avoid using a class here.
struct Car {
var name: String
var manufacturedYear: [Int]
}

var newCar: [Car]

此外,与引用类型不同,值类型只需要对包含递归引用类型的引用进行计数。为了避免Array中不必要的retain和release流量,可以使用值类型而不是引用类型。

使用闭包

说到特性,Swift的闭包是目前最强大的特性之一。另一方面,它们不受循环引用的影响。闭包有可能导致循环引用,原因很简单:它们在不使用时维持对使用它们的对象的强引用。

在本例中,我们有一个包含闭包的循环引用。注意下面连续的代码块是如何修改自我声明的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car {
private var tyres = 0
private var closure : (() -> ()) = { }

init() {
closure = {
self.tyres += 1 // standard way
print(self.tyres)
}
}

func foo() {
closure()
}
}

上面的例子与闭包有很强的连接,而闭包又与对象本身有很强的连接,因为self在闭包块中使用。有两种方法可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car {
private var tyres = 1
private var closure : (() -> ()) = { }

init() {
closure = {
[__unowned self__] in self.tyres += 1 // unowned declaration
print(self.tyres)
}
}

func foo() {
closure()
}
}

有了以上的改进,闭包不再有强引用。但是,使用[unowned self]要小心,如果在调用闭包之前已经释放了对象,则会导致崩溃。同样,你也可以修改实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
private var tyres = 1
private var closure : (() -> ()) = { }
init() {
closure = {
[weak self] in self?.tyres += 1 // weak declaration
print(self?.tyres ?? "")
}
}

func foo() {
closure()
}
}

这里,[weak self]返回与[unowned self]相同的结果,但它是可选处理的。

在闭包的上下文中,从周围的作用域捕获变量和常量。这在它和闭包所需的值之间建立了紧密的连接。在我们的项目中,很可能会有数千个闭包,这意味着检查每一个闭包的内存问题将非常耗时。可以在Xcode中监控内存泄漏;所需要的是打开Instruments 并选择Leaks

Navigate to Xcode and then Open Developer Tool → Instruments → Leaks.

打开后,选择模拟器和应用程序目标并跟踪需要修复的泄漏。

作为最佳实践,在处理闭包或代理时,最好使用weakunowned。在你的项目中保持健壮的编码风格,这样weak self的存在就会立刻显现出来。一旦你准备好了,就去安装SwiftLint,这是一个执行编码标准的强大工具。为了整个团队的利益,编译器时的问题可以被发现,代码样式可以被自动化。

提高Arrays的利用率

Arrays通常将它们的元素存储在不相邻的内存块中。只需分配一个新块并将其附加到数组中,就可以添加数组中的新元素。这对于添加来说非常好,但是对于迭代来说就不那么好了。所以,如果你在一个巨大的数组上迭代,ContinuousArray可能是一个很好的选择。

当使用ContinuousArray 时,它确保数组的所有元素按顺序排列。这在查找以下信息时非常有用。这是一种取舍,一如既往,没有什么神奇的事情发生。由于在ContinuousArray中增加了对数组管理的限制,插入和追加等任务现在变得更加困难。由于我们最近的更改,您的用例将不再受到限制。

Swift对象通常表现良好,我们可以忽略内存问题和安全问题,因为Swift为我们处理一切。这对整体性能有负面影响。您可以使用withUnsafeBufferPointer函数来获取数组元素的指针数组,这使您能够在安全性和性能之间进行权衡。但是您需要小心,因为如果由于某种原因这些元素被释放,它可能会导致崩溃。

Apple SDK for Sentry允许你监视和跟踪应用性能、用户会话、用户可能面临的崩溃等。

一般来说, Sentry.io 中捕捉到这个事件的问题应该可以检查。由其他原因引起的错误不应该显示。要在Discover或Issues页面中查找它们,请使用未处理的错误搜索过滤器unhandled:true。因为会话不受数据速率的限制,所以未处理事件的数量预计不会与失败会话的数量匹配。

利用范型

Swift的泛型类型提供了一个强大的抽象工具。Swift编译器用T的任意值构造CustomFunc<T>。还需要一个函数指针表和一个包含T的方框。这是因为CustomFunc<Int>的行为与CustomFunc<String>的行为不同。下面是一个泛型函数的例子:

1
2
class CustomFunc<T> { ... }
CustomFunc<Int> X // Same function CustomFunc<String> Y // Works for different data types

每次调用此类代码时,Swift都会尝试识别正在使用的具体(非泛型)类型。当优化器看到泛型函数声明并理解具体类型时,Swift编译器可能会生成根据该类型定制的泛型函数的变体。专门化消除了泛型的管理开销。下面是一些更多的泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomStack<T> {
func push(_ element: T) { ... }
func pop() -> T { ... }
}

func customAlgorithm<T>(_ a: [T], length: Int) { ... }

var stackOfInt: CustomStack<Int>

for i in ... {
stack.push(...)
stack.pop(...)
}

var arrayOfInt: [Int] // Compiler emits a specialized version for [Int] type

customAlgorithm(arrayOfInt, arrayOfInt.length)

为了让优化器执行专业化,泛型声明定义必须能够在当前Module中可见。除非启用了-whole-module-optimization开关,否则只有在泛型的声明和调用都与泛型的调用在同一个文件中时才会发生这种情况。

标准库是这一规则的一个例外。标准库中的定义可以在所有模块中使用,并且可以定制以满足特定的需求

优化SpriteKit

SpriteKit是一个快速的2D框架,它使用苹果的Metal库直接访问GPU。像iPad Pro这样的设备有120Hz的显示,你需要努力保持帧更新在分配的8毫秒内。

从你的应用程序包加载纹理(Textures)是非常昂贵的。即使图片很普通,尝试加载一个全屏的背景图片可能会导致你超过你分配的时间限制,导致丢失帧。确保你在背景中预加载纹理,这样当你需要它们的时候,它们就会准备好了。因此,丢失帧的风险大大降低。

纹理或动画停顿和缓慢移动的UI元素会激怒用户,并影响整体用户体验。有两种方法可以评估这类体验:慢速和定格帧(slow and frozen frames)。如果你想让你的应用程序正常运行,这两种情况都应该避免。Sentry’s SDK 跟踪在设备上渲染时遇到的慢帧和冻结帧。

理解SKTexture类似于UIImage,因为它直到需要时才真正加载数据,这对于理解它是如何工作的至关重要。正因为如此,即使是非常大的照片,这种代码也几乎是瞬间的:

1
let texture = SKTexture(imageNamed: "Void")

然而,一旦该纹理被分配给游戏场景中的精灵节点(sprite node ),它就必须在渲染之前被加载。理想情况下,你希望加载发生在场景显示之前——可能在加载屏幕中——以减少帧的困难;因此,你应该像下面这样预加载:

1
2
3
texture.preload {
print("Texture is ready!")
}

使用.replace代替Blend

游戏渲染是开发过程中最耗时的部分之一,甚至包括所有需要的计算。这很复杂,因为大多数精灵都有不规则的形式和alpha透明度,场景通常有许多层,效果经常为场景提供生命。

你可以通过告诉SpriteKit渲染没有alpha透明度的精灵,例如实体形式或背景图片来避免这种情况:

1
yourSprite.blendMode = .replace

实际上,这意味着SpriteKit不需要读取旧的颜色值并将其与新的颜色值混合。

分析访问级别

在程序执行的过程中,由类形成的对象的方法调用和参数访问仍然是未知的。这意味着当你点击Xcode中的运行按钮时,编译器就会启动并执行分配内存和评估多态性使用等任务。最后,如果一个方法或参数不能从程序的作用域之外访问,编译器就会指出它是final的。

当您知道一个类不是任何其他类的基类时,就有必要在类声明中添加final关键字。将类添加到类中时,在类的所有参数和方法中都包含类的最终定义。

因此,让我们假设您想要覆盖类的行为。这样做的后果是,这个类不允许使用final关键字。一个新特性允许将子类不能访问的所有参数和方法指定为private

总结

选择在任何特定情况下采用的任何性能改进可能需要一些思考、测试和试验,特别是当我们希望在向代码中添加更多数据时保持代码的效率时。可能需要混合使用各种性能改进技术,而不是简单地使用一种技术,以获得所需的性能特征。扩展您对Swift以外的理解通常对于确定每种情况的最佳格式非常重要。

在本文中,您了解了如何使用最佳实践优化现有代码库、模块化体系结构以及在代码中创建和利用可重用组件,从而提高iOS应用程序的性能。