由浅入深,从基本概念到源码解析,再到实际项目应用,带你全面掌握 iOS 性能优化之道
一、什么是性能优化?
1.1 为什么性能很重要?
在移动端,性能直接关系到用户体验:
| 指标 |
用户感知 |
业务影响 |
| 启动速度 |
3 秒内无法进入应用,约 77% 用户会放弃 |
流失、留存下降 |
| 界面卡顿 |
掉帧、滑动不跟手 |
评价差、卸载 |
| 内存占用 |
应用被系统强杀、白屏 |
体验中断、投诉 |
| 耗电发热 |
续航变短、设备发烫 |
用户反感 |
苹果对 App Store 的审核和推荐也会考虑应用质量,性能是重要维度之一。
1.2 性能优化的核心目标
- 快:启动快、响应快、界面流畅
- 省:省内存、省电、省流量
- 稳:不崩溃、不卡死、不白屏
1.3 性能优化的「黄金法则」
先测量,再优化;先瓶颈,再细节。
盲目优化往往事倍功半。正确的做法是:用工具定位瓶颈,再针对性地优化。
二、性能指标与测量工具
2.1 关键指标
| 指标 |
说明 |
理想值 |
| FPS |
帧率,60fps 为流畅 |
≥ 55fps |
| 主线程耗时 |
单次任务在主线程的耗时 |
< 16ms(一帧) |
| 启动时间 |
冷启动/热启动到首屏可交互 |
冷启动 < 2s |
| 内存占用 |
常驻内存、峰值内存 |
视业务而定,避免持续增长 |
| CPU 占用 |
主线程 CPU 占比 |
空闲时尽量低 |
2.2 官方工具:Instruments
Instruments 是 Xcode 自带的性能分析工具套件:
- Time Profiler:CPU 耗时分析,定位主线程卡顿
- Allocations:内存分配追踪
- Leaks:内存泄漏检测
- Core Animation:离屏渲染、图层混合检测
- Energy Log:耗电分析
- Network:网络请求分析
2.3 第三方工具与库
| 工具 |
用途 |
特点 |
| YYFPSLabel |
实时 FPS 显示 |
开发阶段监控 |
| MLeaksFinder |
内存泄漏检测 |
无侵入、自动化 |
| Matrix(微信) |
综合性能监控 |
线上 APM |
| DoraemonKit |
开发调试面板 |
多维度自检 |
2.4 简单 FPS 监控实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class FPSMonitor { private var displayLink: CADisplayLink? private var lastTime: CFTimeInterval = 0 private var count: Int = 0 var fpsUpdate: ((Int) -> Void)?
func start() { displayLink = CADisplayLink(target: self, selector: #selector(tick)) displayLink?.add(to: .main, forMode: .common) }
@objc private func tick(_ link: CADisplayLink) { if lastTime == 0 { lastTime = link.timestamp return } count += 1 let delta = link.timestamp - lastTime if delta >= 1.0 { let fps = Int(round(Double(count) / delta)) fpsUpdate?(fps) count = 0 lastTime = link.timestamp } }
func stop() { displayLink?.invalidate() displayLink = nil } }
|
三、UI 与渲染优化
3.1 离屏渲染(Offscreen Rendering)
离屏渲染 是指 GPU 在当前屏幕缓冲区之外新开缓冲区进行渲染,再合成到主缓冲区的过程。额外的缓冲区和上下文切换会带来性能开销。
常见触发离屏渲染的属性:
| 属性 |
说明 |
cornerRadius + masksToBounds |
圆角裁剪 |
shadow(阴影) |
需要额外 Pass 计算 |
mask(遮罩) |
蒙版合成 |
group opacity |
组透明度 |
edge antialiasing |
抗锯齿 |
优化方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| imageView.layer.cornerRadius = 10 imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 10 imageView.layer.masksToBounds = true imageView.clipsToBounds = true
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 10) let mask = CAShapeLayer() mask.path = path.cgPath layer.mask = mask
|
阴影优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| view.layer.shadowOpacity = 0.5 view.layer.cornerRadius = 10 view.layer.masksToBounds = true
let containerView = UIView() containerView.layer.shadowOpacity = 0.5 containerView.layer.shadowRadius = 4 containerView.layer.shadowOffset = .zero
let contentView = UIView() contentView.layer.cornerRadius = 10 contentView.layer.masksToBounds = true contentView.frame = containerView.bounds contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] containerView.addSubview(contentView)
|
3.2 图层混合(Layer Blending)
当多个图层叠在一起且存在透明像素时,GPU 需要进行混合计算。减少透明区域和图层数量可以降低开销。
优化建议:
- 给不透明的视图设置
layer.opaque = true(或 isOpaque = true)
- 避免不必要的半透明叠加
- 减少视图层级
1 2 3
| view.layer.opaque = true view.backgroundColor = .white
|
3.3 TableView / CollectionView 优化
列表是 App 中最常见的性能瓶颈场景。
核心思路:
- Cell 复用:使用
dequeueReusableCell,避免重复创建
- 减少主线程工作:图片解码、复杂计算放到子线程
- 按需加载:快速滑动时减少或暂停非可见 Cell 的加载
- 高度缓存:
UITableViewAutomaticDimension 会反复计算,可缓存高度
示例:Cell 配置优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell let model = dataSource[indexPath.row] cell.imageView?.image = UIImage(contentsOfFile: model.imagePath) cell.label.text = heavyCompute(model) return cell }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell let model = dataSource[indexPath.row] cell.tag = indexPath.row cell.label.text = nil cell.imageView?.image = nil
DispatchQueue.global().async { let image = self.loadImage(path: model.imagePath) let text = self.heavyCompute(model) DispatchQueue.main.async { if cell.tag == indexPath.row { cell.imageView?.image = image cell.label.text = text } } } return cell }
|
预加载与 RunLoop 空闲优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = ... preloadIfNeeded(at: indexPath) return cell }
private func preloadIfNeeded(at indexPath: IndexPath) { let maxIndex = min(indexPath.row + 5, dataSource.count - 1) for i in (indexPath.row + 1)...maxIndex { if !imageCache.isCached(for: dataSource[i].imagePath) { DispatchQueue.global().async { _ = self.loadImage(path: self.dataSource[i].imagePath) } } } }
|
3.4 图片加载与解码优化
图片解码是 CPU 密集型操作,大图在主线程解码会导致卡顿。
1 2 3 4 5 6 7 8 9 10 11
| func decodeImage(_ image: UIImage) -> UIImage? { UIGraphicsBeginImageContextWithOptions(image.size, true, 0) image.draw(at: .zero) let decoded = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return decoded }
|
四、内存优化
4.1 内存管理基础
- 引用计数:OC 使用 MRC/ARC,Swift 使用 ARC
- AutoreleasePool:自动释放池,延迟 release
- 循环引用:block、delegate、闭包持有 self 未使用
weak 导致
4.2 AutoreleasePool 与 RunLoop
主线程 RunLoop 每次循环会创建并销毁一次 @autoreleasepool,因此临时对象会在一次循环结束释放。子线程若没有 RunLoop,需要手动加 @autoreleasepool,否则临时对象会堆积到线程结束。
1 2 3 4 5 6 7 8 9 10
| dispatch_async(dispatch_get_global_queue(0, 0), ^{ @autoreleasepool { for (int i = 0; i < 10000; i++) { NSString *temp = [NSString stringWithFormat:@"item_%d", i]; [array addObject:temp]; } } });
|
objc4 源码中的 AutoreleasePoolPage 结构(简化):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class AutoreleasePoolPage { magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; static void *operator new(size_t size) { return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE); } id *add(id obj) { } static void releaseAll() { } };
|
4.3 循环引用与 weak/strong
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class ViewController: UIViewController { var onComplete: (() -> Void)? func setup() { onComplete = { self.doSomething() } } }
onComplete = { [weak self] in self?.doSomething() }
onComplete = { [weak self] in guard let self = self else { return } self.doSomething() }
|
4.4 大对象与图片内存
一张 1000×1000 的 RGBA 图片,解码后约占 约 4MB 内存。使用 UIImage(named:) 会缓存,大图慎用。
1 2 3 4 5 6 7 8 9
| let image = UIImage(contentsOfFile: path)
let options: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageIfAbsent: true, kCGImageSourceThumbnailMaxPixelSize: 200 ]
|
五、启动优化
5.1 启动阶段
| 阶段 |
说明 |
可优化点 |
| pre-main |
dyld 加载、ObjC 初始化、+load、C++ 静态构造 |
减少 +load、精简动态库 |
| post-main |
main 到首屏可交互 |
异步化、延迟加载 |
5.2 pre-main 优化
- 减少动态库数量:合并动态库,能用静态库则用静态库
- 减少 +load:把逻辑迁移到
+initialize 或首屏使用再初始化
- 减少 ObjC 类/方法数量:删除无用代码,用 Swift 替代部分 OC
5.3 post-main 优化
1 2 3 4 5 6 7 8 9 10 11
| func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool { DispatchQueue.global().async { initAnalytics() } DispatchQueue.global().async { initCrashReporter() } DispatchQueue.global().async { initNetworkConfig() }
setupWindow() return true }
|
延迟加载:
1 2 3 4 5 6 7 8
| DispatchQueue.main.async { self.window?.rootViewController = MainTabBarController() DispatchQueue.main.async { initThirdPartySDK() } }
|
六、网络与 I/O 优化
6.1 网络请求优化
- 合并请求、减少请求次数
- 使用 HTTP/2 多路复用
- 合理设置超时与重试
- 大文件使用断点续传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class RequestMerger { private var pendingRequests: [String: [CompletionHandler]] = [:] private var inflight: [String: URLSessionTask] = [:]
func fetch(key: String, completion: @escaping (Data?) -> Void) { if let task = inflight[key] { pendingRequests[key, default: []].append(completion) return } let task = URLSession.shared.dataTask(with: url) { data, _, _ in let handlers = self.pendingRequests.removeValue(forKey: key) ?? [] DispatchQueue.main.async { handlers.forEach { $0(data) } } } task.resume() inflight[key] = task } }
|
6.2 文件 I/O 优化
- 避免在主线程做大量读写
- 小文件合并、大文件分片
- 使用
mmap 映射大文件
- 合理使用
Data(contentsOf:) 与流式读取
1 2 3 4 5 6 7 8 9 10 11
| if let stream = InputStream(fileAtPath: path) { stream.open() defer { stream.close() } let bufferSize = 1024 * 64 var buffer = [UInt8](repeating: 0, count: bufferSize) while stream.hasBytesAvailable { let read = stream.read(&buffer, maxLength: bufferSize) } }
|
七、多线程与 GCD 优化
7.1 主线程减压
任何耗时操作都不应阻塞主线程超过 16ms(约一帧)。
1 2 3 4 5 6
| DispatchQueue.global(qos: .userInitiated).async { let result = expensiveComputation() DispatchQueue.main.async { self.updateUI(with: result) } }
|
7.2 线程爆炸与串行化
过多并发会导致线程爆炸,反而不利于性能。可使用串行队列 + 多队列分组:
1 2 3
| let imageQueue = DispatchQueue(label: "com.app.image", qos: .userInitiated) let dbQueue = DispatchQueue(label: "com.app.db", qos: .utility)
|
7.3 避免锁竞争
1 2 3 4 5 6 7 8 9 10 11 12 13
| class ThreadSafeArray<Element> { private var array: [Element] = [] private let queue = DispatchQueue(label: "com.app.safe", attributes: .concurrent)
func append(_ element: Element) { queue.async(flags: .barrier) { self.array.append(element) } }
var last: Element? { queue.sync { array.last } } }
|
八、实际项目应用案例
8.1 案例一:电商首页 Feed 列表卡顿
现象:首页信息流快速滑动时明显卡顿,FPS 掉到 40 以下。
排查:
- Time Profiler 发现
cellForRow 内存在 UIImage(contentsOfFile:) 同步解码
- Core Animation 发现 Cell 内圆角 + 阴影组合触发离屏渲染
优化措施:
- 图片改为异步加载 + 子线程解码,使用 Kingfisher 的
downsamplingImageProcessor
- 圆角改为用
UIBezierPath 绘制圆角图,或用 cornerRadius 仅作用在 imageView.layer 上
- 高度缓存,避免
UITableViewAutomaticDimension 反复计算
效果:滑动 FPS 稳定在 58–60。
8.2 案例二:App 冷启动超 3 秒
现象:从点击图标到首屏出现超过 3 秒。
排查:
- 通过
DYLD_PRINT_STATISTICS 发现 pre-main 约 1.2s
- 发现 20+ 个动态库、多个
+load 中做了同步网络请求和大量注册
优化措施:
- 合并部分动态库,能静态链接的改为静态
- 移除
+load 中的网络请求和耗时逻辑,改为首屏展示后异步初始化
- 路由注册从「启动全量注册」改为「首次使用时按需注册」
效果:pre-main 降至约 0.6s,整体冷启动约 1.8s。
8.3 案例三:内存持续增长被系统强杀
现象:在某个二级页面反复进出多次后,App 被系统强杀。
排查:
- Allocations 发现每次进入页面,
ViewModel 和 NetworkManager 持续增长
- Leaks 未报明显泄漏,但 MLeaksFinder 提示
ViewController 未释放
根因:
NetworkManager 持有请求的 closure,closure 捕获了 ViewController
ViewController 又持有 NetworkManager 的 delegate,形成循环引用
优化措施:
- 所有回调使用
[weak self],并在回调内 guard let self
NetworkManager 的 delegate 改为 weak
- 请求完成后主动置空 completion,避免长生命周期持有
效果:反复进出页面,内存稳定回收,不再被强杀。
九、性能优化清单(自检表)
| 类别 |
检查项 |
| UI |
是否避免不必要的离屏渲染?图层是否过多?是否在子线程解码图片? |
| 列表 |
Cell 是否复用?高度是否缓存?是否做了预加载? |
| 内存 |
是否存在循环引用?大图是否控制解码尺寸? |
| 启动 |
动态库数量是否可控?+load 是否精简?是否延迟非必要初始化? |
| 网络 |
是否合并请求?超时和重试是否合理? |
| 线程 |
耗时操作是否在子线程?是否存在锁竞争或线程爆炸? |
十、小结
性能优化是一个持续的过程,需要:
- 建立指标体系:用 FPS、启动时间、内存等量化指标
- 善用工具:Instruments、APM、自研监控
- 由瓶颈入手:先解决主要矛盾,再优化细节
- 平衡取舍:在开发成本、可维护性和性能之间找平衡
- 回归验证:每次改动后做回归测试,避免引入新问题
掌握原理、熟练使用工具、结合业务实践,才能在真实项目中持续提升 App 的性能与体验。