性能优化
一、性能优化总体思路
性能优化的目标是:让应用运行更流畅、占用更少资源、响应更快、功耗更低。优化可分为以下几个维度:
- 启动速度优化
- 内存优化
- CPU优化
- I/O与磁盘优化
- 网络优化
- 渲染与掉帧优化(UI流畅度)
- 包体积与资源优化
二、应用启动优化
问题1:iOS 应用启动流程分为哪几步?
答案:
- pre-main阶段:系统加载可执行文件、动态库、初始化ObjC类、执行+load方法等。
- main阶段:执行main()函数到UIApplicationMain()前的逻辑。
- 首屏渲染阶段:应用创建UIWindow并显示首屏。
优化方向:
- 减少动态库数量(合并静态库、使用静态链接)
- 避免过多的+load方法(改为懒加载)
- 减少didFinishLaunching中复杂逻辑
- 提前异步初始化非关键模块(使用dispatch_async)
- 缓存上次状态(快速恢复UI)
三、内存优化
问题2:如何定位 iOS 应用的内存泄漏?
答案:
- 使用 Instruments 的 Leaks、Allocations 工具 定位内存泄漏位置。
- 检查闭包中的 [weak self] 使用是否正确。
- 避免循环引用(如 delegate 应该用 weak)。
- 检查 NSTimer、CADisplayLink 是否被释放。
- 检查 NotificationCenter 注册后是否移除观察者。
问题3:如何降低内存占用?
答案:
- 控制图片内存(按需加载、使用SDWebImage缓存)
- 使用autoreleasepool控制局部内存释放
- 大对象懒加载(如大数组、缓存)
- 使用 Instruments -> Memory Graph 查看对象生命周期
四、CPU 优化
问题4:哪些操作最容易造成 CPU 占用过高?
答案:
- 大量布局计算(如 AutoLayout 嵌套)
- 主线程进行 JSON 解析或图片解码
- 大量循环、排序、正则匹配等运算
- 滚动时频繁调用layoutSubviews
优化方向:
- 使用异步线程处理计算任务
- 减少 AutoLayout 层级(可用 Frame 计算或 SnapKit 约束复用)
- 预计算高度或布局
- 使用高效的数据结构(如 Dictionary 替代多重遍历)
五、I/O 与磁盘优化
问题5:如何优化文件读写性能?
答案:
- 避免主线程 I/O 操作(放入异步队列)
- 使用缓存机制(如 NSCache)
- 合理分批写入(避免频繁小文件写)
- 压缩或二进制存储结构体数据
- 删除无用缓存文件
六、网络优化
问题6:网络层的常见性能优化手段有哪些?
答案:
- 请求合并、并发控制(例如使用 URLSessionConfiguration)
- 启用 HTTP/2,多路复用
- 启用 Gzip 压缩
- 缓存静态资源(URLCache)
- 断点续传与增量更新
- 合理使用 CDN
- 降级与重试机制,弱网优化
七、渲染性能优化(UI流畅度)
问题7:为什么会掉帧?
答案:
- 一帧渲染周期约16.7ms,超出时间就会掉帧。
- 常见原因:
- 主线程阻塞(计算、网络、I/O)
- 频繁创建/销毁视图
- 图层混合(overdraw)
- 图片解码耗时
优化方向:
- 异步绘制(CoreText、YYText)
- 减少离屏渲染(避免圆角+mask)
- 视图复用(UITableView、UICollectionView)
- 图片提前解码(SDWebImage有缓存解码机制)
八、包体积优化
问题8:如何减小应用包体积?
答案:
- 移除无用资源、图片压缩(TinyPNG等)
- 采用 Assets Catalog、App Thinning(Bitcode + Slicing)
- 移除未使用的类与库
- 资源动态下发(热更新或 CDN)
- 使用 Swift Package Manager 管理第三方库,避免重复依赖
九、监控与分析
问题9:常见的性能监控指标有哪些?
答案:
- CPU 使用率
- Memory 占用
- FPS(帧率)
- 卡顿次数与时长
- 启动时长
- 网络耗时与失败率
常用工具:
- Instruments(Time Profiler, Allocations, Leaks)
- Xcode Organizer(分析崩溃、性能)
- 自研性能监控系统(如卡顿检测、OOM监控)
十、典型面试题综合演练
问题10:如何检测和优化界面卡顿?
答案:
- 利用 CADisplayLink 或 RunLoop Observer 监控主线程卡顿。
- 发现耗时操作(布局、图片解码等)后,移动到子线程。
- 对复杂绘制使用异步渲染(如 YYAsyncLayer)。
- 使用 Instruments 的 Time Profiler 定位耗时函数。
高频考点:
一、启动速度优化(Cold/Warm/Hot)
问题1:iOS 启动分 Cold/Warm/Hot,有何区别?各如何优化与验证?
答案:
- Cold Launch(首次/被杀后):需加载可执行文件、动态库、ObjC 元数据、Swift 运行时,代价最大。
- Warm Launch(仍在内存但无进程):系统已有部分缓存,较快。
- Hot Launch(前后台切换):直接恢复前台,最快。
优化要点(Cold 为重心):
- 精简 pre-main:合并/静态化三方库,减少 +load、C 级别构造器;延迟非关键注册(如日志、埋点)。
- Main 阶段减负:didFinishLaunching 内只保留“首屏必要”,其他全部异步/懒加载。
- 首屏轻量化:减少首屏层级与网络依赖,Skeleton/占位图先画出来,数据再补。
- 资源就近化:把首屏必需资源放本地或采用预取;避免解压/大图首次解码阻塞主线程。
验证:
- 用 DYLD_PRINT_STATISTICS/启动日志量化 pre-main;用 Signpost/os_log 打点 T0~首帧;Xcode Organizer/OpenTelemetry 收敛。
问题2:如何系统性降低 pre-main 时间?
答案:
- 禁用/合并动态库:减少 dyld 绑定、重定位。
- 消灭滥用 +load:挪到 initialize/懒加载/启动后异步;避免全局构造体初始化做重活。
- 裁剪 Swift 反射:减少泛型/协议装箱的元数据成本;禁 @objc/动态派发仅保留必要处。
- 资源放 Assets/Slicing:避免启动期 IO/解压。
验证:
- DYLD_PRINT_STATISTICS 对比;Time Profiler 查看 objc_classInitialize、map_images 等调用栈。
二、渲染与掉帧(Core Animation/RunLoop)
问题3:一帧 16.7ms 的预算如何被吃光?怎么查卡顿根因?
答案:
- 渲染流水线:布局(Auto Layout/手算)→绘制(Core Graphics/文本/图片解码)→合成(Core Animation)。
- 常见罪魁:
- 主线程做重计算/IO/图片解码;
- 复杂约束抖动(layoutIfNeeded 频繁触发);
- 离屏渲染(圆角+mask、阴影无 shadowPath、group opacity);
- 过度绘制(overdraw)/视图频繁创建销毁。
定位:
- Instruments:Time Profiler、Core Animation(Color Offscreen-Rendered/Blended Layers/Overdraw);
- RunLoop 卡顿监控:注册 CFRunLoopObserver 采样 BeforeSources/AfterWaiting 时间窗,>阈值上报堆栈。
优化:
- 异步绘制(文本/富文本用 YYText 思路或 CATiledLayer);
- 避免离屏:用 shadowPath、shouldRasterize 谨慎开启(静态、低动态性视图);
- 减层级、复用 Cell、预计算尺寸;
- 图片提前解码+按需下采样,滚动中暂停复杂绘制。
问题4:离屏渲染与过度混合如何快速消除?
答案:
- 离屏渲染:圆角+遮罩、阴影无路径、rasterize 滥用。
- 用 layer.cornerRadius + continuousCornerRadius(避免 maskToBounds),阴影配 shadowPath,对静态卡片谨慎 shouldRasterize=YES。
- 过度混合(Blend):半透明视图叠加。
- 尽量不透明绘制:opaque=YES、用纯色背景、减少 alpha 叠加。
验证:
- Core Animation 勾选 “Color Offscreen-Rendered/Blended Layers/Overdraw”。
三、图片与文本性能
问题5:大图造成内存暴涨/卡顿,如何治理?
答案:
- 按显示尺寸下采样:CGImageSourceCreateThumbnailAtIndex,避免一次性 decode 原始分辨率。
- 懒加载+解码复用:滚动前预解码,使用 NSCache 控制峰值;内存压力时清理。
- 格式选择:首屏小体积优先(如 HEIC/HEVC 照片资源);雪碧图与向量资源按场景取舍。
- 避免主线程解码:子线程解码后回主线程设图。
验证:
- Allocations/VM Regions 看 IOSurface、CGImage 峰值;滚动 FPS 改善作为验收。
问题6:长文富文本如何兼顾流畅与排版质量?
答案:
- 异步排版/绘制:文本测量与排版在后台,主线程只接管结果层;使用文字分块/分页。
- 可复用缓存:按字体/行宽/内容 key 缓存排版结果;滑出屏幕即回收。
- 截断策略:首屏只渲染可见行数,进入详情再全渲染。
验证:
- 滚动 FPS、CPU 火焰图(排版函数占比下降)。
四、Auto Layout 与列表优化
问题7:Auto Layout 为何慢?如何“既优雅又快”?
答案:
- 复杂约束会触发约束求解器回溯;频繁 layoutIfNeeded/setNeedsLayout 在滚动中放大。
- 优化:
- 减少嵌套层级;复用约束(isActive 开关),避免循环依赖;
- 大量同构 Cell 改用手算 frame 或少量约束;
- 在 tableView(_:heightForRowAt:) 预计算行高,或使用自适应但缓存结果;
- 列表用 预取(prefetchDataSource)、Diffable Data Source 降低大批量 reload 抖动。
验证:
- Time Profiler 搜 layoutSubviews、updateConstraints;滚动掉帧显著减少。
五、网络与弱网体验
问题8:HTTP 性能优化三板斧?如何在弱网下稳住体验?
答案:
- 请求层:连接复用(HTTP/2/3)、并发控制、队列化/合并请求、开启压缩。
- 缓存层:合理 URLCache/ETag/If-None-Match,静态资源长缓存;幂等 GET 优先命中本地。
- 数据层:协议瘦身(字段裁剪/二进制/Protobuf)、分页/增量更新。
- 弱网兜底:超时/重试、降级(低清图/低频刷新)、离线缓存、断点续传。
验证:
- 埋点:RT、成功率、首包时延;抓包对比字节数;弱网代理下 A/B 验证体验。
六、磁盘 IO / 数据库 / Core Data
问题9:为什么频繁小 IO 会拖慢应用?如何改造?
答案:
- 小 IO 触发多次系统调用与磁盘寻址,放大延迟。
- 优化:
- 批量写入、延迟合并(队列缓冲);
- 顺序写优先(避免随机写);
- SQLite 开 WAL、批事务、预编译语句;
- Core Data 用 NSBatchUpdate/Delete、启用 Faulting、只取需要字段;
- 后台清理陈旧缓存、按磁盘配额裁剪。
验证:
- Instruments Filesystem、SQLite 分析;业务时延与耗电数据下降。
七、内存与 OOM(Jetsam)
问题10:iOS OOM 为何难以“捕获”?如何“预防+事后复盘”?
答案:
- OOM 为系统 Jetsam 杀进程,无崩溃栈,不会抛异常。
- 预防:
- 控峰:NSCache + 内存告警(UIApplication.didReceiveMemoryWarningNotification)清理;
- 图片下采样/及时释放临时大对象(循环中加 @autoreleasepool{});
- 避免闭包/定时器/通知导致的循环引用。
- 复盘:
- 记录会话的内存曲线/关键场景快照(对象计数/页面);
- 启用 MetricKit 收集 Jetsam 报告(机型/系统/前台后台)。
验证:
- 崩溃率下降、内存峰值与页面切换驻留下降。
八、线程、锁与调度
问题11:为什么“把任务丢到子线程”仍会卡顿?怎么做才有效?
答案:
- 子线程任务竞争 CPU、锁冲突、错误的 QoS(质量等级)导致主线程饥饿。
- 优化:
- 正确使用 GCD QoS(UI 回主线程、后台解码用 Utility/Background);
- 减少共享可变状态;锁用 读写锁 替代重锁;
- 用生产者-消费者模型限速;避免在主线程等待 semaphore。
验证:
- Time Profiler 观察调度与锁等待;主线程占用率下降、卡顿事件减少。
九、Swift/ARC 与动态派发
问题12:ARC 也会“吃 CPU”,如何写出更省的 Swift 代码?
答案:
- 释放成本:热点循环频繁 retain/release;对象图深会放大 ARC 压力。
- 优化:
- 选择 值类型 + COW(如 Array/Data)降低引用计数风暴;
- final class/private 降低动态派发,启用内联(@inlinable 谨慎用);
- 热路径减少可选/桥接(as Any/Foundation <-> Swift 类型转换)。
验证:
- 火焰图看 swift_release/retain 占比;等价改写后 CPU 下降。
十、能耗优化(Energy)
问题13:应用“看起来不卡”,却非常耗电,怎么查?
答案:
- 高频唤醒:定时器/定位/蓝牙 keep-alive;
- 后台 IO/网络:反复小包/心跳;
- 传感器/动画:长时间 CADisplayLink、动画未停止。
优化:
- 合并心跳、使用 BGTaskScheduler 做批处理;
- 后台传输用 URLSession background;
- 停止不可见页面动画/DisplayLink;降低定位精度与频率。
验证:
- Instruments Energy Log 下降;日常使用温升/掉电速度改善。
十一、监控与量化(必问)
问题14:线上如何持续量化性能并驱动优化闭环?
答案:
- 指标体系:启动(T0~首帧/可交互)、FPS、CPU/Mem、网络 RTT/成功率、IO 时延、能耗事件。
- 采集:Signpost 埋点、MetricKit、崩溃/OOM/Jetsam、弱网回放;
- 治理:设阈值与回退闸,灰度对比 A/B,版本周报跟踪回归。
一句话答题模板:“先建模(指标/目标),再采集(端侧+服务端),再治理(阈值/A-B/回退),最后复盘(周报+回归测试)。”
十二、可直接复述的代码片段(面试易加分)
片段A:图片按需下采样(避免一次性解码原图)
func downsampledImage(from url: URL, to size: CGSize, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
let sourceOpt = [kCGImageSourceShouldCache: false] as CFDictionary
guard let src = CGImageSourceCreateWithURL(url as CFURL, sourceOpt) else { return nil }
let maxPixel = max(size.width, size.height) * scale
let opt: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixel
]
guard let cgimg = CGImageSourceCreateThumbnailAtIndex(src, 0, opt as CFDictionary) else { return nil }
return UIImage(cgImage: cgimg)
}片段B:RunLoop 卡顿监控(采样主线程卡点)
final class HiccupMonitor {
private var observer: CFRunLoopObserver?
private let threshold: CFTimeInterval = 0.2
private var lastTS: CFTimeInterval = 0
func start() {
let activities: CFRunLoopActivity = [.beforeSources, .afterWaiting]
let cb: CFRunLoopObserverCallBack = { observer, activity, info in
let m = Unmanaged<HiccupMonitor>.fromOpaque(info!).takeUnretainedValue()
let now = CACurrentMediaTime()
if activity == .beforeSources { m.lastTS = now }
if activity == .afterWaiting, now - m.lastTS > m.threshold {
// 这里上报堆栈/打点
}
}
var ctx = CFRunLoopObserverContext(version: 0, info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil, release: nil, copyDescription: nil)
observer = CFRunLoopObserverCreate(kCFAllocatorDefault, activities.rawValue, true, 0, cb, &ctx)
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
}
}片段C:网络缓存与 ETag(弱网省流)
var req = URLRequest(url: url)
req.cachePolicy = .useProtocolCachePolicy // 配合服务器返回 ETag/Cache-Control
// 命中本地缓存则直接 304,不取大包
URLSession.shared.dataTask(with: req) { data, resp, err in
// 统计 RTT/缓存命中、失败率
}.resume()十三、结构化回答模板
结论先行(一句话) → 原理(为什么慢) → 手段(怎么做) → 验证(如何量化) → 风险(权衡/副作用)。
示例(离屏渲染):
- 结论:减少离屏渲染能显著提升滚动 FPS。
- 原理:离屏需额外开辟缓冲区 + 上下文切换。
- 手段:去遮罩、设 shadowPath、静态视图启用 shouldRasterize。
- 验证:Core Animation 可视化 + FPS 改善。
- 风险:栅格化缓存增内存,动态内容别开。
以上为“性能优化高频考点—可直接面试复述版”。需要的话我可以继续补一套**“场景题(列表卡顿、首屏慢、弱网大图、OOM复盘)逐题攻破与标准答法”**的扩展版——本段已结束,可继续下一组考点。
参考资料
- 《iOS 保持界面流畅的技巧》 —— ibireme (opens in a new tab)
- https://github.com/texturegroup/texture (opens in a new tab)
- https://github.com/johnil/VVeboTableViewDemo (opens in a new tab)
- https://github.com/forkingdog/UITableView-FDTemplateLayoutCell (opens in a new tab)
- https://github.com/facebook/AsyncDisplayKit/tree/master/examples/SocialAppLayout (opens in a new tab)