> import UIKit

class SCDisplayLinkProxy: NSObject {

    // MARK: Lifecycle

    init(target: AnyObject, selector: Selector) {

        self.target = target

        self.selector = selector

    }

    // MARK: Internal

    weak var target: AnyObject?

    let selector: Selector

    @objc

    func onDisplayLinkTick(_ displayLink: CADisplayLink) {

        _ = target?.perform(selector, with: displayLink)

    }

}帮我分析为什么这么卸

CADisplayLink 代理模式分析

  这是一个经典的循环引用解决方案,专门用于 CADisplayLink。

1. 问题背景:CADisplayLink 的循环引用

  错误写法(会导致内存泄漏):

  class MyViewController: UIViewController {

      var displayLink: CADisplayLink?

      override func viewDidLoad() {

          super.viewDidLoad()

          // ❌ 错误:CADisplayLink 强引用 self

          displayLink = CADisplayLink(

              target: self,  // CADisplayLink 强引用 self

              selector: #selector(onTick)

          )

          displayLink?.add(to: .main, forMode: .common)

      }

      @objc func onTick() {

          // 更新动画

      }

      deinit {

          print("不会被调用!因为循环引用")

      }

  }

  循环引用链:

  ViewController (强引用) → displayLink

          ↑                      ↓

          └──────── (强引用) ← target (self)

  后果:

  - ViewController 永远不会释放

  - deinit 永远不会被调用

  - 内存泄漏

  ---

2. 解决方案:SCDisplayLinkProxy 的设计原理

  关键设计点:

  (1) weak var target: AnyObject?

  weak var target: AnyObject?  // 👈 弱引用,不增加引用计数

  - 弱引用目标对象,不会造成循环引用

  - 目标对象释放时自动变为 nil

  (2) 中间代理对象

  init(target: AnyObject, selector: Selector) {

      self.target = target      // 弱引用目标

      self.selector = selector  // 保存要调用的方法

  }

  - Proxy 强引用 selector(值类型,无引用计数)

  - Proxy 弱引用 target

  (3) 转发调用

  @objc

  func onDisplayLinkTick(_ displayLink: CADisplayLink) {

      _ = target?.perform(selector, with: displayLink)

      // 如果 target 已释放,这里什么都不做

  }

  - CADisplayLink 调用 Proxy 的方法

  - Proxy 再转发给真正的 target

  - 如果 target 已释放,安全地跳过

  ---

3. 正确使用方式

  class MyViewController: UIViewController {

      var displayLink: CADisplayLink?

      var proxy: SCDisplayLinkProxy?  // 保存 proxy 引用

      override func viewDidLoad() {

          super.viewDidLoad()

          // ✅ 正确:使用 Proxy 打破循环引用

          proxy = SCDisplayLinkProxy(

              target: self,

              selector: #selector(onTick)

          )

          displayLink = CADisplayLink(

              target: proxy!,  // CADisplayLink 强引用 proxy

              selector: #selector(SCDisplayLinkProxy.onDisplayLinkTick(_:))

          )

          displayLink?.add(to: .main, forMode: .common)

      }

      @objc func onTick(_ displayLink: CADisplayLink) {

          // 更新动画

          print("FPS: \(1.0 / displayLink.duration)")

      }

      deinit {

          displayLink?.invalidate()  // 必须手动停止

          print("ViewController 正常释放 ✅")

      }

  }

  引用关系:

  ViewController (强引用) → proxy (弱引用) → ViewController

          ↓

      displayLink (强引用) → proxy

  - displayLink 强引用 proxy

  - proxy 弱引用 ViewController

  - 没有循环引用 ✅

  ---

  4. 完整工作流程

  1. CADisplayLink 每帧触发

     ↓

  2. 调用 proxy.onDisplayLinkTick(_:)

     ↓

  3. proxy 检查 target 是否存在

     ↓

  4. 如果存在,调用 target.perform(selector, with: displayLink)

     ↓

  5. 目标对象的 onTick 方法执行

  如果 ViewController 已释放:

  3. proxy.target == nil

  4. 安全跳过,不会崩溃

  ---

