应用技术
性能优化

性能优化

一、性能优化总体思路

性能优化的目标是:让应用运行更流畅、占用更少资源、响应更快、功耗更低。优化可分为以下几个维度:

  • 启动速度优化
  • 内存优化
  • CPU优化
  • I/O与磁盘优化
  • 网络优化
  • 渲染与掉帧优化(UI流畅度)
  • 包体积与资源优化

二、应用启动优化

问题1:iOS 应用启动流程分为哪几步?

答案:

  1. pre-main阶段:系统加载可执行文件、动态库、初始化ObjC类、执行+load方法等。
  2. main阶段:执行main()函数到UIApplicationMain()前的逻辑。
  3. 首屏渲染阶段:应用创建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:如何检测和优化界面卡顿?

答案:

  1. 利用 CADisplayLink 或 RunLoop Observer 监控主线程卡顿。
  2. 发现耗时操作(布局、图片解码等)后,移动到子线程。
  3. 对复杂绘制使用异步渲染(如 YYAsyncLayer)。
  4. 使用 Instruments 的 Time Profiler 定位耗时函数。

高频考点:

一、启动速度优化(Cold/Warm/Hot)

问题1:iOS 启动分 Cold/Warm/Hot,有何区别?各如何优化与验证?

答案:

  • Cold Launch(首次/被杀后):需加载可执行文件、动态库、ObjC 元数据、Swift 运行时,代价最大。
  • Warm Launch(仍在内存但无进程):系统已有部分缓存,较快。
  • Hot Launch(前后台切换):直接恢复前台,最快。

优化要点(Cold 为重心):

  1. 精简 pre-main:合并/静态化三方库,减少 +load、C 级别构造器;延迟非关键注册(如日志、埋点)。
  2. Main 阶段减负:didFinishLaunching 内只保留“首屏必要”,其他全部异步/懒加载。
  3. 首屏轻量化:减少首屏层级与网络依赖,Skeleton/占位图先画出来,数据再补。
  4. 资源就近化:把首屏必需资源放本地或采用预取;避免解压/大图首次解码阻塞主线程。

验证:

  • 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复盘)逐题攻破与标准答法”**的扩展版——本段已结束,可继续下一组考点。


参考资料