Skip to content

01 - View 基础与布局引擎 ​

目录 ​

  1. UIView 底层原理
  2. 图层系统(Core Animation)
  3. 视图层次与渲染管线
  4. 布局引擎详解
  5. AutoLayout 机制
  6. 约束优先级详解
  7. StackView 原理
  8. SnapticK 声明式布局
  9. 布局性能分析
  10. 面试考点汇总

1. UIView 底层原理 ​

1.1 UIView 的内存布局 ​

UIView 对象内存布局(64-bit):
┌─────────────────────────────────────────────────────┐
│ NSObject isa 指针(指向 UIView 的 Class 对象)       │
├─────────────────────────────────────────────────────┤
│ UIView 私有关联对象(weak reference list 等)         │
├─────────────────────────────────────────────────────┤
│ CALayer *layer(强引用,UIView 的 layer 代理)       │
│ view.layer == layer 为 true(关联对象代理模式)       │
├─────────────────────────────────────────────────────┤
│ CGRect _frame(布局框架,等价于 layer.frame 但不直接 │
│ 关联,UIView 通过 setNeedsLayout 触发布局更新)       │
├─────────────────────────────────────────────────────┤
│ CGRect _bounds(局部坐标系)                         │
├─────────────────────────────────────────────────────┤
│ CGPoint _center(中心点)                            │
├─────────────────────────────────────────────────────┤
│ CGFloat _transform(仿射变换矩阵)                   │
├─────────────────────────────────────────────────────┤
│ CGFloat _alpha, _opacity, _transform、_contentScale │
├─────────────────────────────────────────────────────┤
│ NSString *_restorationIdentifier                    │
├─────────────────────────────────────────────────────┤
│ UIView 私有 ivars(系统内部管理,不暴露)            │
└─────────────────────────────────────────────────────┘

关键结论:
- UIView 只是 CALayer 的代理层,本身不存储像素数据
- UIView 的渲染完全委托给 layer(CALayer)
- UIView.frame 与 layer.frame 通过关联属性同步

1.2 UIView vs CALayer 的本质区别 ​

┌──────────────┬──────────────────┬─────────────────────┐
│    维度       │     UIView       │    CALayer          │
├──────────────┼──────────────────┼─────────────────────┤
│ 继承关系     │ UIResponder      │ NSObject            │
│ 核心职责     │ 事件响应+渲染代理 │ 像素渲染            │
│ 像素存储     │ ❌ 不存储         │ ✅  backingStore    │
│ 事件处理     │ ✅                │ ❌                  │
│ 层级管理     │ ✅ 通过 superview │ ✅ 通过 superlayer  │
│ 动画支持     │ ✅ 隐式动画代理   │ ✅ 隐式动画          │
│ 创建方式     │ [[UIView alloc]  │ [[CALayer alloc]    │
│              │ initWithFrame:]  │                     │
├──────────────┼──────────────────┼─────────────────────┤
│ 性能         │ 有 responder 链开销 │ 纯渲染,更轻量     │
│ 适用场景     │ UI 组件           │ 自定义渲染/粒子      │
└──────────────┴──────────────────┴─────────────────────┘

1.3 UIView 的生命周期方法 ​

swift
// UIView 生命周期完整流程
class MyView: UIView {
    
    // 1. 初始化(代码创建时调用)
    override init(frame: CGRect) {
        super.init(frame: frame)
        print("1. init(frame:) - 内存分配完成")
        // ✅ 适合:设置基础属性、初始化数据
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        print("1. init(coder:) - XIB/Storyboard 加载")
    }
    
    // 2. 布局前 - 第一次
    override func awakeFromNib() {
        super.awakeFromNib()
        print("2. awakeFromNib - XIB/Storyboard 加载后")
        // ✅ 适合:XIB 组件的初始化(只在 XIB 时调用)
    }
    
    // 3. 布局前 - 尺寸已确定
    override func prepareForReuse() {
        super.prepareForReuse()
        print("3. prepareForReuse - Cell 复用准备")
        // ✅ 适合:TableViewCell/CollectionViewCell 的复用清理
    }
    
    // 4. 添加超视图后
    override func didAddSubview(_ subview: UIView) {
        super.didAddSubview(subview)
        print("4. didAddSubview - 子视图已添加")
    }
    
    // 5. 布局前 - 可修改 frame
    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: newSuperview)
        print("5. willMove - 即将移动")
    }
    
    // 6. 布局前 - 可修改 frame
    override func layoutSubviews() {
        super.layoutSubviews()
        print("6. layoutSubviews - 布局完成(可重写)")
        // ✅ 适合:调整子视图的 frame
        // 注意:可能被多次调用,需做 diff
    }
    
    // 7. 首次布局完成
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        print("7. draw - 绘制(CoreGraphics)")
        // ⚠️ 不推荐重写,使用 CoreGraphics 绘制开销大
        // 现代方案:CAShapeLayer + UIBezierPath
    }
    
    // 8. 布局完成
    override func layoutIfNeeded() {
        super.layoutIfNeeded()
        print("8. layoutIfNeeded - 布局完成(触发)")
    }
    
    // 9. 设置动画约束
    override func updateConstraints() {
        super.updateConstraints()
        print("9. updateConstraints - 约束更新")
        // ✅ 适合:修改约束,但不常用
    }
}