5. 设计优缺点分析

  优点:

  1. ✅ 解决循环引用: 通过弱引用打破引用环

  2. ✅ 安全性: target 释放后不会崩溃

  3. ✅ 简洁: 代码量少,易于理解

  4. ✅ 通用性: 可用于所有类似场景(Timer、NSRunLoop等)

  潜在问题:

  1. ⚠️ 需要手动 invalidate: 即使用了 Proxy,仍需在 deinit 中调用 displayLink?.invalidate()

  2. ⚠️ 多一层间接调用: 轻微性能开销(实际可忽略不计)

  ---

  6. 为什么不能直接用 [weak self]?

  // ❌ CADisplayLink 不支持闭包,只能用 target-action

  displayLink = CADisplayLink(

      target: self,  // 必须传入对象,不能用闭包

      selector: #selector(onTick)

  )

  // ⚠️ Timer 有类似问题(iOS 10+ 可以用闭包)

  Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in

      self?.onTick()  // ✅ iOS 10+ 可以这样写

  }

  ---

  7. 其他解决方案对比

  方案1: iOS 10+ 使用闭包API

  if #available(iOS 10.0, *) {

      displayLink = CADisplayLink(block: { [weak self] displayLink in

          self?.onTick(displayLink)

      })

  }

  方案2: 使用 NSProxy (更高级)

  class WeakProxy: NSProxy {

      weak var target: AnyObject?

      override func forwardingTarget(for aSelector: Selector!) -> Any? {

          return target

      }

  }

  你们的方案是最经典的解决方式,在 iOS 10 之前是唯一正确方法。

  ---

  总结

  这个 SCDisplayLinkProxy 类是一个循环引用打破器:

  1. 问题: CADisplayLink 强引用 target,导致内存泄漏

  2. 方案: 插入中间代理,用 weak 引用目标对象

  3. 原理:

    - CADisplayLink → (强) → Proxy → (弱) → ViewController

    - 打破了循环引用链

  4. 适用场景: 所有需要 target-action 且会造成循环引用的地方

  这是 iOS 开发中处理 CADisplayLink、Timer 等对象的标准最佳实践

  需要我展示如何在你们项目中使用这个类吗?

> layoutIfNeeded()能在ViewDidLoad中调用吗

layoutIfNeeded() 在 viewDidLoad 中调用的问题

  简短回答

  技术上可以调用,但通常无效或不推荐 ⚠️

⏺ ---

  1. 视图生命周期和布局时机

  class MyViewController: UIViewController {

      override func viewDidLoad() {

          super.viewDidLoad()

          // ❌ 此时视图的 frame 可能还是错误的

          print("viewDidLoad: \(view.frame)")

          // 可能输出: (0, 0, 0, 0) 或者 (0, 0, 375, 667)

          view.layoutIfNeeded()  // ⚠️ 调用了,但可能无效

      }

      override func viewWillAppear(_ animated: Bool) {

          super.viewWillAppear(animated)

          // ⚠️ frame 可能还不准确(取决于约束)

          print("viewWillAppear: \(view.frame)")

      }

      override func viewDidLayoutSubviews() {

          super.viewDidLayoutSubviews()

          // ✅ 此时 frame 已经确定

          print("viewDidLayoutSubviews: \(view.frame)")

      }

      override func viewDidAppear(_ animated: Bool) {

          super.viewDidAppear(animated)

          // ✅ 视图已经在屏幕上,frame 确定

          print("viewDidAppear: \(view.frame)")

      }

  }

  生命周期顺序:

  1. init / loadView

  2. viewDidLoad          ← 视图加载到内存,但 frame 未确定

  3. viewWillAppear

  4. viewWillLayoutSubviews

  5. viewDidLayoutSubviews ← Auto Layout 约束已应用

  6. viewDidAppear         ← 视图已在屏幕上

