31. 性能精通
性能优化的目标是“稳定、可预期”,不是临时修补。核心流程是:指标 → 工具 → 定位 → 优化 → 验证。
一、先量化再优化
1. 帧预算概念
- 60 FPS:每帧约 16.7ms
- 120 Hz 设备:每帧约 8.3ms
超过这个预算,就会出现掉帧或卡顿。
2. 自己实现一个 FPS 监控
final class FPSMonitor {
private var link: CADisplayLink?
private var lastTime: CFTimeInterval = 0
private var count: Int = 0
func start(_ handler: @escaping (Int) -> Void) {
stop()
link = CADisplayLink(target: self, selector: #selector(step))
link?.add(to: .main, forMode: .common)
_handler = handler
}
func stop() {
link?.invalidate()
link = nil
lastTime = 0
count = 0
}
private var _handler: ((Int) -> Void)?
@objc private func step(_ link: CADisplayLink) {
if lastTime == 0 {
lastTime = link.timestamp
return
}
count += 1
let delta = link.timestamp - lastTime
if delta >= 1 {
let fps = Int(round(Double(count) / delta))
_handler?(fps)
count = 0
lastTime = link.timestamp
}
}
}
二、Instruments 的正确打开方式
1. Time Profiler
定位 CPU 热点函数,找到“最耗时”的逻辑。
2. Core Animation
查看掉帧区域和渲染瓶颈,尤其适合排查复杂列表和动画。
3. Allocations / Leaks
观察内存曲线是否持续上升,定位泄漏对象。
4. Main Thread Checker
找出不应该在主线程执行的任务。
三、用 Signpost 标记关键流程
import os.signpost
let log = OSLog(subsystem: "com.jj.app", category: .pointsOfInterest)
let signpostID = OSSignpostID(log: log)
func renderFeed() {
os_signpost(.begin, log: log, name: "FeedRender", signpostID: signpostID)
// 渲染逻辑
os_signpost(.end, log: log, name: "FeedRender", signpostID: signpostID)
}
这样可以在 Instruments 中清晰看到“关键业务流程耗时”。
四、卡顿排查流程(实战版)
- 复现场景(如列表滑动、进入详情)
- 打开 Time Profiler + Core Animation
- 找到主线程耗时函数
- 判断是“计算重”还是“IO/解码重”
- 调整代码并对比优化前后
五、性能问题典型场景与解决
1. 图片解码导致掉帧
问题:UIImage(data:) 在主线程解码成本高。
解决:后台线程解码 + 缩略图。
import ImageIO
func downsample(data: Data, to size: CGSize, scale: CGFloat) -> UIImage? {
let options = [kCGImageSourceShouldCache: false] as CFDictionary
guard let source = CGImageSourceCreateWithData(data as CFData, options) else { return nil }
let maxDimension = max(size.width, size.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension
] as CFDictionary
guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { return nil }
return UIImage(cgImage: image)
}
2. JSON 解析阻塞主线程
问题:解析大 JSON 在主线程执行。
解决:解析放到后台线程。
DispatchQueue.global().async {
let result = try? JSONDecoder().decode([Post].self, from: data)
DispatchQueue.main.async {
self.items = result ?? []
self.tableView.reloadData()
}
}
3. 列表布局过重
问题:cellForRowAt 做了大量计算。
解决:预计算高度、缓存布局结果。
final class HeightCache {
private var cache: [Int: CGFloat] = [:]
func height(for id: Int) -> CGFloat? { cache[id] }
func setHeight(_ height: CGFloat, for id: Int) { cache[id] = height }
}
六、内存与泄漏排查
1. 闭包循环引用
class PostVC: UIViewController {
var onFinish: (() -> Void)?
func setup() {
onFinish = { [weak self] in
self?.dismiss(animated: true)
}
}
deinit { print("PostVC deinit") }
}
2. 通知未移除
class ObserverVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(onMsg), name: .init("msg"), object: nil)
}
@objc private func onMsg() {}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
七、启动性能优化要点
- 启动时只做“必须任务”
- 非关键模块延迟初始化
- 同步 IO 改为异步读取
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 必要初始化
DispatchQueue.global().async {
// 延迟加载统计、日志等
}
return true
}
八、验证与回归
优化后必须验证:
- FPS 是否提升
- CPU 曲线是否下降
- 内存是否稳定
- 核心业务是否被影响