尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入UIKitPlus源码:PreConstraint延迟约束系统的实现原理与内存安全细节

深入UIKitPlus源码:PreConstraint延迟约束系统的实现原理与内存安全细节 深入UIKitPlus源码PreConstraint延迟约束系统的实现原理与内存安全细节【免费下载链接】UIKitPlus Declarative UIKit with LivePreview for iOS9 (best alternative to SwiftUI)项目地址: https://gitcode.com/gh_mirrors/ui/UIKitPlusUIKitPlus 是一个用 Swift 编写的声明式 UIKit 框架被称为 SwiftUI 的最佳替代方案。它的招牌能力是「延迟约束」在视图还没有被加入父视图之前就能提前写好所有约束甚至只凭一个 tag 数字就能引用一个尚未创建的视图。这个能力背后是源码中一个名为 PreConstraint 的内部类在支撑。本文将带你深入 UIKitPlus 的 PreConstraint 延迟约束系统看清它的实现原理以及其中几处精妙的内存安全细节。延迟约束解决什么问题在传统 UIKit 中写约束有个硬性前提视图必须已经加到父视图上。view.widthAnchor.constraint(equalToConstant: 100) // 需要先 addSubview这带来两个常见痛点顺序尴尬你无法在创建视图的同时顺手声明它的布局必须先 addSubview 再写约束代码散落各处互相引用困难两个视图互相约束时必须先把其中一个声明为外部变量。UIKitPlus 的解决方案是让声明完全自由UButton(Click me).width(300).centerInSuperview() // 声明即完成无需父视图而真正让这一切成立的幕后功臣就是 PreConstraint——一条预约束记录。PreConstraint约束的草稿箱当你在视图还没有父视图时就调用.width(300)这类 DSL 方法UIKitPlus 并不会真的去创建 NSLayoutConstraint那时根本创建不了而是生成一条 PreConstraint 记录把你想怎么布局这件事先存下来。一条 PreConstraint 记录的字段非常直观字段含义value约束数值且是一个响应式的StateCGFloat之后改了还能实时生效relation/multiplier/priorityAuto Layout 的标准三要素attribute1/attribute2本视图与目标视图的布局属性fromView发起约束的视图destinationView约束目标的占位符可以是视图也可以是 tagconstraint最终真正激活后的 NSLayoutConstraint 引用这些记录会被分类存放在视图的内部属性容器 PropertiesInternal 中按类型分成三组notAppliedPreConstraintsSolo自身约束如宽高notAppliedPreConstraintsSuper相对父视图的约束notAppliedPreConstraintsRelative相对其他视图的约束注意notApplied未应用和applied已应用的命名——这暗示了它们之间存在一次移交这正是延迟的精髓。激活时机视图搬进父视图的那一刻真正的魔法发生在视图被加入父视图的瞬间。DeclarativeProtocolMovedToSuperview.swift 中只有短短几行func movedToSuperview() { for constraint in _properties.notAppliedPreConstraintsSolo { declarativeView.activateSolo(constraint) } for constraint in _properties.notAppliedPreConstraintsSuper { declarativeView.activateSuper(constraint) } for constraint in _properties.notAppliedPreConstraintsRelative { declarativeView.activateRelative(constraint) } }也就是说声明阶段只记账激活阶段才建账。视图一旦搬进父视图三条流水线的草稿约束被依次转正为真正的 NSLayoutConstraint。内存安全细节一WeakBox 与弱引用目标约束要引用两个视图如果 PreConstraint 强持有它们很容易形成引用环视图持有属性容器 → 容器持有 PreConstraint → PreConstraint 强持有视图。源码的对策在 PreConstraint.swift 里weak var fromView: BaseView? /// A container that stores a weak reference to its Element public class WeakBoxElement: Hashable where Element: AnyObject { weak var underlying: Element? }fromView直接声明为weak目标视图则被包进一个WeakBox内部类型别名为WeakBaseView构造时自动转换if let view destinationView as? BaseView { self.destinationView WeakBaseView(view) }用WeakBox而不是直接写weak var是因为 Swift 的弱引用无法持有协议类型PreConstraintViewable。WeakBox 让弱引用一个协议对象成为可能同时它遵守Hashable还能安全地放进集合。弱引用也带来了自然的容错目标视图消失时WeakBox.underlying自动变nil约束只是安静地不再激活而不是崩溃。内存安全细节二tag 型延迟约束UIKitPlus 还支持只凭 tag 建立约束UView().size(100).top(to: 7) // 此时 tag 为 7 的视图还不存在 UView().size(200).tag(7)这靠的是 PreConstraintView 这个枚举public enum PreConstraintView { case view(BaseView) case tag(Int) }PreConstraint里存的是一个占位符而非具体视图。激活时才调用unwrapWithSuperview用viewWithTagInSuperview现场查找。所以约束声明的顺序完全无所谓甚至目标视图可以过几秒才动态添加——所有引用它的视图会在它出现的瞬间粘上去。内存安全细节三deinit 里的线程安全监听器value是响应式状态。PreConstraint 在初始化时订阅它让约束常数能随状态实时更新valueListener value.listen { [weak self] constant in self?.constraint?.constant constant self?.fromView?.superview?.layoutIfNeeded() }这里有个隐蔽的坑监听器引用了 self弱但如果 State 比 PreConstraint 活得久State 内部会一直存着这个闭包。PreConstraint 释放时必须主动取消监听否则闭包和 State 之间会互相拖累。源码在deinit中处理了这一点但 Swift 6 下 deinit 是非隔离的nonisolated不能直接触碰MainActor的成员。解法是一个专为此设计的小类/// PreConstraint can be released from a nonisolated deinitializer... private final class PreConstraintListenerStorage: unchecked Sendable { private let lock NSLock() private var listener: StateListener? func cancel() { lock.withLock { listener?.cancel() listener nil } } }配合deinit { valueListenerStorage.cancel() }锁保护的存储成为deinit 唯一可以安全触碰的状态取消监听在任意线程都是同步且幂等的。这是整个文件中注释密度最高的一段也是框架作者对 Swift 并发模型打磨最细的地方。顺带一提等值判断也刻意做成了线程安全的nonisolated static func (lhs: PreConstraint, rhs: PreConstraint) - Bool { lhs.id rhs.id // 只读不可变的 UUID不碰任何隔离状态 }测试视角四条内存安全断言框架没有只靠注释保证正确性PreConstraintOwnershipTests.swift 用四个测试把内存语义钉死测试验证的内存语义testPreConstraintStateUpdateUpdatesConstraintConstant状态变更 → 约束常数实时同步testPreConstraintDeallocCancelsListenerPreConstraint 释放 → 监听器随之取消不留残骸testInvertedPreConstraintListenerIndependentCleanup反转约束A→B 变 B→A持有独立监听器原始约束销毁不影响它testEphemeralStateConstraintDoesNotLeakState、PreConstraint、Listener 三者引用计数干净归零无泄漏其中反转约束是 tag 约束的底层支撑当 A 约束到 B从 B 的视角看就是一条反转的 PreConstraint两者各自持有、独立清理监听器互不牵连。小结值得抄进你工具箱的三个设计草稿-激活两段式把无法立即完成的操作先记账等前提满足再统一执行。适用于任何有生命周期时序的框架开发WeakBox 包装协议型弱引用Swift 弱引用不支持协议类型时的通用解法还顺便解决了 Hashable 入集合的问题NSLock 小盒子 deinit 取消监听Swift 6 下非隔离 deinit 触碰 MainActor 状态的规范写法值得逐字模仿。PreConstraint 一共只有一百多行代码却同时解决了延迟布局、响应式约束、tag 引用和并发安全四个问题——这正是 UIKitPlus 用 UIKit 写出声明式代码背后那种工程密度的缩影。如果你想动手验证可以从Tests/UIKitPlusTests/PreConstraintOwnershipTests.swift跑起再看 Classes/Objects/PreConstraint.swift 的完整实现整个延迟约束系统的脉络会非常清晰。【免费下载链接】UIKitPlus Declarative UIKit with LivePreview for iOS9 (best alternative to SwiftUI)项目地址: https://gitcode.com/gh_mirrors/ui/UIKitPlus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表