⏺ ---

  2. 为什么在 viewDidLoad 中通常无效?

  问题 1: 约束还没应用

  override func viewDidLoad() {

      super.viewDidLoad()

      let label = UILabel()

      view.addSubview(label)

      label.snp.makeConstraints { make in

          make.center.equalToSuperview()

          make.size.equalTo(CGSize(width: 100, height: 50))

      }

      // ❌ 此时约束虽然添加了,但系统还没计算布局

      print(label.frame)  // 输出: (0, 0, 0, 0)

      view.layoutIfNeeded()  // ⚠️ 可能触发布局,但 view.frame 可能不准

      print(label.frame)  // 可能还是 (0, 0, 0, 0) 或错误值

  }

  问题 2: 父视图的 frame 未确定

  override func viewDidLoad() {

      super.viewDidLoad()

      // view.frame 可能是:

      // - (0, 0, 0, 0) - 如果还没加入窗口

      // - (0, 0, 375, 667) - Storyboard 的模拟尺寸

      // 但不是最终屏幕尺寸

      print(view.frame)  // 不准确

  }

  ---

  3. 正确的调用时机

场景 1: 获取准确 frame

  // ✅ 推荐:在 viewDidLayoutSubviews

  override func viewDidLayoutSubviews() {

      super.viewDidLayoutSubviews()

      // 此时所有约束已应用,frame 准确

      print(myLabel.frame)  // 正确的值

      // 可以安全地使用 frame 做计算

      setupGradientLayer()

  }

  // ✅ 或者在 viewDidAppear

  override func viewDidAppear(_ animated: Bool) {

      super.viewDidAppear(animated)

      // 视图已在屏幕上,frame 100% 准确

      startAnimation()

  }

  场景 2: 强制立即布局(动画前)

  // ✅ 约束动画的正确写法

  func animateButton() {

      // 1. 修改约束

      buttonTopConstraint.constant = 100

      // 2. 标记需要布局(不立即执行)

      view.setNeedsLayout()

      // 3. 在动画块中强制立即布局

      UIView.animate(withDuration: 0.3) {

          self.view.layoutIfNeeded()  // ✅ 这里是正确的用法

      }

  }

  场景 3: 动态添加子视图后需要立即获取 frame

  func addCustomView() {

      let customView = UIView()

      view.addSubview(customView)

      customView.snp.makeConstraints { make in

          make.center.equalToSuperview()

          make.size.equalTo(100)

      }

      // ✅ 如果需要立即获取 frame

      view.layoutIfNeeded()  // 强制立即计算布局

      print(customView.frame)  // 现在可以得到正确的值

      // 使用 frame 做后续操作

      addShadow(to: customView)

  }

  ---

  4. 实际问题案例

  案例 1: CAGradientLayer 位置错误

  // ❌ 错误写法

  override func viewDidLoad() {

      super.viewDidLoad()

      let gradientLayer = CAGradientLayer()

      gradientLayer.frame = myView.bounds  // ⚠️ bounds 可能是 (0,0,0,0)

      myView.layer.addSublayer(gradientLayer)

  }

  // ✅ 正确写法

  override func viewDidLayoutSubviews() {

      super.viewDidLayoutSubviews()

      if gradientLayer == nil {

          gradientLayer = CAGradientLayer()

          myView.layer.addSublayer(gradientLayer!)

      }

      gradientLayer?.frame = myView.bounds  // bounds 已确定

  }

  案例 2: 圆角和阴影

  // ❌ 错误

  override func viewDidLoad() {

      super.viewDidLoad()

      // frame 不准,圆角半径错误

      myView.layer.cornerRadius = myView.frame.height / 2

  }

  // ✅ 正确

  override func viewDidLayoutSubviews() {

      super.viewDidLayoutSubviews()

      // 在布局完成后设置

      myView.layer.cornerRadius = myView.frame.height / 2

  }

  ---