2. 图层系统(Core Animation) ​

2.1 CALayer 架构 ​

Core Animation 图层树:
┌────────────────────────────────────────────────────┐
│                    CALayer (根图层)                 │
│                  (window的layer)                  │
├────────────────────────────────────────────────────┤
│           superlayer(父图层)                      │
│           一个图层有且只有一个 superlayer            │
├────────────────────────────────────────────────────┤
│           sublayers(子图层数组)                    │
│           可嵌套任意层,形成图层树                   │
├────────────────────────────────────────────────────┤
│           渲染相关属性:                           │
│           • backgroundColor                      │
│           • borderWidth / borderColor            │
│           • cornerRadius / masksToBounds          │
│           • shadow* / shadowPath                │
│           • contents(CGImage)                  │
│           • contentsScale(@2x/@3x 适配)         │
├────────────────────────────────────────────────────┤
│           变换相关属性:                           │
│           • transform(CATransform3D)           │
│           • sublayerTransform                      │
│           • anchorPoint / position / bounds       │
└────────────────────────────────────────────────────┘

CALayer 渲染流程:
┌───────────┐    ┌─────────────┐    ┌───────────────┐    ┌──────────────┐
│ CALayer   │───▶│ CATransaction│──▶│ 提交到CADisplay │──▶│ CADisplayLink │
│ (创建/更新)│   │ (事务管理)   │    │ (60fps/120fps) │    │ (帧同步)     │
└───────────┘    └─────────────┘    └───────────────┘    └──────────────┘
     │                  │                    │                   │
     ▼                  ▼                    ▼                   ▼
  属性设置          批处理/事务           渲染指令            vsync 信号

2.2 隐式动画机制 ​

