【iOS 印象】性能优化梳理(Swift)

性能监控

  • 业务性能监控:在 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 与数据源操作在主线程
  • 数据库操作、日志记录、网络回调在相应的固定线程
  • 不同业务,通过创建队列保证数据一致性

预加载与延迟加载

  • 预处理:耗时操作提前在后台线程进行处理
  • 延迟加载:必要可视内容优先加载,其他内容稍后或需要展示时再加载

参考与更多

猜你喜欢

转载自juejin.im/post/5b6d5a04518825615d2fdf33