<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>卡顿 on Last Stand</title><link>https://amatsuzero.github.io/LastStand/tags/%E5%8D%A1%E9%A1%BF/</link><description>Recent content in 卡顿 on Last Stand</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Thu, 07 May 2026 16:54:12 +0800</lastBuildDate><atom:link href="https://amatsuzero.github.io/LastStand/tags/%E5%8D%A1%E9%A1%BF/index.xml" rel="self" type="application/rss+xml"/><item><title>卡顿-TableView优化</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-tableview%E4%BC%98%E5%8C%96/</link><pubDate>Sat, 02 May 2026 22:32:27 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-tableview%E4%BC%98%E5%8C%96/</guid><description>&lt;p&gt;列表滚动是用户最敏感的交互场景之一。本文介绍UITableView和UICollectionView的性能优化方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="列表卡顿的常见原因"&gt;列表卡顿的常见原因&lt;/h2&gt;
&lt;pre class="mermaid"&gt;flowchart TB
subgraph frame["每一帧（16.67ms内）需要完成"]
A["1. 计算可见Cell"] --&gt; B["2. 复用或创建Cell"]
B --&gt; C["3. 配置Cell内容"]
C --&gt; C1["设置文本"]
C --&gt; C2["加载图片"]
C --&gt; C3["计算布局"]
C --&gt; C4["渲染视图"]
C1 --&gt; D["4. 提交渲染"]
C2 --&gt; D
C3 --&gt; D
C4 --&gt; D
end
D --&gt; E["⚠️ 任何一步超时都会导致掉帧"]
&lt;/pre&gt;
&lt;h3 id="常见问题"&gt;常见问题&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;影响&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高度计算慢&lt;/td&gt;
&lt;td&gt;复杂布局、Auto Layout&lt;/td&gt;
&lt;td&gt;滚动卡顿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cell配置慢&lt;/td&gt;
&lt;td&gt;大量视图操作、图片加载&lt;/td&gt;
&lt;td&gt;滚动卡顿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复用失效&lt;/td&gt;
&lt;td&gt;未正确注册、identifier错误&lt;/td&gt;
&lt;td&gt;内存暴涨、卡顿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;离屏渲染&lt;/td&gt;
&lt;td&gt;圆角、阴影&lt;/td&gt;
&lt;td&gt;GPU瓶颈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;图片加载&lt;/td&gt;
&lt;td&gt;主线程解码&lt;/td&gt;
&lt;td&gt;滚动卡顿&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="cell复用机制"&gt;Cell复用机制&lt;/h2&gt;
&lt;h3 id="正确使用复用"&gt;正确使用复用&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OptimizedTableViewController&lt;/span&gt;: UITableViewController {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; cellIdentifier = &lt;span style="color:#e6db74"&gt;&amp;#34;OptimizedCell&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;viewDidLoad&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;.viewDidLoad()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 注册Cell类（推荐）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.register(OptimizedCell.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, forCellReuseIdentifier: cellIdentifier)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 或注册Nib&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// tableView.register(UINib(nibName: &amp;#34;OptimizedCell&amp;#34;, bundle: nil), forCellReuseIdentifier: cellIdentifier)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tableView&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; tableView: UITableView, cellForRowAt indexPath: IndexPath) -&amp;gt; UITableViewCell {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 使用dequeueReusableCell（带indexPath的版本会自动创建）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;: indexPath) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt;! OptimizedCell
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 配置Cell&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; item = items[indexPath.row]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cell.configure(with: item)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; cell
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="cell的prepareforreuse"&gt;Cell的prepareForReuse&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OptimizedCell&lt;/span&gt;: UITableViewCell {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; titleLabel = UILabel()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; avatarImageView = OptimizedImageView()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; currentTask: URLSessionTask?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;prepareForReuse&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;.prepareForReuse()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 重置状态&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; titleLabel.text = &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; avatarImageView.image = &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 取消进行中的任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentTask?.cancel()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentTask = &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; avatarImageView.cancelLoading()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;configure&lt;/span&gt;(with item: Item) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; titleLabel.text = item.title
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 异步加载图片&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; avatarImageView.setImage(from: item.avatarURL, placeholder: UIImage(named: &lt;span style="color:#e6db74"&gt;&amp;#34;placeholder&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="高度缓存"&gt;高度缓存&lt;/h2&gt;
&lt;h3 id="问题每次都计算高度"&gt;问题：每次都计算高度&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 问题代码：每次滚动都重新计算&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tableView&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; tableView: UITableView, heightForRowAt indexPath: IndexPath) -&amp;gt; CGFloat {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; item = items[indexPath.row]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; calculateHeight(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;: item) &lt;span style="color:#75715e"&gt;// 耗时操作&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="解决方案1预计算并缓存"&gt;解决方案1：预计算并缓存&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;HeightCachedTableViewController&lt;/span&gt;: UITableViewController {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items: [Item] = []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; heightCache: [IndexPath: CGFloat] = [:]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setItems&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; newItems: [Item]) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = newItems
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; heightCache.removeAll()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 预计算高度（可以在后台线程）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; precomputeHeights()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.reloadData()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;precomputeHeights&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; width = tableView.bounds.width
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (index, item) &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; items.enumerated() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; indexPath = IndexPath(row: index, section: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; heightCache[indexPath] = calculateHeight(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;: item, width: width)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;calculateHeight&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; item: Item, width: CGFloat) -&amp;gt; CGFloat {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 计算文本高度&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; textHeight = item.content.boundingRect(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; with: CGSize(width: width &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;, height: .greatestFiniteMagnitude),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options: [.usesLineFragmentOrigin, .usesFontLeading],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; attributes: [.font: UIFont.systemFont(ofSize: &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;)],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context: &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ).height
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; ceil(textHeight) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 加上其他元素的高度&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tableView&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; tableView: UITableView, heightForRowAt indexPath: IndexPath) -&amp;gt; CGFloat {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; heightCache[indexPath] ?? UITableView.automaticDimension
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 数据更新时更新缓存&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;updateItem&lt;/span&gt;(at indexPath: IndexPath, with item: Item) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items[indexPath.row] = item
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; heightCache[indexPath] = calculateHeight(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;: item, width: tableView.bounds.width)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.reloadRows(at: [indexPath], with: .automatic)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="解决方案2使用estimatedheight"&gt;解决方案2：使用estimatedHeight&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;EstimatedHeightTableViewController&lt;/span&gt;: UITableViewController {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; heightCache: [IndexPath: CGFloat] = [:]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;viewDidLoad&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;.viewDidLoad()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 设置估算高度（重要！）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.estimatedRowHeight = &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.rowHeight = UITableView.automaticDimension
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tableView&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 缓存实际显示的高度&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; heightCache[indexPath] = cell.bounds.height
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tableView&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -&amp;gt; CGFloat {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 优先返回缓存的高度&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; heightCache[indexPath] ?? &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="解决方案3固定高度"&gt;解决方案3：固定高度&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 如果所有Cell高度相同，直接设置固定值&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;viewDidLoad&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;super&lt;/span&gt;.viewDidLoad()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.rowHeight = &lt;span style="color:#ae81ff"&gt;80&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 固定高度，性能最好&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.estimatedRowHeight = &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 关闭估算&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="异步渲染"&gt;异步渲染&lt;/h2&gt;
&lt;h3 id="基本原理"&gt;基本原理&lt;/h3&gt;
&lt;p&gt;将复杂的渲染工作移到后台线程：&lt;/p&gt;</description></item><item><title>卡顿-主线程优化</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%BC%98%E5%8C%96/</link><pubDate>Sat, 02 May 2026 22:32:27 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%BC%98%E5%8C%96/</guid><description>&lt;p&gt;主线程阻塞是卡顿最常见的原因。本文介绍如何优化主线程的工作，减少阻塞时间。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="主线程的职责"&gt;主线程的职责&lt;/h2&gt;
&lt;p&gt;主线程（UI线程）负责：&lt;/p&gt;
&lt;pre class="mermaid"&gt;mindmap
root((主线程职责))
事件处理
触摸事件
手势识别
UI更新
布局计算
视图绘制
动画执行
Core Animation
UIView动画
系统回调
生命周期
AppDelegate
定时器
NSTimer
CADisplayLink
通知处理
NotificationCenter
KVO
&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;原则：主线程应该只做UI相关的轻量级工作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="常见的主线程阻塞场景"&gt;常见的主线程阻塞场景&lt;/h2&gt;
&lt;h3 id="1-耗时计算"&gt;1. 耗时计算&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 问题代码：在主线程进行复杂计算&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;processData&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; result = heavyComputation(data) &lt;span style="color:#75715e"&gt;// 阻塞主线程&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; updateUI(with: result)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 优化后：异步计算&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;processDataAsync&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .userInitiated).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; result = &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.heavyComputation(&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.updateUI(with: result)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-文件io"&gt;2. 文件I/O&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 问题代码：主线程读写文件&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadConfig&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? Data(contentsOf: configURL) &lt;span style="color:#75715e"&gt;// 阻塞&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; parseConfig(data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 优化后：异步I/O&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadConfigAsync&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .utility).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? Data(contentsOf: &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.configURL)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.parseConfig(data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-数据库操作"&gt;3. 数据库操作&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 问题代码：主线程数据库查询&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadUsers&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; users = database.query(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT * FROM users&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// 阻塞&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tableView.reloadData()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 优化后：异步查询&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadUsersAsync&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; database.queryAsync(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT * FROM users&amp;#34;&lt;/span&gt;) { [&lt;span style="color:#66d9ef"&gt;weak&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;] users &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;?.users = users
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;?.tableView.reloadData()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="任务异步化"&gt;任务异步化&lt;/h2&gt;
&lt;h3 id="基本原则"&gt;基本原则&lt;/h3&gt;
&lt;pre class="mermaid"&gt;flowchart TB
subgraph main["主线程任务"]
direction LR
A1["UI更新&lt;br/&gt;视图刷新、动画"]
A2["用户交互&lt;br/&gt;点击响应、手势处理"]
A3["轻量计算&lt;br/&gt;简单数据转换"]
end
subgraph bg["后台线程任务"]
direction LR
B1["复杂计算&lt;br/&gt;数据处理、算法"]
B2["I/O操作&lt;br/&gt;文件读写、网络请求"]
B3["数据库操作&lt;br/&gt;查询、写入"]
B4["图片处理&lt;br/&gt;解码、缩放、滤镜"]
end
main --&gt; |"轻量、快速"| UI((用户界面))
bg --&gt; |"耗时、阻塞"| main
&lt;/pre&gt;
&lt;h3 id="gcd任务调度"&gt;GCD任务调度&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;AsyncTaskManager&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 计算密集型任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;compute&lt;/span&gt;&amp;lt;T&amp;gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; work: @escaping () -&amp;gt; T,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion: @escaping (T) -&amp;gt; Void) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .userInitiated).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; result = work()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion(result)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// I/O任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;io&lt;/span&gt;&amp;lt;T&amp;gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; work: @escaping () &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; -&amp;gt; T,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion: @escaping (Result&amp;lt;T, Error&amp;gt;) -&amp;gt; Void) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .utility).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; result = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; work()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion(.success(result))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion(.failure(error))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 低优先级后台任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;background&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; work: @escaping () -&amp;gt; Void) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .background).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; work()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 使用示例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;AsyncTaskManager.compute({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 复杂计算&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.processLargeDataSet()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}) { result &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 主线程更新UI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.displayResult(result)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="swift-concurrency"&gt;Swift Concurrency&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 使用async/await&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DataProcessor&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;processAsync&lt;/span&gt;() async &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; -&amp;gt; ProcessedData {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 在后台执行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; await Task.detached(priority: .userInitiated) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.heavyProcessing()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }.value
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @MainActor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadAndDisplay&lt;/span&gt;() async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; await processAsync()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 自动在主线程更新UI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; updateUI(with: data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showError(error)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 使用TaskGroup并行处理&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;processImagesParallel&lt;/span&gt;(urls: [URL]) async -&amp;gt; [UIImage] {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; await withTaskGroup(of: UIImage?.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;) { group &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; url &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; urls {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; group.addTask {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; await &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.loadImage(from: url)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; images: [UIImage] = []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; await image &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; group {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; image = image {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; images.append(image)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; images
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="任务拆分与调度"&gt;任务拆分与调度&lt;/h2&gt;
&lt;h3 id="大任务拆分"&gt;大任务拆分&lt;/h3&gt;
&lt;p&gt;当必须在主线程执行大量工作时，可以拆分成小块：&lt;/p&gt;</description></item><item><title>卡顿-原理</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E5%8E%9F%E7%90%86/</link><pubDate>Sat, 02 May 2026 22:32:27 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E5%8E%9F%E7%90%86/</guid><description>&lt;p&gt;理解卡顿的原理是优化的基础。本文从屏幕显示的底层机制出发，沿着&amp;quot;VSync信号 -&amp;gt; 渲染管线 -&amp;gt; RunLoop协作 -&amp;gt; 掉帧产生&amp;quot;的逻辑链条，逐步揭示卡顿产生的根本原因。&lt;/p&gt;
&lt;h2 id="屏幕显示的基础vsync与缓冲机制"&gt;屏幕显示的基础：VSync与缓冲机制&lt;/h2&gt;
&lt;p&gt;要理解卡顿，首先要理解屏幕是如何显示画面的。&lt;/p&gt;
&lt;h3 id="vsync信号"&gt;VSync信号&lt;/h3&gt;
&lt;p&gt;VSync（Vertical Synchronization，垂直同步）是显示器发出的信号，用于协调GPU渲染和屏幕显示的时机。在60Hz的屏幕上，每16.67ms发出一次VSync信号。当信号到来时，屏幕从帧缓冲区读取数据进行显示。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;时间轴：
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│←── 16.67ms ──→│←── 16.67ms ──→│←── 16.67ms ──→│
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↑ ↑ ↑
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VSync 1 VSync 2 VSync 3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 显示帧1 显示帧2 显示帧3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="双缓冲与三缓冲"&gt;双缓冲与三缓冲&lt;/h3&gt;
&lt;p&gt;为了避免画面撕裂（GPU写入和屏幕读取同一块内存导致），iOS使用多缓冲机制：&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
subgraph 双缓冲机制
direction TB
GPU["GPU"] --&gt;|渲染到| BB["后缓冲区（Back Buffer）"]
BB &lt;--&gt;|VSync时交换| FB["前缓冲区（Frame Buffer）"]
FB --&gt;|读取显示| Display["显示器"]
end
&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;双缓冲（默认）&lt;/strong&gt;：GPU渲染到后缓冲区，VSync到来时交换前后缓冲区，显示器从前缓冲区读取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三缓冲（高负载时自动启用）&lt;/strong&gt;：增加第三个缓冲区，当渲染无法在一个VSync周期内完成时，GPU可以在额外缓冲区继续渲染，不必等待交换完成。代价是增加一帧延迟和内存占用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;系统在两者之间动态切换，开发者无需手动干预。&lt;/p&gt;
&lt;h3 id="promotion自适应刷新率"&gt;ProMotion自适应刷新率&lt;/h3&gt;
&lt;p&gt;ProMotion设备（iPhone 13 Pro及以上）支持10Hz-120Hz的自适应刷新率，这意味着帧时间预算不再固定：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;刷新率&lt;/th&gt;
&lt;th&gt;帧时间&lt;/th&gt;
&lt;th&gt;卡顿阈值建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;120Hz&lt;/td&gt;
&lt;td&gt;8.33ms&lt;/td&gt;
&lt;td&gt;&amp;gt;16ms视为掉帧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60Hz&lt;/td&gt;
&lt;td&gt;16.67ms&lt;/td&gt;
&lt;td&gt;&amp;gt;33ms视为掉帧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30Hz&lt;/td&gt;
&lt;td&gt;33.33ms&lt;/td&gt;
&lt;td&gt;&amp;gt;66ms视为掉帧&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 检查设备最大刷新率&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; maxFrameRate = UIScreen.main.maximumFramesPerSecond
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// CADisplayLink适配ProMotion&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#75715e"&gt;#available&lt;/span&gt;(&lt;span style="color:#75715e"&gt;iOS&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15.0&lt;/span&gt;, &lt;span style="color:#f92672"&gt;*&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; displayLink?.preferredFrameRateRange = CAFrameRateRange(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; minimum: &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; maximum: &lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; preferred: &lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="ios渲染架构"&gt;iOS渲染架构&lt;/h2&gt;
&lt;p&gt;了解了屏幕显示机制后，接下来看iOS是如何将UI元素最终变成屏幕上的像素的。&lt;/p&gt;</description></item><item><title>卡顿-图片优化</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E5%9B%BE%E7%89%87%E4%BC%98%E5%8C%96/</link><pubDate>Sat, 02 May 2026 22:32:27 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E5%9B%BE%E7%89%87%E4%BC%98%E5%8C%96/</guid><description>&lt;p&gt;图片处理是iOS应用中常见的性能瓶颈。本文介绍图片解码原理以及各种优化方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="图片解码原理"&gt;图片解码原理&lt;/h2&gt;
&lt;h3 id="为什么需要解码"&gt;为什么需要解码&lt;/h3&gt;
&lt;p&gt;图片文件（PNG、JPEG等）是压缩格式，无法直接显示。GPU需要的是位图（Bitmap）格式：&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
A[压缩图片&lt;br/&gt;PNG/JPEG] --&gt; B[解码&lt;br/&gt;Decode&lt;br/&gt;CPU密集操作]
B --&gt; C[位图&lt;br/&gt;Bitmap&lt;br/&gt;GPU可直接使用]
&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;位图大小 = 宽度 × 高度 × 每像素字节数
例如：1000×1000 RGBA图片 = 1000 × 1000 × 4 = 4MB&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="默认解码时机"&gt;默认解码时机&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 加载图片（此时未解码）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; image = UIImage(named: &lt;span style="color:#e6db74"&gt;&amp;#34;large_image&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 设置到ImageView（仍未解码）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;imageView.image = image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 在 CATransaction commit 的 prepare 阶段，Core Animation 会在主线程对未解码的图片执行解码&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 未提前解码的图片一定会在此阶段被解码，从而阻塞主线程引发卡顿&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="异步解码"&gt;异步解码&lt;/h2&gt;
&lt;h3 id="基本原理"&gt;基本原理&lt;/h3&gt;
&lt;p&gt;将解码工作移到后台线程：&lt;/p&gt;
&lt;pre class="mermaid"&gt;sequenceDiagram
participant M as 主线程
participant B as 后台线程
M-&gt;&gt;B: 请求加载图片
Note over M: 继续处理其他事件
B-&gt;&gt;B: 加载压缩数据
B-&gt;&gt;B: 解码为位图
B-&gt;&gt;B: 创建CGImage
B-&gt;&gt;M: 返回解码后的图片
M-&gt;&gt;M: 更新UI
&lt;/pre&gt;
&lt;h3 id="实现方案1强制解码"&gt;实现方案1：强制解码&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;extension&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UIImage&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/// 强制解码图片&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;decodedImage&lt;/span&gt;() -&amp;gt; UIImage? {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;guard&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; cgImage = &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.cgImage &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; width = cgImage.width
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; height = cgImage.height
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 创建位图上下文&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; colorSpace = CGColorSpaceCreateDeviceRGB()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue &lt;span style="color:#f92672"&gt;|&lt;/span&gt; CGBitmapInfo.byteOrder32Little.rawValue)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;guard&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; context = CGContext(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data: &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; width: width,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height: height,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitsPerComponent: &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bytesPerRow: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; space: colorSpace,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitmapInfo: bitmapInfo.rawValue
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ) &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 绘制到上下文（触发解码）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context.draw(cgImage, &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;: CGRect(x: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, y: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, width: width, height: height))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 从上下文创建新图片&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;guard&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decodedCGImage = context.makeImage() &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; UIImage(cgImage: decodedCGImage, scale: scale, orientation: imageOrientation)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 异步使用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loadImageAsync&lt;/span&gt;(named name: String, completion: @escaping (UIImage?) -&amp;gt; Void) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .userInitiated).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; image = UIImage(named: name)?.decodedImage()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion(image)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="实现方案2使用imageio"&gt;实现方案2：使用ImageIO&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ImageIO&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ImageDecoder&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;decodeImage&lt;/span&gt;(from url: URL) -&amp;gt; UIImage? {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;guard&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; source = CGImageSourceCreateWithURL(url &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; CFURL, &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; options: [CFString: Any] = [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kCGImageSourceShouldCache: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kCGImageSourceShouldCacheImmediately: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; &lt;span style="color:#75715e"&gt;// 立即解码&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;guard&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; cgImage = CGImageSourceCreateImageAtIndex(source, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, options &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; CFDictionary) &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; UIImage(cgImage: cgImage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;decodeImageAsync&lt;/span&gt;(from url: URL, completion: @escaping (UIImage?) -&amp;gt; Void) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.global(qos: .userInitiated).async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; image = ImageDecoder.decodeImage(from: url)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DispatchQueue.main.async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; completion(image)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="实现方案3使用uigraphicsimagerenderer"&gt;实现方案3：使用UIGraphicsImageRenderer&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;extension&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UIImage&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;decodedImageUsingRenderer&lt;/span&gt;() -&amp;gt; UIImage {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; format = UIGraphicsImageRendererFormat()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; format.scale = scale
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; format.opaque = &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; format.preferredRange = .standard
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; renderer = UIGraphicsImageRenderer(size: size, format: format)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; renderer.image { context &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; draw(at: .zero)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="图片降采样"&gt;图片降采样&lt;/h2&gt;
&lt;h3 id="为什么需要降采样"&gt;为什么需要降采样&lt;/h3&gt;
&lt;p&gt;当图片尺寸远大于显示尺寸时，加载原图是浪费：&lt;/p&gt;</description></item><item><title>卡顿-检测</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E6%A3%80%E6%B5%8B/</link><pubDate>Thu, 07 May 2026 16:54:12 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E6%A3%80%E6%B5%8B/</guid><description>&lt;p&gt;准确检测卡顿是优化的前提。本文介绍多种卡顿检测方案，从开发调试到线上监控都有对应的方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="检测方案概览"&gt;检测方案概览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;原理&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CADisplayLink&lt;/td&gt;
&lt;td&gt;监控帧节奏、FPS、慢帧/掉帧&lt;/td&gt;
&lt;td&gt;简单直接、适合趋势观察&lt;/td&gt;
&lt;td&gt;无法直接给出堆栈，FPS 只能辅助判断&lt;/td&gt;
&lt;td&gt;开发调试/线上辅助指标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RunLoop Observer&lt;/td&gt;
&lt;td&gt;监控RunLoop状态&lt;/td&gt;
&lt;td&gt;可获取堆栈&lt;/td&gt;
&lt;td&gt;有一定开销&lt;/td&gt;
&lt;td&gt;开发/线上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子线程Ping&lt;/td&gt;
&lt;td&gt;定时检测主线程响应&lt;/td&gt;
&lt;td&gt;实现简单&lt;/td&gt;
&lt;td&gt;精度有限&lt;/td&gt;
&lt;td&gt;线上监控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruments&lt;/td&gt;
&lt;td&gt;系统级分析&lt;/td&gt;
&lt;td&gt;信息详细&lt;/td&gt;
&lt;td&gt;只能开发时用&lt;/td&gt;
&lt;td&gt;性能分析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MetricKit&lt;/td&gt;
&lt;td&gt;系统数据收集&lt;/td&gt;
&lt;td&gt;无额外开销&lt;/td&gt;
&lt;td&gt;iOS 13+&lt;/td&gt;
&lt;td&gt;线上监控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry ANR V2&lt;/td&gt;
&lt;td&gt;帧延迟分析&lt;/td&gt;
&lt;td&gt;区分阻塞类型&lt;/td&gt;
&lt;td&gt;需集成SDK&lt;/td&gt;
&lt;td&gt;线上监控&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="1-cadisplaylink监控"&gt;1. CADisplayLink监控&lt;/h2&gt;
&lt;h3 id="基本原理"&gt;基本原理&lt;/h3&gt;
&lt;p&gt;CADisplayLink 是一个和屏幕显示刷新节奏同步的回调。它适合回答“当前 UI 刷新是否顺畅、是否出现慢帧、掉帧或冻结帧”，但 &lt;strong&gt;FPS 本身只是辅助参考&lt;/strong&gt;，不能单独作为卡顿结论。&lt;/p&gt;
&lt;p&gt;原因是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;FPS 只描述结果，不描述原因&lt;/strong&gt;：FPS 下降只能说明某段时间显示帧变少了，不能告诉你是主线程计算、布局、图片解码、锁等待、I/O、GPU 渲染还是系统主动降刷新率导致的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ProMotion 会动态调整刷新率&lt;/strong&gt;：在支持 120Hz 的机型上，系统可能根据内容和功耗策略把刷新率降到 10Hz、24Hz、30Hz、60Hz、80Hz、120Hz 等。静止页面低 FPS 可能只是系统主动省电，不是卡顿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主线程阻塞会让回调延迟&lt;/strong&gt;：如果主线程真的被阻塞，CADisplayLink 回调本身也会延后，此时需要结合实际回调间隔、RunLoop 状态和堆栈采样判断。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此线上卡顿监控更推荐记录 &lt;strong&gt;帧间隔 / 慢帧 / 冻结帧 / 交互场景&lt;/strong&gt;，而不是只显示一个“当前 FPS”。&lt;/p&gt;</description></item><item><title>卡顿-离屏渲染</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E7%A6%BB%E5%B1%8F%E6%B8%B2%E6%9F%93/</link><pubDate>Sat, 02 May 2026 22:32:27 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E7%A6%BB%E5%B1%8F%E6%B8%B2%E6%9F%93/</guid><description>&lt;p&gt;离屏渲染（Offscreen Rendering）是GPU渲染中的性能杀手。本文介绍离屏渲染的原理、触发条件以及优化方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="什么是离屏渲染"&gt;什么是离屏渲染&lt;/h2&gt;
&lt;h3 id="正常渲染流程"&gt;正常渲染流程&lt;/h3&gt;
&lt;p&gt;正常情况下，GPU直接将渲染结果写入帧缓冲区（Frame Buffer）：&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
subgraph 正常渲染["正常渲染 On-Screen Rendering"]
A[GPU&lt;br/&gt;渲染] --&gt; B[Frame Buffer&lt;br/&gt;帧缓冲区] --&gt; C[屏幕&lt;br/&gt;显示]
end
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#e8f5e9
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接渲染到帧缓冲区&lt;/li&gt;
&lt;li&gt;渲染完成即可显示&lt;/li&gt;
&lt;li&gt;效率最高&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="离屏渲染流程"&gt;离屏渲染流程&lt;/h3&gt;
&lt;p&gt;离屏渲染需要先渲染到离屏缓冲区，再合成到帧缓冲区：&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
subgraph 离屏渲染["离屏渲染 Offscreen Rendering"]
A[GPU&lt;br/&gt;渲染] --&gt; B[Offscreen Buffer&lt;br/&gt;离屏缓冲区]
B --&gt; C[Frame Buffer&lt;br/&gt;帧缓冲区]
C --&gt; D[屏幕&lt;br/&gt;显示]
end
E[额外的缓冲区] -.-&gt; B
style A fill:#e1f5fe
style B fill:#f48fb1
style C fill:#fff3e0
style D fill:#e8f5e9
style E fill:#f48fb1,stroke:#f44336,stroke-dasharray: 5 5
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;性能开销：&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>卡顿</title><link>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E5%8D%A1%E9%A1%BF/</link><pubDate>Sat, 02 May 2026 22:32:27 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/interview/ios-performance/stutter-%E5%8D%A1%E9%A1%BF/</guid><description>&lt;p&gt;卡顿是影响用户体验的关键问题之一。当应用无法在一帧的时间内（通常为16.67ms）完成渲染工作时，就会出现掉帧，用户会感知到界面不流畅。&lt;/p&gt;
&lt;p&gt;本系列文章从渲染原理入手，系统性地介绍iOS卡顿的检测与优化方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;卡顿监控是 APM 流畅性子系的核心能力，线上指标体系、检测采集、上报策略请参考 &lt;a href="https://amatsuzero.github.io/LastStand/posts/interview/ios-apm/apm/"&gt;APM 系列&lt;/a&gt;：&lt;a href="https://amatsuzero.github.io/LastStand/posts/interview/ios-apm/%E6%8C%87%E6%A0%87%E4%BD%93%E7%B3%BB/"&gt;指标体系&lt;/a&gt;、&lt;a href="https://amatsuzero.github.io/LastStand/posts/interview/ios-apm/%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86/"&gt;数据采集&lt;/a&gt;、&lt;a href="https://amatsuzero.github.io/LastStand/posts/interview/ios-apm/%E4%B8%9A%E7%95%8C%E6%96%B9%E6%A1%88/"&gt;业界方案（微信 Matrix / 字节 Slardar 卡死监控）&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="什么是卡顿"&gt;什么是卡顿&lt;/h2&gt;
&lt;p&gt;iOS设备的屏幕刷新率通常为60Hz（ProMotion设备可达120Hz），这意味着每秒需要渲染60帧画面。每一帧的渲染时间窗口约为16.67ms（1000ms / 60）。&lt;/p&gt;
&lt;p&gt;当CPU或GPU无法在这个时间窗口内完成工作时，就会发生掉帧（Frame Drop），用户会感知到卡顿。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;掉帧情况&lt;/th&gt;
&lt;th&gt;用户感知&lt;/th&gt;
&lt;th&gt;严重程度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;偶尔掉1-2帧&lt;/td&gt;
&lt;td&gt;几乎无感知&lt;/td&gt;
&lt;td&gt;轻微&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连续掉帧&lt;/td&gt;
&lt;td&gt;明显卡顿&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长时间阻塞（&amp;gt;250ms）&lt;/td&gt;
&lt;td&gt;界面冻结&lt;/td&gt;
&lt;td&gt;严重&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="渲染流程概述"&gt;渲染流程概述&lt;/h2&gt;
&lt;p&gt;iOS的渲染流程涉及CPU和GPU的协作，通过Core Animation Pipeline完成：&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart TB
subgraph APP["App 进程（CPU 阶段）"]
direction TB
Layout["Layout（布局计算）&lt;br/&gt;layoutSubviews、约束计算"]
Display["Display（绘制显示）&lt;br/&gt;drawRect:、文本绘制"]
Prepare["Prepare（准备）&lt;br/&gt;图片解码、格式转换"]
Commit["Commit（提交）&lt;br/&gt;打包图层树"]
Layout --&gt; Display --&gt; Prepare --&gt; Commit
end
subgraph RS["Render Server 进程（GPU 阶段）"]
direction TB
Decode["解码图层树"]
Vertex["顶点着色器"]
Raster["光栅化"]
Fragment["片段着色器"]
Buffer["帧缓冲区写入"]
Decode --&gt; Vertex --&gt; Raster --&gt; Fragment --&gt; Buffer
end
APP --&gt;|"IPC 传输"| RS
RS --&gt; VSync["VSync 信号到来"]
VSync --&gt; Screen["显示到屏幕"]
&lt;/pre&gt;
&lt;h3 id="cpu与gpu的职责"&gt;CPU与GPU的职责&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;主要工作&lt;/th&gt;
&lt;th&gt;常见瓶颈&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;布局计算、视图创建、文本计算、图片解码&lt;/td&gt;
&lt;td&gt;复杂布局、大量文本、主线程阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;纹理渲染、图层混合、离屏渲染&lt;/td&gt;
&lt;td&gt;离屏渲染、大量透明图层、超大图片&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="卡顿的常见原因"&gt;卡顿的常见原因&lt;/h2&gt;
&lt;h3 id="cpu瓶颈"&gt;CPU瓶颈&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主线程阻塞&lt;/strong&gt;：耗时操作在主线程执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂布局&lt;/strong&gt;：大量约束计算、频繁布局&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文本计算&lt;/strong&gt;：复杂富文本、大量文本渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片解码&lt;/strong&gt;：大图片在主线程解码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象创建&lt;/strong&gt;：大量对象的创建和销毁&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="gpu瓶颈"&gt;GPU瓶颈&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;离屏渲染&lt;/strong&gt;：圆角、阴影、遮罩触发离屏渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图层混合&lt;/strong&gt;：大量透明图层叠加&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超大图片&lt;/strong&gt;：纹理尺寸超过GPU限制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂特效&lt;/strong&gt;：模糊、滤镜等GPU密集操作&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="文章导航"&gt;文章导航&lt;/h2&gt;
&lt;p&gt;本系列包含以下文章，建议按顺序阅读：&lt;/p&gt;</description></item></channel></rss>