swift
// Core Animation 的默认隐式动画
@objc class MyLayer: CALayer {
    override init() {
        super.init()
        
        // 触发隐式动画的关键:
        // 修改可动画的属性 → 自动添加默认 CATransition
        
        // 可动画的属性:
        // ✅ bounds / position / frame(frame 会触发 bounds+position)
        // ✅ opacity / alpha
        // ✅ backgroundColor / borderColor
        // ✅ cornerRadius
        // ✅ shadow*
        // ✅ transform
        // ✅ contents
        
        // 不可动画的属性:
        // ❌ sublayers(需要手动添加/移除动画)
        // ❌ masksToBounds
        // ❌ borderWidth
        // ❌ 需要设置 actionForKey 禁用
        
        print("默认动画时长:\(self.action(forKey: "bounds")?.duration ?? "N/A")")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 重写 action(forKey:) 控制动画行为
    override func action(forKey event: String) -> CAAction? {
        switch event {
        case "backgroundColor":
            // 自定义淡入动画
            let anim = CABasicAnimation(keyPath: "backgroundColor")
            anim.duration = 0.3
            anim.fromValue = UIColor.white.cgColor
            anim.toValue = UIColor.red.cgColor
            return anim
        case "bounds":
            // 禁用 bounds 动画
            return nil
        default:
            // 使用默认行为
            return super.action(forKey: event)
        }
    }
}

// 动画类型对比:
// CABasicAnimation:单一属性变化(位置/大小/颜色)
// CAKeyframeAnimation:关键帧路径动画
// CATransition:过渡动画(fade/slide/rotate/ripple 等)
// CAAnimationGroup:多动画组合

2.3 图层类型速查 ​

swift
// 常用 CALayer 子类:
//
// CAScrollLayer    — 滚动支持
// CATiledLayer     — 平铺渲染(大图片/地图)
// CALayer          — 基础层(自定义渲染)
// CAGradientLayer  — 渐变背景(性能比 UIImageView 好)
// CAShapeLayer     — 矢量形状(贝塞尔路径)
// CAGlassLayer     — iOS 15+ 毛玻璃效果
// CAEmitterLayer   — 粒子系统
// CAReplicatorLayer — 复制图层(涟漪效果/频谱)
// CAEAGLLayer      — OpenGL ES 渲染
// CVOpenGLESTexture — 视频帧渲染
//
// 使用建议:
// - 渐变:用 CAGradientLayer(GPU 渲染,比 CG 快 10x+)
// - 描边/圆角:用 CAShapeLayer(避免离屏渲染)
// - 动画:用 Core Animation(GPU 合成,不阻塞主线程)
// - 自定义图形:用 UIGraphicsImageRenderer(离屏渲染)

3. 视图层次与渲染管线 ​

3.1 渲染管线全链路 ​

iOS 渲染管线(完整流程):
┌──────────────────────────────────────────────────────────────┐
│ 第 1 阶段:布局(Layout Phase)                              │
│                                                              │
│  1. setNeedsLayout() 标记需要布局                            │
│  2. 等待下一个 runloop 的 update 事件                         │
│  3. 调用 layoutIfNeeded() / layoutSubviews()                  │
│  4. 递归调用 subviews.layoutSubviews()                        │
│  5. 计算每个 view 的 frame/bounds(AutoLayout 求解)           │
│  6. 将 frame 同步到 layer.frame                               │
│                                                              │
│  触发方式:                                                   │
│  • setNeedsLayout() — 标记,延迟到下一次 runloop               │
│  • layoutIfNeeded() — 强制立即执行                            │
│  • UIView.animate() — 隐式触发                               │
│                                                              │
│  耗时:通常 16ms 以内(60fps),超时会掉帧                     │
├──────────────────────────────────────────────────────────────┤
│  第 2 阶段:绘制(Draw Phase)                               │
│                                                              │
│  1. drawHierarchy(in:) / draw(_:)                           │
│  2. 递归调用 subviews 的 draw                                 │
│  3. 每个 view 的 layer 的 draw                                │
│  4. CGImage 绘制到 backingStore(位图)                       │
│                                                              │
│  触发方式:                                                   │
│  • 需要 content 为 nil(自动绘制)                            │
│  • 手动重写 draw(_:) 方法(不推荐)                           │
│  • UIGraphicsBeginImageContext()                            │
│                                                              │
│  ⚠️ 注意:draw 阶段在主线程执行,非常耗时!                    │
│  ✅ 替代方案:使用 CAShapeLayer / 预渲染图片                  │
├──────────────────────────────────────────────────────────────┤
│  第 3 阶段:提交(Commit Phase)                             │
│                                                              │
│  1. 将所有 layer 合成指令提交到 CADisplayLink                  │
│  2. CATransaction.commit() — 批量提交                        │
│  3. 等待 VSync 信号(60fps/120fps)                          │
│  4. GPU 执行合成指令(离屏渲染/混合/裁切)                     │
│  5. 输出到屏幕                                                │
│                                                              │
│  💡 关键点:                                                  │
│  • 合成阶段在 GPU 执行,不阻塞主线程                          │
│  • 离屏渲染会在 CPU 创建临时缓冲区,性能下降 5-10x             │
└──────────────────────────────────────────────────────────────┘

完整时序图:
Frame 1 (0ms)     Frame 2 (16.67ms)   Frame 3 (33.33ms)
┌────────┐        ┌────────┐           ┌────────┐
│布局      │──▶    │布局      │──▶       │布局      │
│绘制      │──▶    │绘制      │──▶       │绘制      │
│提交      │──▶    │提交      │──▶       │提交      │
└────────┘        └────────┘           └────────┘
                     ▲                      ▲
                  VSync(GPU 合成)       VSync(GPU 合成)

3.2 离屏渲染(Off-Screen Rendering)深度分析 ​

swift
// ⚠️ 离屏渲染 = 性能杀手
// 原理:GPU 无法在当前渲染帧处理,需要创建新的缓冲区

// 触发离屏渲染的情况:
// 1. layer.masksToBounds = true / clipsToBounds = true
// 2. layer.cornerRadius > 0(且未使用 CAShapeLayer)
// 3. layer.shadow* 属性(阴影)
// 4. layer.shouldRasterize = true
// 5. CGPath 复杂路径(CAShapeLayer)
// 6. 模糊效果 / 混合模式

// 检测离屏渲染:
// 1. Instruments → Core Animation → Color Off-screen Rendered
// 2. 红色 = 离屏渲染,绿色 = 正常

// 优化方案对比表:
问题原因传统方案现代方案性能提升
圆角masksToBounds=true用 CAShapeLayer 替代使用 layer.mask + UIBezierPath5-10x
阴影shadowPath 未设置预渲染 shadowImage设置 shadowPath 为 CGPath3-5x
圆角+阴影两者同时触发分别优化用 CAShapeLayer + shadowPath5-8x
裁剪圆角clipsToBounds使用 layer.mask使用 UIGraphicsImageRenderer 预渲染3-5x
swift
// ✅ 最佳实践:圆角优化
extension UIView {
    // 方法 1:使用 CAShapeLayer(推荐 ✅✅)
    func setCornerRadius(_ radius: CGFloat) {
        layer.cornerRadius = radius
        layer.mask = CAShapeLayer()
        let maskPath = UIBezierPath(roundedRect: bounds, cornerRadius: radius)
        (layer.mask as? CAShapeLayer)?.path = maskPath.path
    }
    
