技术探索与实践实践总结

代码小镇
2025-06-20 02:02
阅读 396

技术探索与实践:在 iOS 开发中的一次性能优化实战


在我目前工作的这家互联网公司里,我们做的是一个用户量相当可观的 App,功能涵盖社交、内容展示、实时互动等多个模块。App 上线初期运行尚可,但随着业务不断扩展,尤其是 UI 层面日益复杂,我们开始接到越来越多关于“卡顿”、“页面加载慢”的用户反馈。

作为负责核心页面开发和性能优化的一员,我参与了一次比较系统的性能排查与改进工作。今天我就想分享一下这次经历,希望能给同样面临类似问题的朋友提供一点参考思路。

这篇文章不是那种泛泛而谈的“理论课”,也不是堆砌术语的“技术文档”,而是基于我们实际项目中的挑战、尝试、踩坑以及最终优化效果的一个完整复盘。希望能让你看到一些真实的工作场景和技术落地的过程。


项目背景:一次页面卡顿引发的性能反思

事情的起因是这样的:我们的某个主流程页面(比如首页信息流)出现了明显的掉帧现象,尤其是在低端设备上更为明显。用户滑动时会有短暂的卡顿感,甚至出现 UI 线程阻塞导致主线程不响应的情况。产品经理直接把这个问题标记为 P0 级别 Bug —— 因为这直接影响了用户体验,也影响了关键路径上的转化率。

我们当时的页面结构相对复杂,包含多个 CollectionView 嵌套、大量图片异步加载、复杂的布局计算、动态高度 Cell 以及部分自定义动画。虽然架构层面采用了 MVP 和组件化设计,但在性能层面确实没有很好地做过专项治理。

于是我和团队决定,对这个页面进行全面的性能诊断,并找出瓶颈点,进行针对性优化。


初期排查:用工具定位问题所在

我们第一步当然是用 Instruments 套件来分析 CPU、内存、Core Animation 的表现。

📊 使用 Time Profiler 找性能热点

通过 Time Profiler 我们发现,在滑动过程中 CPU 长时间被 layoutSubviews 占据,某些 Cell 在出屏重用时频繁调用 layoutSubviews,甚至还有重复调用的现象。

另外,还有一些子视图的初始化逻辑非常耗时,特别是在 Cell 被 dequeueReusableCell 复用的时候,每次都会重新 setup UI,没有做到懒加载或缓存。

🎯 Core Animation 分析渲染层

通过勾选 "Color Blended Layers" 和 "Color Offscreen-Rendered Yellow" 这两个选项,我们发现有些圆角设置触发了 offscreen render,拖慢了合成效率。还有一些模糊处理 Layer 设置方式不对,也是造成额外负担的原因之一。

此外,还存在不必要的透明度设置,例如将 UIView 的 alpha 设为 1.0,却仍然使用了 isOpaque = false,这些都会让 GPU 渲染更费力。


解决方案设计:从哪些方面入手?

根据以上发现,我们制定了以下几个方向的优化策略:

  1. UI 构建阶段优化(Cell 初始化、布局计算)
  2. 减少冗余布局刷新
  3. 图像资源管理优化
  4. 避免不必要的离屏渲染
  5. 异步预加载数据 + 缓存策略升级

下面我会逐个展开说明。


1. Cell 初始化重构:避免每次 dequeue 都创建 UI

之前我们的做法是:每次 dequeue Cell 后都走一遍完整的 addSubview 流程,这不仅耗时而且浪费资源,因为 Cell 是可复用的。

我们做了一个重构,将所有的子 view 都提前通过 lazy init 方式完成初始化:

class CustomTableViewCell: UITableViewCell {
    
    private lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
        label.textColor = .label
        return label
    }()
    
    private lazy var imageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFill
        iv.clipsToBounds = true
        iv.layer.cornerRadius = 8
        return iv
    }()
    
    // 只在 initContentView 中添加一次 subviews
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupContentView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(with item: FeedItem) {
        titleLabel.text = item.title
        imageView.kf.setImage(with: URL(string: item.imageUrl))
    }