5. 总结和最佳实践

  | 方法                    | 适合调用 layoutIfNeeded() | 原因              |

  |-----------------------|-----------------------|-----------------|

  | viewDidLoad           | ❌ 不推荐                 | frame 未确定,约束未应用 |

  | viewWillAppear        | ⚠️ 谨慎使用               | frame 可能还不准确    |

  | viewDidLayoutSubviews | ✅ 推荐                  | 约束已应用,frame 准确  |

  | viewDidAppear         | ✅ 可以                  | 视图已在屏幕上         |

  | 动画块内                  | ✅ 常用                  | 强制立即应用约束变化      |

  最佳实践:

  class MyViewController: UIViewController {

      // ✅ viewDidLoad: 只做视图初始化

      override func viewDidLoad() {

          super.viewDidLoad()

          setupUI()           // 添加子视图

          setupConstraints()  // 添加约束

          // ❌ 不要调用 layoutIfNeeded()

      }

      // ✅ viewDidLayoutSubviews: 依赖 frame 的操作

      override func viewDidLayoutSubviews() {

          super.viewDidLayoutSubviews()

          updateGradientFrame()  // 更新图层 frame

          updateCornerRadius()   // 更新圆角

      }

      // ✅ 动画中使用

      func animate() {

          constraint.constant = 100

          UIView.animate(withDuration: 0.3) {

              self.view.layoutIfNeeded()  // 立即应用约束

          }

      }

  }

  简记:

  - viewDidLoad: 不要调用 layoutIfNeeded()

  - viewDidLayoutSubviews: 可以用 frame 了

  - 动画块: 必须调用 layoutIfNeeded() 来驱动约束动画

  需要我帮你分析具体的使用场景吗?

> tableView的plain样式和Group样式区别

UITableView: Plain vs Grouped 样式详解

  1. 视觉外观对比

Plain 样式 (.plain)

  ┌─────────────────────────────┐

  │ Section Header (浮动悬停)    │  ← 滚动时会悬停在顶部

  ├─────────────────────────────┤

  │ Row 1                       │  ← 左右贴边,无圆角

  │─────────────────────────────│

  │ Row 2                       │

  │─────────────────────────────│

  │ Row 3                       │

  ├─────────────────────────────┤

  │ Section Header 2 (浮动)      │  ← 滚动时替换上一个 Header

  ├─────────────────────────────┤

  │ Row 4                       │

  │─────────────────────────────│

  │ Row 5                       │

  └─────────────────────────────┘

  Grouped 样式 (.grouped)

  ┌─────────────────────────────┐

  │                             │  ← 上边距

  │ Section Header (不悬停)      │  ← 跟随内容滚动

  │ ┌─────────────────────────┐ │

  │ │ Row 1                   │ │  ← 有左右边距

  │ ├─────────────────────────┤ │

  │ │ Row 2                   │ │  ← 圆角分组

  │ ├─────────────────────────┤ │

  │ │ Row 3                   │ │

  │ └─────────────────────────┘ │

  │ Section Footer              │

  │                             │  ← Section 间距

  │ Section Header 2            │

  │ ┌─────────────────────────┐ │

  │ │ Row 4                   │ │

  │ └─────────────────────────┘ │

  │                             │

  └─────────────────────────────┘

  关键视觉差异:

  | 特性         | Plain  | Grouped       |

  |------------|--------|---------------|

  | Cell 宽度    | 铺满屏幕宽度 | 左右有边距(约16pt)  |

  | Section 背景 | 无明显分组  | 有圆角白色背景卡片     |

  | Section 间距 | 紧密相连   | Section 之间有间距 |

  | 整体背景色      | 白色     | 浅灰色 (系统分组背景色) |

  ---

  2. Section Header/Footer 行为差异