    // 方法 2:使用 UIBezierPath(适合动态圆角)
    func setCornerRadiusWithMask(_ radius: CGFloat) {
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: radius)
        let maskLayer = CAShapeLayer()
        maskLayer.path = path.cgPath
        layer.mask = maskLayer
    }
    
    // 方法 3:预渲染为图片(适合固定圆角 + 复杂背景)
    func preRenderAsImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { ctx in
            drawHierarchy(in: bounds, afterScreenUpdates: true)
        }
    }
}

// ✅ 最佳实践:阴影优化
extension UIView {
    func setShadow(offset: CGSize, color: UIColor, opacity: Float, radius: CGFloat) {
        layer.shadowOffset = offset
        layer.shadowColor = color.cgColor
        layer.shadowOpacity = opacity
        layer.shadowRadius = radius
        // ⚠️ 关键:设置 shadowPath,避免离屏渲染!
        let path = UIBezierPath(rect: bounds.insetBy(dx: 0, dy: 0))
        layer.shadowPath = path.cgPath
    }
}

4. 布局引擎详解 ​

4.1 布局系统架构 ​

┌───────────────────────────────────────────────────────────────┐
│                    iOS 布局引擎架构                             │
├───────────────┐   ┌──────────────┐   ┌────────────────────┐   │
│   UIView      │   │ NSLayoutConstraint │   │ NSLayoutConstraint │   │
│   (框架)      │──▶│   约束求解器    │──▶│   AutoLayout引擎  │   │
│   _frame      │   │ (Engine)       │   │ (布局传递)        │   │
│   _bounds     │   │                │   │                    │   │
│   _center     │   │ 输入:           │   │ 输出:              │   │
│               │   │ • NSLayoutConstraint 对象      │   │ • 最终 frame 值         │   │
│               │   │ • NSLayoutConstraint 优先级      │   │ • 布局完成通知        │   │
│               │   │ • NSLayoutConstraint 状态      │   │                    │   │
└───────────────┘   └──────────────────────────────┘   └────────────────────┘
         │                      │                            │
         ▼                      ▼                            ▼
   视图层级           约束求解器              布局传递(自上而下→自下而上)
   UIView            NSLayoutConstraint  Engine            layoutSubviews
                      Engine              Engine