![技术对比分析-2](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062002/3eb152a0-eb27-4cd8-8926-3c4f3cb71410.jpg)


    private func setupContentView() {
        contentView.addSubview(titleLabel)
        contentView.addSubview(imageView)

        // 设置 AutoLayout 约束
        ...
    }
}

这样一来,每次复用只需执行配置(configure),而不是每次都 addSubView、设置属性、重新 layout。CPU 时间明显下降。


2. 减少 layoutSubviews 调用频次

另一个问题是,我们在 Cell 内部用了比较多的自动布局代码,有时候会因为多次调用 setNeedsLayout() 导致 layoutSubviews 被反复触发。

为了避免这一点,我们采取了以下几点优化:

  • 所有约束都在 awakeFromNib 或者 viewDidLoad 中一次性布置好,不再动态更新
  • 对于需要异步调整 Frame 的操作,改用 layer.frame 替代 AutoLayout 更新(如在 collectionView(_:willDisplay:forItemAt:) 中)

此外,对于需要高度动态计算的 Cell,我们统一采用估算高度机制,利用 estimatedItemSize 来加速首次渲染:

collectionView.collectionViewLayout.invalidateLayout()
collectionView.reloadData()

改为:

collectionView.performBatchUpdates(nil, completion: nil)

这样能有效减少主线程 Block。


3. 图像资源与异步加载优化

我们原本使用的是 SDWebImage,但由于部分图片尺寸较大且没有压缩,导致加载过程中占用较多内存,有时还触发内存警告。

后来我们接入了 Kingfisher,它内置支持 WebP、Progressive JPEG、GIF 多图格式,并可以配合服务器端实现懒加载、CDN 加速等能力。

同时我们对大图进行了降采样处理:

imageView.kf.indicatorType = .activity
let options: [KingfisherOptionsInfoItem] = [
    .scaleFactor(UIScreen.main.scale),
    .transition(.fade(0.2)),
    .cacheOriginalImage,
    .imageProcessor(DownsamplingImageProcessor(size: imageView.bounds.size))
]
imageView.kf.setImage(with: url, options: options)

此外,我们也实现了本地缓存+内存缓存分级机制,减少重复请求和 IO 操作。


4. 避免不必要的离屏渲染(Off-screen Rendering)

iOS 中常见的几种会触发 off-screen rendering 的场景包括:

  • 圆角设置(cornerRadius + maskToBounds)
  • 阴影(shadowPath 不设置)
  • 渐变遮罩、滤镜等
  • 使用 shouldRasterize

我们检查后发现有几个地方确实是这么干的,比如在某些 Card 样式的 Cell 上设置了 cornerRadius 并加上了 maskToBounds:

view.layer.cornerRadius = 10
view.layer.masksToBounds = true

这种组合会强制开启 off-screen rendering。对此,我们做了如下调整:

  • 优先使用带圆角的图片裁剪方式(前端切图),而非 layer 属性
  • 如果必须做圆角,建议配合 shouldRasterize = true + rasterizationScale = screen scale
  • 所有阴影均指定 shadowPath,避免系统自动绘制路径

示例优化后的代码如下:

cellContainerView.layer.cornerRadius = 10
cellContainerView.layer.shadowColor = UIColor.black.cgColor
cellContainerView.layer.shadowOpacity = 0.2
cellContainerView.layer.shadowOffset = CGSize(width: 0, height: 2)
cellContainerView.layer.shadowRadius = 4


![技术概念图解-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062002/c31092e7-afe3-4ff1-9e32-b1f9dc97c7a0.jpg)


// 必须手动设置 path,否则会自动触发 offscreen rendering
cellContainerView.layer.shadowPath = UIBezierPath(roundedRect: cellContainerView.bounds, cornerRadius: 10).cgPath

cellContainerView.layer.shouldRasterize = true
cellContainerView.layer.rasterizationScale = UIScreen.main.scale

这样既能保留视觉效果,又能极大降低 GPU 绘制成本。


5. 异步预加载 + 数据模型缓存

除了 UI 性能外,我们还在数据准备环节做了优化。例如:当用户滑动快要到某一页时,就预先下载下一批数据并解析成 model,这样用户真正到达该页时几乎零延迟加载。