Plain 样式的 Section Header

  // ✅ Plain 样式特性

  - Header 会浮动悬停在顶部(Sticky Header)

  - 向下滚动时,新的 Header 会"推开"上一个 Header

  - 适合快速定位当前所在的 Section

  演示:

  初始状态:

  ┌─────────────────┐

  │ Header A        │  ← 固定在顶部

  ├─────────────────┤

  │ Row A1          │

  │ Row A2          │

  │ Row A3          │

  └─────────────────┘

  向下滚动后:

  ┌─────────────────┐

  │ Header B        │  ← "推开"了 Header A

  ├─────────────────┤

  │ Row B1          │

  │ Row B2          │

  │ Row B3          │

  └─────────────────┘

  Grouped 样式的 Section Header

  // ✅ Grouped 样式特性

  - Header 不悬停,跟随内容一起滚动

  - 滚动离开屏幕后就看不见了

  - 强调分组的独立性

  代码验证:

  // Plain 样式

  let plainTableView = UITableView(frame: .zero, style: .plain)

  // Section Header 会悬停

  // Grouped 样式

  let groupedTableView = UITableView(frame: .zero, style: .grouped)

  // Section Header 不会悬停

  ---

  3. 技术特性对比

⏺ | 特性                | Plain                  | Grouped                       |

  |-------------------|------------------------|-------------------------------|

  | Section Header 悬停 | ✅ 是(Sticky)            | ❌ 否(跟随滚动)                     |

  | 默认背景色             | .systemBackground (白色) | .systemGroupedBackground (浅灰) |

  | Cell 分隔线          | 铺满宽度                   | 有左侧缩进                         |

  | Section 间距        | 0                      | 约 35pt(可自定义)                  |

  | 默认 Header 高度      | 如果未实现代理方法,会有默认高度       | 如果返回 0,高度为 0                  |

  | 适合长列表             | ✅ 是                    | ⚠️ 不太适合                       |

  | 内存占用              | 较低(无额外间距)              | 稍高(Section 间距)                |

  默认高度差异

  // Plain 样式

  func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {

      // 如果不实现,系统会返回默认值(约 28pt)

  }

  // Grouped 样式

  func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {

      // 如果不实现,系统会返回默认值(约 35pt)

      // 如果返回 0 或 .leastNormalMagnitude,可以完全隐藏

  }

  // ⚠️ 注意:要完全隐藏 Header,需要这样做

  func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {

      return .leastNormalMagnitude  // 使用极小值而非 0

  }

  func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

      return nil

  }

  ---

  4. iOS 13+ 新增样式

  // iOS 13+ 引入了更多样式

  let tableView = UITableView(frame: .zero, style: .insetGrouped)

  // .insetGrouped - 类似 Grouped,但边距更大,更圆润

  // 用于 iPad 和现代设计

  // iOS 13+ 样式对比

  .plain           // 传统 Plain

  .grouped         // 传统 Grouped

  .insetGrouped    // ✨ 新增,更大的圆角和边距(推荐)

  InsetGrouped 特点:

  ┌─────────────────────────────┐

  │                             │

  │  ┌───────────────────────┐  │  ← 边距更大

  │  │ Row 1                 │  │  ← 圆角更圆润

  │  ├───────────────────────┤  │

  │  │ Row 2                 │  │

  │  └───────────────────────┘  │

  │                             │

  └─────────────────────────────┘

  ---

  5. 使用场景建议