4.2 AutoLayout 核心组件 ​

swift
// NSLayoutConstraint 是 AutoLayout 的核心类
// 它定义了两个视图之间的线性关系:

// NSLayoutConstraint 的构成要素:
/*
    NSLayoutConstraint(
        item: viewA,           // 被约束的视图
        attribute: .top,       // 约束属性(top/left/width等)
        relatedBy: .equal,     // 关系(>= <= ==)
        toItem: viewB,         // 目标视图(nil = 超级视图/参考系)
        attribute: .topMargin, // 目标属性
        multiplier: 1.0,       // 乘数系数
        constant: 20.0         // 偏移常量
    )
    
    // 简化理解:viewA.top = viewB.top * multiplier + constant
    // 例:viewA.top = viewB.top * 1.0 + 20.0 → viewA 顶部在 viewB 顶部下方 20pt
*/

// NSLayoutConstraint 的状态机:
//   NSLayoutPriorityHigh  = 1000(必需)
//   NSLayoutPriorityWindowSizeStayable = 999(系统窗口大小保持)
//   NSLayoutPriorityFittingSizeLayout = 50(压缩优先级)
//   NSLayoutPriorityDefaultLow = 251(默认低优先级)
//   NSLayoutPriorityRequired = 1000(必须满足)
//   NSLayoutPriorityDefaultHigh = 750(默认高优先级)

// NSLayoutConstraint 的求解过程:
// 1. 收集所有约束 → NSLayoutConstraint 对象数组
// 2. 构建约束图(Constraint Graph):
//    每个约束是有向边,视图是节点
// 3. 求解线性方程组(使用高斯消元法等)
// 4. 处理优先级(优先级低的约束在冲突时让步)
// 5. 输出每个视图的最终 frame
// 6. 通过 setNeedsLayout → layoutSubviews 应用到 UIView

4.3 布局传递流程 ​

AutoLayout 布局传递(双遍算法):

第一遍:自上而下(Forward Pass)— 确定约束
┌───────────────────────────────────┐
│  UIWindow                            │
│  ┌───────────────────────────────┐ │
│  │  ViewController.view              │ │
│  │  ┌────────────────────────────┐ │ │
│  │  │  NavigationBar              │ │ │
│  │  │  ┌────────────────────────┐ │ │ │
│  │  │  │  Content View             │ │ │ │
│  │  │  │  ┌───────────────────┐ │ │ │ │
│  │  │  │  │  TableView          │ │ │ │ │
│  │  │  │  │  ┌───────────────┐ │ │ │ │ │
│  │  │  │  │  │  Cell[0]        │ │ │ │ │
│  │  │  │  │  │  Cell[1]        │ │ │ │ │
│  │  │  │  │  └─────────────────┘ │ │ │ │
│  │  │  │  └──────────────────────┘ │ │ │
│  │  └──────────────────────────────┘ │ │
│  └──────────────────────────────────┘ │
└────────────────────────────────────────┘

第二遍:自下而上(Backward Pass)— 确定尺寸
┌───────────────────────────────────┐
│  Cell[0](返回 intrinsicContentSize)  │
│  Cell[1](返回 intrinsicContentSize)  │
│  TableView(根据 cells 累加高度)       │
│  Content View(根据 TableView 布局)   │
│  ViewController.view                  │
│  Window(根据 view 布局)             │
└────────────────────────────────────────┘

最终结果:每个视图都有了确定的 frame

5. AutoLayout 机制 ​

5.1 创建约束的 4 种方式 ​

swift
// 方式 1:NSLayoutConstraint(代码创建)
// 精确控制,但代码冗长
let constraint = NSLayoutConstraint(
    item: viewA,
    attribute: .top,
    relatedBy: .equal,
    toItem: viewB,
    attribute: .top,
    multiplier: 1.0,
    constant: 20.0
)
view.addConstraint(constraint)

