由浅入深,从基本概念到源码解析,再到实际项目应用,带你全面掌握 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 的性能与体验。