ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

iOS性能优化实战:启动、卡顿、内存与网络全攻略

2026/10/1 3:49:18 拓冰建站 浏览量
iOS性能优化实战:启动、卡顿、内存与网络全攻略 1. 别急着优化先搞清楚瓶颈在哪我见过太多团队把性能优化做成“感觉驱动”老板觉得启动慢就闷头删启动逻辑测试反馈页面卡就怀疑是 tableView 的问题线上用户骂发热耗电又去查定位权限。结果是优化了一轮又一轮版本发出去用户照样给一星。做 iOS 性能优化这么多年我最大的体会是如果没有先量出基线你根本不知道自己在优化什么更不知道优化完是变好了还是变坏了。1.1 用户感知和性能指标怎么对应先建立一套客观指标。我惯用的分类是这样的用户说客观指标主要嫌疑对象“打开很慢”冷启动耗时、首帧渲染时间动态库加载、启动任务、首帧布局“划着划着就卡”掉帧率、主线程耗时主线程计算、离屏渲染、图片解码“一拍照就闪退”内存峰值、jetsam 记录图片内存、缓存膨胀、泄漏“Wi-Fi 下也一直转圈”网络请求耗时、超时率请求队列、DNS、弱网策略“用一会儿就发烫”CPU 占用率、耗电后台任务、定时器、无限重试这套映射不一定 100% 正确但它能帮你把模糊的用户反馈翻译成可量化的指标。真实项目里用户说的“卡”可能是网络慢用户说的“慢”可能是内存被杀后台重新加载一定要先确认再动手。1.2 我惯用的性能摸底流程第一步永远是用真机、Release 配置、关掉调试器跑一遍功能链路。Debug 模式下编译优化全开不了跑出来的数据和线上完全是两回事模拟器更不用说CPU 指令集和 GPU 渲染路径都不同只能看个大概。第二步是抓一轮 Instruments 现场。我会按启动、滑动、内存三个场景分别录制启动场景从点图标开始记录看 main 函数之前的时间和先帧渲染时间。滑动场景在首页/Feed 流来回滑动观察主线程占用和 Core Animation 掉帧。内存场景连续打开大图、进入详情页再返回盯 Memory Footprint 曲线。第三步是建立基线。把测出来的启动耗时、帧率曲线、内存峰值记进项目的性能看板后续每次改动都能对比。没有基线的优化都是耍流氓。2. 启动阶段每一毫秒都写在账单上冷启动是用户对 App 的第一印象也是性能优化里最有“技术含量”的部分。很多人以为启动慢就是 didFinishLaunching 里代码太多其实从点击图标到首帧渲染中间好几个阶段是主线程绕不过去的硬开销。2.1 冷启动的时间线拆解冷启动大致分成这几段用户点击图标系统启动进程。dyld 加载可执行文件和动态库做各种绑定和 rebase。运行时初始化ObjC 注册类、加载分类、执行load。调用main()然后是UIApplicationMain。application(_:didFinishLaunchingWithOptions:)执行。创建 window、rootViewController完成首帧渲染。用户能感知的时间是从第 1 步到第 6 步的全部时间。我见过不少优化方案只盯着第 5 步把 didFinishLaunching 里的代码挪走、延迟结果首帧确实快了但总启动时间没变化——因为瓶颈在第 2、3 步。2.2 main() 之前到底做了什么这一段不是靠“写代码”能优化的而是靠“减少东西”来优化的。动态库是最大的敌人之一。每多一个动态库dyld 就要多做一次加载和绑定。检查一下你的 App 链接了多少个动态库特别是通过 CocoaPods 或 Swift Package Manager 引入的库。如果某个库只是用了一两个功能优先考虑换静态库或者直接把源码编译进来。我实操过的一个项目把几个小功能动态库改成静态库后启动时间直接少了 200 多毫秒。load方法不能再加了。load是在main()之前执行的而且是同步的。统计一下项目里所有load很多第三方 SDK 都爱在这里做 hook 和注册。能改成initialize就改initialize是懒加载的不在启动关键路径上。实在不能改的确认一下里面有没有做重量级操作。类和方法的数量也在拖慢启动。方法数量太多会导致启动时的 selector 注册、缓存建立变慢。这个不会特别夸张但项目大到一定程度几千个类、几万行 Objective-C 代码的影响就出来了。可以考虑用__attribute__((objc_classes))之类的编译手段减少 ObjC 运行时开销或者把部分代码迁移到 Swift减少 Objective-C 动态派发的成本。调试启动阶段时打开环境变量能看到 dyld 的加载明细。在 scheme 的 Arguments 里加DYLD_PRINT_STATISTICS1启动后 console 会打印出每一步的时间开销哪家库加载慢了一眼就能定位。2.3 main() 之后的任务调度与异步化didFinishLaunching 里塞了一堆 SDK 初始化这是启动慢的另一大主因。但清理的时候不要一刀切全异步你要区分哪些是首帧必需哪些是可以晚一点再要的。我的分类逻辑是这样的首帧必需window 创建、rootViewController 创建、数据驱动的“首页骨架”。首帧后尽快统计、日志、推送注册、用户状态恢复。空闲时再做数据库迁移、缓存清理、预加载下一步页面。首帧必需的工作同步执行其余的全部延后。延后的手段不是简单DispatchQueue.main.async因为 async 到下一个 runloop 循环如果主线程被首帧任务占满效果有限。更靠谱的做法是在首帧渲染完成之后再执行或者注册 RunLoop 空闲回调在kCFRunLoopBeforeWaiting里处理低优先级任务。一个简单的示例func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) - Bool { // 首帧必需创建 window 和 rootViewController window UIWindow(frame: UIScreen.main.bounds) window?.rootViewController makeRootViewController() window?.makeKeyAndVisible() // 非必需延后到首帧完成 DispatchQueue.main.async { [weak self] in self?.setupAnalytics() self?.setupPush() } // 更不紧急的RunLoop 空闲时执行 addIdleTask { self.migrateDatabaseIfNeeded() } return true }实测下来启动任务按这个思路拆完很多项目能砍掉 30% 以上的启动耗时同时不会引入“广告页都出来了统计还没初始化”的问题。2.4 二进制重排与启动 trace 的读法App 启动过程会执行大量函数调用这些函数分布在二进制文件的不同页面上。如果主流程方法分散在多个内存页启动时就会反复触发缺页中断每一次都要去磁盘读页累积起来就是几百毫秒。解决办法是二进制重排把启动主路径上的方法集中放在连续的地址空间里减少缺页。实现方案是写一个.order文件列出启动路径上的符号然后在 Build Settings 里给Order File指过去。Xcode 14 之后的构建系统也支持这个方法。生成 order 文件最省力的方式是用 Instruments 的 App Launch 模板跑一遍启动导出符号列表作为参考。这个方法我之前在一个中型 App 上试过首启时间降了 100 多毫秒效果非常明显。启动 trace 的读法也有技巧。Instrument 的 App Launch 模板会给出一个清晰的阶段划分dyld、main、first frame。哪个阶段超长就针对哪个阶段做优化不要凭感觉改代码。我见过有人为了优化启动把didFinishLaunching里拆出来了一个后台线程跑数据初始化结果启动没快多少数据还经常没准备好导致页面空白——这就是典型的没看 trace、乱优化。3. 卡顿与掉帧别让主线程替你背锅启动性能搞定之后大部分用户抱怨集中在日常使用“滑动卡顿”。一说到卡顿很多人第一反应就是“主线程被阻塞了”这个判断没错但不够精确。主线程阻塞只是结果背后的原因千差万别。3.1 帧率、掉帧与屏幕渲染的底层关系屏幕每秒刷新 60 次意味着每一帧只有约 16.7 毫秒的预算。一次触摸事件、一次布局计算、一次图片解码、一次离屏渲染都可能把这一帧的预算耗尽。如果主线程在跑一个 100 毫秒的耗时任务屏幕就只能等到任务结束才更新表现在用户眼里就是“卡了一下”。现代 iPhone 的 ProMotion 屏幕是 120Hz 刷新率单帧预算只有 8.3 毫秒对主线程的要求更苛刻。所以优化目标不一定是“让每一帧都在预算内”这在真实业务里很难做到更务实的目标是别让单帧耗时超过 50 毫秒也就是掉帧不要超过 3 帧这是用户在快速滑动时基本感知不到的临界值。3.2 卡顿点的定位与 Instruments 实操定位卡顿最直接的手段是 Instruments 的 Time Profiler。操作路径是选择 Time Profiler 模板开始录制在 App 里复现卡顿操作通常是上下快速滑动列表停止录制然后在 Call Tree 里勾选“Invert Call Tree”和“Separate by Thread”过滤掉系统线程只看主线程的调用栈。这里有个容易被忽略的细节只盯采样次数最多的函数不一定是正确的卡顿源。比如滚动列表时整个滚动过程都在频繁执行layoutSubviews这不代表布局有问题。你要做的是在时间轴上找到掉帧的那个区间然后把光标定位过去看那一刻 CPU 在做什么。如果某一瞬间 CPU 占用从 20% 跳到 90%那 90% 的时刻的调用栈才是真正的卡顿点。我常用的另一个手段是自定义卡顿监控逻辑其实很简单监听 RunLoop 的beforeSources和afterWaiting两个状态记录时间差如果超过阈值就认为发生了卡顿同时抓取当前主线程的调用栈。这个方案很多大厂 App 都在用适合做线上监控但在开发期定位问题还是 Instruments 最直观。一个简易的监控思路final class MainThreadMonitor { var stuckThreshold: TimeInterval 0.05 func start() { let observer CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.beforeSources.rawValue | CFRunLoopActivity.afterWaiting.rawValue, true, 0) { _, activity, _ in // 记录时间戳若间隔超过阈值捕获主线程调用栈 } CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes) } }线上卡顿率的统计口径最好和 App 版本挂钩单独一个“卡顿率 1%”没意义要能看出 1.2.0 版本比 1.1.0 是涨了还是降了。3.3 图像加载、解码与离屏渲染的常见坑列表卡顿里最容易被忽视的坑是图片解码。很多人以为UIImageView设置 image 的瞬间就完成了所有图片工作实际上图片文件从磁盘读出来只是压缩数据真正显示时要先解码成位图。系统默认的解码时机是在主线程、要绘制的那一刻也就是你刚设置 image 的瞬间。一个大图解码可能耗时 50 到 100 毫秒主线程瞬间卡住。优化方案是预解码在子线程把图片绘制到一个CGContext或者用CIImage触发强制解码拿到解码后的CGImage再回主线程赋给 imageView。SDWebImage 和 YYImage 这类库内部有处理但如果你用了系统的UIImage(named:)或者自研图片加载器就得自己处理。离屏渲染也是老生常谈但永远有人踩的坑。cornerRadius加masksToBounds、给图层加阴影、使用shouldRasterize不当都会触发 GPU 额外开一块离屏缓冲区来渲染。优化思路很简单圆角图片用带圆角的资源不要运行时裁剪。阴影用shadowPath指定路径比默认的阴影计算快很多。shouldRasterize只适用于静态内容如果你把它加在会频繁变化的 cell 上反而是性能灾难。另一个常见问题是cell 高度频繁计算。iOS 的自动布局本身不慢但如果你在heightForRowAtIndexPath里创建临时视图来做估算那每次滚动都会重复计算。正确的做法是用UITableView.automaticDimension加预估高度或者缓存已计算过的高度按行号建一个字典。4. 内存与 CPU峰值降下来体验才稳得住内存优化显然没有启动优化那么“性感”但它直接决定用户会不会被系统杀掉后台、会不会在使用中 crash。iOS 的内存机制比较“暴力”到达阈值就发警告再涨就直接 jetsam。优化内存的目标有两个降低峰值、消除泄漏。4.1 内存预算是怎么算出来的不同设备的内存上限差异巨大。iPhone 15 Pro 可能有 8GB但老款 iPhone 可能只有 2 到 3GB可用额度完全不一样。App 层能控制的不是总物理内存而是系统给进程分配的 footprint。要建立一套内存基线首先要确认你的 App 在“崩溃前”能达到哪个量级。Xcode 的 Debug 菜单里有Memory GaugeInstruments 的 Allocations 模板也能看到内存使用。注意内存不是越低越好关键是可控。你需要在每个操作路径上知道内存大概会涨到多少比如进详情页涨 80MB退出后回落到 40MB这是正常曲线进详情页涨 80MB退出后还是 80MB那八成是泄漏或者全局缓存没释放。4.2 图片内存的精细化管理图片内存是 iOS App 内存消耗的大头。一张 1000x1000 的 RGBA 图片解码后的位图大小是 1000 x 1000 x 4 4MB这个数据是固定不变的内存占用。如果列表有 50 个这样的图片那就是 200MB非常容易触发 jetsam。管理图片内存有几个实用策略按屏幕尺寸采样列表缩略图不需要加载原图用preferredThumbnailSize或downsample机制先缩小再解码。系统提供CGImageSourceCreateThumbnailAtIndex可以指定kCGImageSourceThumbnailMaxPixelSize这是我最常用的方式。分页加载列表滚动时只加载将要显示的那几行的图片离开可视区域的 cell其图片资源可以释放。重复图片去重使用 NSCache 做缓存时key 必须是唯一的 URL 或路径但内存缓存里的 UIImage 最好统一到一份避免同一个图片被解码两份。观察内存警告在didReceiveMemoryWarning里清理可再生的缓存比如图片缓存、预加载的数据。NSCache会自动响应系统压力但自建缓存池要主动做清理。4.3 循环引用、大对象与 autoreleasepool 的实际取舍循环引用是内存泄漏最常见的原因。Block 捕获self而 self 又持有 block 时会出现。排查方式除了 Instruments 的 Leaks 模板还可以在业务代码里养成习惯deinit 不打印就是没释放。每个页面打开再关闭确认deinit被调用能挡住大部分泄漏问题。autoreleasepool的使用场景没有网上传的那么玄。ARC 下在主线程的 runloop 里会自动创建释放池但循环里创建大量临时对象时手动加一个autoreleasepool能显著降低峰值内存。典型场景是批量处理数据var thumbnailImages: [UIImage] [] for i in 0..1000 { autoreleasepool { // 每次循环结束自动释放临时对象 let tempImage processImage(at: i) thumbnailImages.append(tempImage) } }但需要注意autoreleasepool只对自动释放对象有影响对强引用持有的对象没作用。不要滥用否则只是给代码添乱。CPU 优化相对更直白定位到高耗时函数后优先考虑算法替换而不是无脑塞后台线程。比如数据处理量大的列表页把reloadData()换成diff更新会比把所有计算丢到异步队列更有效。计算本身如果非做不可要分析是否能缓存结果、是否能增量计算。过度并行化反而可能引入线程爆炸和锁竞争尤其在图片加载这类高频 IO 场景线程池化是最优解。5. 网络、缓存与 WebView用户等不起也转不起圈移动端性能优化绕不开网络。很多 App 的“卡顿”其实不是渲染问题而是网络请求迟迟不返回页面空转等待。网络优化做得好体感提升比处理几个卡顿点更明显。5.1 网络请求层的性能优化我知道这不是章标题里的常规内容但把它和 iOS 性能放在一起是因为网络请求的响应时间会直接拉长用户可感知的页面加载时间。基础策略包括请求合并同一个页面的多个数据接口如果互不依赖可以合并成一个聚合接口减少 RTT 开销。尤其在弱网环境每个请求的握手时间都可能超过 100ms。连接复用确保所有请求都走 HTTP/2 或 HTTP/3而不是每个请求新建 TCP 连接。NSURLSession默认有连接复用但要注意不要频繁创建新的 session。超时与重试策略设置合理的超时时间默认 60 秒太长了。页面级请求建议超时 15 秒以内上传任务可以长一些重试只能用于幂等请求并且要加指数退避否则弱网下会引发请求风暴。DNS 解析优化如果 App 首启或者网络切换后需要做域名解析可以用预解析缓存 DNS 结果或者直接使用 IP 直连方案。HTTPDNS 在很多业务里能明显减少首包时间。数据压缩服务端能开 gzip 的都开客户端能发送 Accept-Encoding 的都带上。高版本 HTTP 有更高效的压缩算法能省下不少传输流量。实际案例是我处理过一个 Feed 流加载慢的问题排查后发现每次刷新都会重新请求 20 多个列表接口每个请求之间还有串行依赖。改成聚合接口后首屏整体加载时间从 4 秒降到了 1.5 秒这比花一周优化主线程代码的效果好得多。5.2 图片加载与缓存策略网络图片是另一个大头。图片加载要考虑三级缓存内存缓存、磁盘缓存、网络加载。内存缓存用NSCache能自动响应内存紧张可以设置 countLimit 和 totalCostLimit。磁盘缓存注意清理策略按 LRU 或者按时间过期都行关键是别让它无限膨胀。网络加载要支持断点续传并做 ETag/Last-Modified 验证避免每次请求都下载全量图片。我通常按照“先查内存再查磁盘没有才发请求”的顺序设计图片加载器并在子线程做解码和缩放。如果使用第三方库要确认内部是否在解码后回调到主线程——网上有不少 SDK 会把解码放在回调线程但真正应用到 UI 上还要经过主线程中间那一步的耗时照样需要关注。5.3 WebView/H5 与原生交互的优化App 里嵌 H5 页面是性能的重灾区尤其是电商、内容社区这类重度使用 WebView 的场景。优化的核心逻辑是不要让 H5 加载依赖网络不要每次创建 WebView 都从零开始。常见的优化措施WKWebView 复用与预热提前创建 WebView 并预加载一个空白页或者常用页面这样用户真正进入 H5 时WebView 启动开销已经省掉。离线包策略把静态资源JS、CSS、图片打包进 App本地资源加载只从服务端拉取数据。这个方案对弱网用户体感提升最大。JS 注入与通信优化尽可能减少WKScriptMessageHandler的调用频率原生和 H5 之间的通信尽量批量传递避免一次改一个数据就触发一次 bridge。检查 cookie 与缓存策略H5 页面频繁刷新、重复登录很多时候是缓存策略设置不对或者 cookie 被意外清理。排查这类问题要同时看请求头里的 Cache-Control 和本地存储。还有一个热门场景是 H5 里唤起 App。要用 Universal Link 而不是自定义 scheme因为 Universal Link 不会弹出系统确认框体验更顺滑同时可以在链接上带参数直接跳转到目标页面。这个跳转逻辑不要放在主线程做耗时判断否则会阻塞 H5 渲染。关于“自动播放”这类限制更多是浏览器的媒体策略和性能优化关系不大但为了用户体验H5 页面应该尽量在自己可控的交互事件里去触发播放不要在首次加载时就强行播放。如果用户反馈播放不了优先确认 WebView 的 mediaTypesRequiringUserActionForPlayback 设置是否符合业务需求。6. 监控体系与线上告警优化过一次不代表一劳永逸很多团队把性能优化当成一次性项目版本发布前突击一波发完就没人管了。但真实情况是——性能是会退化的。新需求加了 init 库、某个同事在 didFinishLaunching 里多写了一行同步代码、某个页面无脑加载大图性能都会不知不觉跌回去。所以线上监控必须成为常态化机制。6.1 埋点与监控指标怎么定性能监控的核心指标不需要太多选几个能反映用户真实体验的就好。我的建议是最少四个维度启动耗时统计 cold start 和 warm start按版本、机型分桶。卡顿率基于 RunLoop 监控统计每秒掉帧次数重点看单次卡顿超过 50ms 的比例。内存峰值在页面进入、退出时记录 footprint监控异常增长。网络成功率把首页、详情页等关键接口的耗时和成功率单独统计阈值告警。每个指标都要能下钻到具体设备、系统版本、页面路径。用户报告“旧款 iPhone 一直闪退”如果监控数据里能看到 iPhone 8 的卡顿率和内存峰值上涨明显定位范围就小了很多。6.2 MetricKit 与自研监控的取舍苹果在 iOS 13 之后提供了 MetricKit能拿到启动耗时、卡顿、CPU、内存、磁盘等系统级诊断数据。它的好处是系统层面可信度高、不侵入 App 包体坏处是数据是日粒度聚合不是逐条实时上报适合做趋势分析不适合做实时告警。我现在的做法是两者配合线上用自研的轻量性能 SDK 埋点每天上报一次聚合数据同时开启 MetricKit 的MXMetricManager在后台拿到苹果自己的诊断结果用来交叉验证自研数据是否准确。自研监控要特别注意性能开销不能反噬业务。采集卡顿调用栈本身可能就消耗资源所以线上卡顿监控的采样率通常控制在 10% 以内并且只在用户主动上报问题或连续卡顿时才完整抓取调用栈。6.3 构建优化Xcode 打包突然变慢先别急着换电脑最后聊一个和 iOS 性能相关但容易被忽略的问题Xcode 打包突然很慢。不少团队遇到这个情况就归咎于电脑老化其实很多慢是构建系统状态脏了。我的一些排查经验清理 DerivedDatarm -rf ~/Library/Developer/Xcode/DerivedData删掉后重新编译很多时候能解决“没改几行代码但编译要跑十几分钟”的问题。检查索引状态Xcode 后台索引占用 CPU 会导致编译慢先确认 Index 进度或者 Product 菜单里选择 Build 之前先让索引跑完。看具体阶段如果卡在 link 阶段多半是二进制文件太大或链接器在做重复符号合并如果卡在 Run Script 阶段检查脚本里是不是执行了耗时的 shell 命令。关闭无用的重编译Swift 的 whole module optimization 适合 ReleaseDebug 模式尽量保持 Incremental可以加快日常迭代。构建速度和线上性能优化其实是同一套思维先量化再动手。别因为觉得“慢”就去换电脑先搞清楚慢在哪个阶段。写在最后的一点经验如果让我给 iOS 性能优化排优先级我的选择是启动速度 卡顿体验 内存稳定性 网络加载。原因是启动速度影响首次印象卡顿影响着整个使用过程的耐心内存直接关系到崩溃率网络问题则可以通过缓存和服务端优化来兜底。另一个很重要的认知是性能优化不是“技术炫技”而是都需要拿数据说话。每次改完我都习惯用同一台真机、同一套测试路径、同一个 Release 包复测一遍把优化前后数字对比记下来。即使某个改动没有带来明显的指标提升只要不影响体验也值得保留——因为这次改动很可能避免了一个未来的性能坑。最后给新手的建议是不要一开始就追求什么黑科技先从最简单的做起——把动态库改成静态库、批掉多余的load、给图片做采样缩放、给网络层加合理超时。这四个动作做完80% 的性能问题都能看到改善。剩下的那 20%等你真的遇到再说吧。