// 方式 2:NSLayoutConstraint.activate()(批量激活)
let constraints = [
    viewA.topAnchor.constraint(equalTo: viewB.topAnchor, constant: 20),
    viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor, constant: 10),
    viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor, constant: -10),
    viewA.heightAnchor.constraint(equalTo: viewB.heightAnchor, multiplier: 0.5)
]
NSLayoutConstraint.activate(constraints)

// 方式 3:VFL(Visual Format Language)
// 简洁但可读性差,已不推荐
let views = ["viewA": viewA, "viewB": viewB]
let constraints = NSLayoutConstraint.constraints(
    withVisualFormat: "V:|-[viewA]-[viewB]-|",
    options: [],
    metrics: nil,
    views: views
)

// 方式 4:NSLayoutAnchor(现代推荐 ✅)
// 类型安全,链式调用,自动推断约束关系
viewA.topAnchor.constraint(equalTo: viewB.topAnchor, constant: 20).isActive = true
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor, constant: 10).isActive = true
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor, constant: -10).isActive = true
viewA.heightAnchor.constraint(equalTo: viewB.heightAnchor, multiplier: 0.5).isActive = true

// 方式 5:UIStackView(声明式)
let stack = UIStackView(arrangedSubviews: [viewA, viewB])
stack.axis = .vertical
stack.spacing = 10
stack.distribution = .fillEqually

5.2 约束优先级系统 ​

swift
// 约束优先级详解(按重要性排序)

/*
┌─────────────────────────────────────────────┐
│                    优先级层级                    │
├──────────────────────────────────────────────┤
│  1000 (Required)         — 必须满足             │
│     • 内容压缩/内容伸展优先级                   │
│     • 系统默认约束                             │
├──────────────────────────────────────────────┤
│   750 (High)              — 默认高优先级         │
│     • UIView 默认约束                           │
│     • NSLayoutConstraint.defaultHigh           │
├──────────────────────────────────────────────┤
│   500 (Medium)            — 中间优先级           │
│     • 手动设置的中等优先级                       │
├──────────────────────────────────────────────┤
│   251 (Low)               — 默认低优先级         │
│     • NSLayoutConstraint.defaultLow            │
│     • 内容压缩优先级                           │
├──────────────────────────────────────────────┤
│    1 (Lowest)             — 最低优先级           │
│     • 自定义最低优先级                          │
└──────────────────────────────────────────────┘

约束冲突时的处理规则:
1. 高优先级约束优先满足
2. 相同优先级:先添加的约束优先
3. 相同优先级+同时添加:系统随机选择一个约束作为"winner"
4. 冲突必须解决:否则触发断言崩溃(fatal error)
*/

// 优先级应用场景:

// 场景 1:可变间距(低优先级间距约束)
leadingConstraint.priority = .defaultLow  // 允许压缩
// 这样当空间不足时,间距会先被压缩而不是裁剪内容

// 场景 2:等高(低优先级高度约束)
heightConstraint.priority = .defaultLow  // 允许压缩
// 当内容高度可变时,高度约束不强制等高等

// 场景 3:优先填充(高优先级拉伸约束)
widthConstraint.priority = .defaultHigh  // 优先拉伸
// 优先满足宽度,再处理高度

6. StackView 原理 ​

6.1 UIStackView 内部机制 ​

swift
/*
UIStackView 的工作原理:

┌───────────────────────────────────────────┐
│               UIStackView                   │
│                                           │
│  arrangedSubviews = [                    │
│    view0(intrinsic width: 100)          │
│    view1(intrinsic width: 150)          │
│    view2(intrinsic width: 200)          │
│  ]                                        │
│                                           │
│  axis = .horizontal → 水平排列             │
│  spacing = 10(子视图之间的间距)            │
│  distribution = .fill → 填满剩余空间         │
│  alignment = .fill → 对齐方式               │
│  layoutMargins = UIEdgeInsets                │
│  axisLayoutMargins(垂直方向间距)           │
│                                           │
│  计算过程:                                  │
│  1. 收集所有 arrangedSubviews 的尺寸         │
│  2. 根据 axis 计算主轴/交叉轴尺寸            │
│  3. 根据 distribution 分配剩余空间           │
│  4. 根据 spacing 设置子视图间距               │
│  5. 生成 NSLayoutConstraint 并添加            │
│  6. 触发布局传递(layoutSubviews)           │
└───────────────────────────────────────────┘

distribution 模式对比:
┌───────┬──────────────────┬───────────────────────────┐
│ 模式   │  描述             │  适用场景                   │
├───────┼──────────────────┼───────────────────────────┤
│ fill   │ 填满剩余空间       │ 等分布局(最常见)           │
│ equalSpacing │ 等间距      │ 子视图尺寸固定               │
│ fillProportionally │ 按尺寸比例│ 内容不等宽度的列表           │
│ equalCentering │ 等中心间距  │ 中间留白场景                 │
└───────┴──────────────────┴───────────────────────────┘
*/