使用 Plain 样式的场景

  // ✅ 联系人列表(按字母分组)

  class ContactsViewController: UITableViewController {

      init() {

          super.init(style: .plain)  // Header 悬停显示当前字母

      }

      // Section Header 显示 A, B, C... 并悬停

  }

  // ✅ 聊天消息列表

  // ✅ 邮件列表

  // ✅ 通知列表

  // ✅ 搜索结果

  典型 App 示例:

  - 通讯录 App - Plain 样式,字母索引悬停

  - 微信消息列表 - Plain 样式

  - 邮件 App - Plain 样式

  使用 Grouped 样式的场景

  // ✅ 设置页面(iOS 设置 App)

  class SettingsViewController: UITableViewController {

      init() {

          super.init(style: .grouped)  // 清晰的分组

      }

      // Section: 账号设置

      // Section: 隐私设置

      // Section: 关于

  }

  // ✅ 表单页面

  // ✅ 详情页面(用户信息、商品详情)

  // ✅ 配置页面

  典型 App 示例:

  - iOS 设置 App - Grouped/InsetGrouped

  - 支付宝设置页 - Grouped

  - 用户资料编辑页 - Grouped

  ---

  6. 实际代码示例

  // Plain 样式 - 联系人列表

  class ContactsTableViewController: UITableViewController {

      let contacts = [

          "A": ["Alice", "Andy"],

          "B": ["Bob", "Betty"],

          "C": ["Charlie"]

      ]

      override func viewDidLoad() {

          super.viewDidLoad()

          tableView = UITableView(frame: .zero, style: .plain)  // Plain 样式

      }

      override func numberOfSections(in tableView: UITableView) -> Int {

          return contacts.keys.count

      }

      override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {

          return Array(contacts.keys.sorted())[section]  // "A", "B", "C"

          // ✅ 滚动时会悬停在顶部

      }

  }

  // Grouped 样式 - 设置页面

  class SettingsTableViewController: UITableViewController {

      override func viewDidLoad() {

          super.viewDidLoad()

          if #available(iOS 13.0, *) {

              tableView = UITableView(frame: .zero, style: .insetGrouped)  // 现代样式

          } else {

              tableView = UITableView(frame: .zero, style: .grouped)

          }

      }

      override func numberOfSections(in tableView: UITableView) -> Int {

          return 3  // 账号、隐私、关于

      }

      override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {

          return ["账号设置", "隐私设置", "关于"][section]

          // ✅ 不会悬停,跟随内容滚动

      }

      override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {

          if section == 0 {

              return "您可以在此修改账号信息"  // Section Footer 说明文字

          }

          return nil

      }

  }

  ---

  7. 如何消除 Grouped 样式的顶部/底部空白

  // ❌ Grouped 样式默认有顶部和底部空白

  let tableView = UITableView(frame: .zero, style: .grouped)

  // 顶部有约 35pt 的空白

  // ✅ 解决方案 1: 设置 Header/Footer 高度

  func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {

      return section == 0 ? .leastNormalMagnitude : 10  // 第一个 Section 移除顶部空白

  }

  func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {

      return .leastNormalMagnitude  // 移除底部空白

  }

  // ✅ 解决方案 2: 调整 contentInset

  tableView.contentInset = UIEdgeInsets(top: -35, left: 0, bottom: 0, right: 0)

  ---

  8. 快速对比表格

  | 需求              | 选择样式                     | 原因             |

  |-----------------|--------------------------|----------------|

  | 长列表(如通讯录、消息)    | .plain                   | Header 悬停,快速定位 |

  | 设置页面            | .insetGrouped (iOS 13+)  | 清晰分组,现代设计      |

  | 表单页面            | .grouped 或 .insetGrouped | 分组清晰           |

  | 搜索结果            | .plain                   | 无需分组视觉         |

  | 商品详情(多 Section) | .grouped                 | 分组展示不同信息       |

  | 短列表(< 20 项)     | .grouped                 | 视觉更舒适          |

  | 超长列表(> 1000 项)  | .plain                   | 性能更好           |

  ---

  总结

  Plain 样式:

  - 适合长列表、快速滚动场景

  - Header 悬停,方便定位

  - 视觉紧凑,无分组感

  Grouped 样式:

  - 适合设置页、表单、详情页

  - 清晰的分组视觉

  - Header 不悬停,更独立

  现代推荐:

  - iOS 13+ 优先使用 .insetGrouped 替代 .grouped

  - 更符合 Apple 设计规范

  需要我帮你分析具体的使用场景吗?

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