我们通过结合 RxCocoa 的 observeOn 机制,将数据加载移到后台线程,并用 LRU 缓存 Model 结构体:

class FeedViewModel {
    private let feedCache = NSCache<NSString, FeedItemModel>()
    
    func loadFeedItems(at page: Int, completion: @escaping ([FeedItemModel]) -> Void) {
        DispatchQueue.global().async {
            let cacheKey = "feed_page_$page)"
            
            if let cached = self.feedCache.object(forKey: cacheKey as NSString) {
                DispatchQueue.main.async { completion([cached]) }
                return
            }
            
            // 从网络获取数据
            let newItems = API.load(page: page)
            for item in newItems {
                self.feedCache.setObject(item, forKey: item.id as NSString)
            }
            
            DispatchQueue.main.async { completion(newItems) }
        }
    }
}

此外,我们将一些常用的布局参数(如 cell 高度)缓存下来,防止重复计算。


实际效果对比:优化前 vs 优化后

指标 优化前平均值 优化后平均值
FPS 滑动帧率 42 fps 58 fps
内存占用峰值 ~1.1GB ~780MB
首屏加载时间 ~2.3s ~1.1s
页面卡顿反馈 明显上升趋势 用户基本无反馈

尤其在低端设备(如 iPhone SE 第二代)上,FPS 由原来的 35 左右提升到了接近 50,滑动体验流畅了很多。

产品部门也非常满意,认为这次优化大大提升了整体产品质感和用户体验。


踩过的那些坑

当然,在这个过程中我们也遇到不少问题,记录几个印象比较深的例子:

⚠️ Cell 高度动态计算导致主线程堵塞

一开始我们为了精准控制 Cell 高度,每个 Cell 都是通过计算出来的 frame 来设置 contentSize,但这会导致大量同步 layout 操作堆积在主线程。

解决方式:我们改用 UITableViewAutomaticDimension + estimatedRowHeight,或者 UICollectionViewFlowLayout 的 estimatedItemSize 来实现动态高度,从而减少主线程压力。

🧨 多线程访问 NSCache 引发崩溃

我们一开始用了普通的 Dictionary 缓存 Model,结果在并发请求多个 page 的时候偶尔出现线程冲突 Crash。

解决方式:换成了 NSCache,它是线程安全的,且自带内存清理机制,非常适合做 model 缓存。

📉 Kingfisher 下载进度回调未正确更新 UI

有一段时间我们在 ImageView 上显示下载进度环,但用 KF 的 downloadProgress 方法没有成功绑定到主线程,导致 UI 更新失败。

解决方式:使用 .observeOn(MainScheduler.instance) 将回调切换回主线程,确保 UI 安全更新。


我的几点经验总结

回顾整个过程,我总结了几点值得思考的经验:

  1. 性能优化不能等到上线后再做
    应该在开发早期就把性能指标纳入测试范畴,比如加入 FPS 监控、内存统计,提前发现问题。

  2. 合理使用调试工具比盲打更高效
    Instruments + Xcode Debug Memory Graph 工具,能帮你快速锁定性能瓶颈。

  3. 不要盲目复制“最佳实践”
    每个项目结构不同,技术栈也不同,要结合自身项目特点灵活应用。

  4. 持续监控很重要
    优化完成后不代表结束,我们通过 Firebase Performance Monitoring 实时追踪页面加载时间和 FPS,方便后续进一步迭代。


最后的一些话

这次优化让我深刻体会到,一个好的开发者,不仅要写得出功能,更要能写出高效的代码。

很多时候所谓的“卡顿”,其实并不是代码写的错,而是在细节上的考虑不够周全,比如布局是否复用、资源是否压缩、线程是否隔离等等。这些小点累积起来,就会形成大的差异。

如果你也在做类似的项目,希望我的经验能给你一些启发。也欢迎你在评论区交流你们的优化实战故事 —— 每个工程师的成长路上,都有那么几次“抠细节”的经历吧 😂


作者简介
本人目前任职于某知名互联网公司,担任 iOS 客户端研发,主要负责核心产品线开发及性能优化工作。热爱写代码,也喜欢研究技术背后的原理与实践。欢迎关注我在 GitHub 或 微信公众号 获取更多移动端技术干货。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