// StackView 的高级用法:
extension UIStackView {
    
    // 动态添加子视图(自动创建约束)
    func addArrangedSubview(_ view: UIView) {
        super.addArrangedSubview(view)
        // 自动创建约束:
        // view.leadingAnchor == self.layoutMarginsGuide.leadingAnchor
        // view.trailingAnchor == self.layoutMarginsGuide.trailingAnchor
        // view.topAnchor == self.arrangedSubviews.last?.bottomAnchor + spacing
    }
    
    // 移除子视图(自动移除约束)
    func removeArrangedSubview(_ view: UIView) {
        super.removeArrangedSubview(view)
        // 注意:不会 removeFromSuperview,只是移除约束
        view.removeFromSuperview()  // 需要手动调用
    }
    
    // 插入子视图到指定位置
    func insertArrangedSubview(_ view: UIView, at stackIndex: Int) {
        super.insertArrangedSubview(view, at: stackIndex)
    }
}

6.2 StackView 常见坑 ​

swift
// ❌ 坑 1:StackView 内的子视图约束优先级问题
// StackView 内部会自动管理子视图的约束
// 不要在 StackView 子视图上设置 leading/trailing 约束
// StackView 会覆盖你设置的约束!

// 正确做法:只在子视图上设置 width/height 约束
button.widthAnchor.constraint(equalToConstant: 100).isActive = true  // ✅
button.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true  // ❌ StackView 会管理

// ❌ 坑 2:contentCompressionResistancePriority 冲突
// StackView 中子视图的压缩优先级会冲突
// 解决:设置合理的 compression resistance 优先级
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

// ❌ 坑 3:StackView 不处理 intrinsicContentSize 的动态变化
// 当子视图内容变化导致尺寸变化时,StackView 不会自动更新
// 解决:调用 setNeedsLayout() + layoutIfNeeded()

7. SnapKit 声明式布局 ​

swift
// SnapKit 是 AutoLayout 的 DSL 封装,简化约束编写
// 原理:在内部转换为 NSLayoutConstraint

import SnapKit

class MyViewController: UIViewController {
    
    let title = UILabel()
    let button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(title)
        view.addSubview(button)
        
        // 声明式约束(可读性极高)
        title.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).offset(20)
            make.left.equalTo(view.snp.left).offset(16)
            make.right.equalTo(view.snp.right).offset(-16)
        }
        
        button.snp.makeConstraints { make in
            make.top.equalTo(title.snp.bottom).offset(16)
            make.centerX.equalTo(view.snp.centerX)
            make.width.equalTo(120)
            make.height.equalTo(44)
        }
    }
}

// SnapKit 优势:
// • 类型安全(编译期检查约束属性类型)
// • 链式调用(代码更紧凑)
// • 自动处理约束激活/失效
// • 支持更新约束(updateConstraints)
// • 支持约束失效(removeConstraints)
// • 支持约束调试(printLogOnFail)

8. 布局性能分析 ​

8.1 常见布局性能瓶颈 ​

布局性能问题排查清单:

┌───────┬──────────────────────┬───────────────┬──────────┐
│ 问题   │ 产生原因              │ 检测方式       │ 解决方案  │
├───────┼──────────────────────┼───────────────┼──────────┤
│ 约束过多│ 一个视图上约束>10      │ 调试器/日志   │ 合并约束  │
│ 约束冲突│ 多约束冲突无法求解      │ 崩溃日志      │ 修复优先级│
│ 频繁布局│ 循环触发 setNeedsLayout│ Instruments   │ 避免重复  │
│ 离屏渲染│ cornerRadius/shadow    │ CA Color Offscreen │ CAShapeLayer│
│ 复杂约束│ 嵌套约束/循环约束      │ 调试器警告      │ 简化约束  │
│ 动态约束│ 运行时频繁修改约束      │ 性能分析      │ 预定义约束│
│ 惯性布局│ large contents size 缓存未命中│ 调试器     │ 设置intrinsicSize│
└───────┴──────────────────────┴───────────────┴──────────┘

8.2 布局性能优化策略 ​

swift
// 优化策略(按优先级排序):

// 1. 减少约束数量(最有效的优化)
// 每个视图的约束数量控制在 5-8 个以内
// 合并多个约束为一个(如用 multiplier 代替多个 constant)

// 2. 避免嵌套 StackView
// 嵌套 StackView 会创建大量隐式约束
// 替代方案:使用 AutoLayout 直接约束

// 3. 使用 prefersStatusBarUpdateAnimation
// 避免在布局过程中触发状态栏更新

// 4. 预渲染静态内容
// 静态背景/图标等预渲染为图片(避免每帧重新绘制)

// 5. 减少 draw(_:) 调用
// 使用 CoreGraphics 绘制的视图放在后台渲染

// 6. 使用 UIView.performWithoutAnimation
// 在代码中修改布局时避免触发动画
UIView.performWithoutAnimation {
    view.frame = newFrame
}

// 7. 约束复用(TableView/CollectionView)
// 复用单元格时,约束不需要重新创建
// NSLayoutConstraint 对象可复用

// 8. 使用 AutoResizing(Autoresizing)
// 简单场景用 autoresizingMask 比 AutoLayout 快 5-10x
// 适合:简单的相对位置调整

9. 面试题汇总 ​

高频面试题 ​

Q1: UIView 和 CALayer 的本质区别是什么?为什么 UIView 需要 layer?

答:

  • UIView 继承自 UIResponder,负责事件响应和渲染代理
  • CALayer 是实际存储像素数据的层,负责渲染
  • UIView 通过 layer 代理与 CALayer 关联(关联对象模式)
  • UIView 的渲染完全委托给 layer,自身不存储任何像素数据
  • UIView 的优势:可以响应事件、管理层级关系、处理手势

Q2: 离屏渲染的产生原因和优化方案?

答:

  • 产生原因:GPU 无法在当前渲染帧处理,需要创建新的缓冲区
  • 触发条件:masksToBounds/clipsToBounds、cornerRadius(非 CAShapeLayer)、shadow、复杂混合模式、模糊效果
  • 检测:Instruments → Core Animation → Color Off-screen Rendered(红色 = 离屏)
  • 优化:
    • 圆角:用 CAShapeLayer + mask 替代
    • 阴影:设置 shadowPath 避免离屏
    • 预渲染:静态内容用 UIGraphicsImageRenderer
    • 避免同时使用圆角 + 阴影 + 裁剪

Q3: AutoLayout 的求解过程?

答:

  1. 收集所有 NSLayoutConstraint 对象
  2. 构建约束图(视图为节点,约束为边)
  3. 求解线性方程组(高斯消元法)
  4. 处理优先级(高优先级优先满足,低优先级让步)
  5. 输出每个视图的最终 frame
  6. 通过 layoutSubviews 应用到 UIView
  7. 双遍算法:自上而下确定约束 → 自下而上确定尺寸

Q4: StackView 的工作原理?有哪些坑?

答:

  • 内部通过自动创建 NSLayoutConstraint 实现
  • 遍历 arrangedSubviews 计算尺寸并分配
  • 坑:子视图上设置 leading/trailing 会被覆盖、压缩优先级冲突、动态内容不自动更新

Q5: 如何优化列表的布局性能?

答:

  • 减少约束数量(5-8 个以内)
  • 预计算 intrinsicContentSize
  • 使用 Content Size Category 缓存
  • 避免在布局回调中修改约束
  • 复用约束对象而非每次创建新约束
  • 复杂单元格使用 draw 替代 AutoLayout

10. 参考资源 ​