Texture 布局引擎深入解析:从 ASLayoutSpec 到 UIKit 渲染的完整流程

发布时间:2026/9/27 7:11:03
Texture 布局引擎深入解析:从 ASLayoutSpec 到 UIKit 渲染的完整流程 移动开发UI组件【免费下载链接】TextureSmooth asynchronous user interfaces for iOS apps.项目地址https://gitcode.com/gh_mirrors/te/Texture点击查看免费下载导读本篇技术指南以 TextureAsyncDisplayKit官方开发文档《Layout specs》为骨架系统讲解 Texture 的默认布局引擎Layout Spec 体系如何描述节点尺寸与排列、如何通过layoutSpecThatFits:递归计算布局以及布局结果最终如何被 UIKit 消费并驱动渲染。全文以examples/LayoutSpecExamples示例项目中的PhotoWithOutsetIconOverlay为实战案例并结合Source/Layout与Source/ASDisplayNodeLayout*.mm的源码实现帮助读者掌握布局计算的触发时机、调用栈结构、pending layout 机制与四阶段渲染流程。一、布局是什么尺寸、顺序与层级在 Texture 中布局Layout描述节点应该如何被设定尺寸以便显示。docs/_docs/development/layout-specs.md明确指出布局信息不仅定义了节点的尺寸size还定义了节点之间的水平或垂直排列顺序ordering当结合节点层级hierarchy来解读时节点的 frame 才最终确定。Texture 提供了两套可选的布局引擎默认的ASLayoutSpec系统本文的主体基于布局描述Layout Spec对象递归计算Yoga可选集成通过ASDisplayNodeYoga相关源码与#if YOGA编译分支接入。布局计算的入口是ASLayoutElement协议——任何可以参与布局的对象ASDisplayNode与ASLayoutSpec都遵循该协议。Source/Layout/ASLayoutElement.h中对其职责的定义是一个布局必须指定 1) 该 layoutElement 对象的尺寸而非位置以及 2) 其所有直接子对象的尺寸与位置。树形递归由父节点向子节点请求布局驱动父节点先获知子节点尺寸随后在尺寸已知后设置子节点位置。协议的核心方法layoutThatFits:会缓存约束与结果源码位置因此重复调用不会重复计算。二、实战案例PhotoWithOutsetIconOverlay官方文档推荐的学习方式是打开 Texture 的示例工程在子类化的ASDisplayNode各函数上打断点逐步观察 Texture 期望使用者如何与其 API 交互并步进查看暂停的调用栈。文档选取的示例来自examples/LayoutSpecExamples工程对应实现位于 examples/LayoutSpecExamples/Sample/LayoutExampleNodes.m。该节点创建两个ASNetworkImageNode子节点照片与图标并用ASAbsoluteLayoutSpec描述它们的摆放位置- (instancetype)init { self [super init]; if (self) { _photoNode [[ASNetworkImageNode alloc] init]; _photoNode.URL [NSURL URLWithString:http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-photo.png]; _iconNode [[ASNetworkImageNode alloc] init]; _iconNode.URL [NSURL URLWithString:http://texturegroup.org/static/images/layout-examples-photo-with-outset-icon-overlay-icon.png]; [_iconNode setImageModificationBlock:^UIImage *(UIImage *image, ASPrimitiveTraitCollection traitCollection) { CGSize profileImageSize CGSizeMake(60, 60); return [image makeCircularImageWithSize:profileImageSize withBorderWidth:10]; }]; } return self; } - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { _iconNode.style.preferredSize CGSizeMake(40, 40); _iconNode.style.layoutPosition CGPointMake(150, 0); _photoNode.style.preferredSize CGSizeMake(150, 150); _photoNode.style.layoutPosition CGPointMake(40 / 2.0, 40 / 2.0); ASAbsoluteLayoutSpec *absoluteSpec [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:[_photoNode, _iconNode]]; // ASAbsoluteLayoutSpecs .sizing property recreates the behavior of ASDK Layout API 1.0s ASStaticLayoutSpec absoluteSpec.sizing ASAbsoluteLayoutSpecSizingSizeToFit; return absoluteSpec; }几个关键点style.preferredSize与style.layoutPosition这些是ASLayoutElementStyle提供的样式属性。ASLayoutElementStyle通过ASStackLayoutElement、ASAbsoluteLayoutElement等协议簇对外暴露见 Source/Layout/ASLayoutElement.h。preferredSize声明节点期望尺寸layoutPosition声明节点在其父坐标系中的固定位置供绝对布局使用。ASAbsoluteLayoutSpec一种将子节点放置在固定位置的布局描述A layout spec that positions children at fixed positions见 Source/Layout/ASAbsoluteLayoutSpec.h。其sizing枚举有两种取值ASAbsoluteLayoutSpecSizingDefault规格占据可能的最大尺寸ASAbsoluteLayoutSpecSizingSizeToFit规格尺寸取所有子节点 frame 的并集用以复刻旧版 ASDK Layout API 1.0 中ASStaticLayoutSpec的行为枚举定义。三、布局流程触发、递归与调用栈3.1 递归计算入口布局计算是递归进行的并有多个起始触发点。其中一个触发点是父节点 frame 改变时另一个是节点树首次创建时——此时会发起一次布局展平layout flattening计算。递归计算的核心方法是layoutThatFits:- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize { return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; }这正是ASDisplayNode在 Source/ASDisplayNodeLayout.mm 中的实现它把约束尺寸的max作为父尺寸随后递归调用到其底层的整棵节点树。3.2 典型的调用栈下图展示了在一个包含简单视图层级、由ASDKViewController承载的布局中点击 Photo with outset icon overlay 示例并打断点在-[PhotoWithOutsetIconOverlay layoutSpecThatFits:]时看到的典型调用栈调用栈的第一个显著分支是顶层的-[ASDisplayNode calculateLayoutThatFits]它负责在 Texture 布局与 Yoga 引擎之间做选择- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize { __ASDisplayNodeCheckForLayoutMethodOverrides; switch (self.layoutEngineType) { case ASLayoutEngineTypeLayoutSpec: return [self calculateLayoutLayoutSpec:constrainedSize]; #if YOGA case ASLayoutEngineTypeYoga: return [self calculateLayoutYoga:constrainedSize]; #endif // If YOGA is not defined but for some reason the layout type engine is Yoga // we explicitly fallthrough here default: break; } // If this case is reached a layout type engine was defined for a node that is currently // not supported. ASDisplayNodeAssert(NO, No layout type determined); return nil; }layoutEngineType的判定逻辑同样在 Source/ASDisplayNodeLayout.mm 中只有在启用 YOGA 且节点拥有 Yoga 父/子节点时才返回ASLayoutEngineTypeYoga否则一律走 Layout Spec 引擎。调用栈随后穿过各个调用者最终到达节点树中的各个节点。整个栈依赖ASDisplayNode子类实现layoutSpecThatFits:并返回一个非空non-empty的布局规格。基类的默认实现会直接断言失败- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { __ASDisplayNodeCheckForLayoutMethodOverrides; ASDisplayNodeAssert(NO, -[ASDisplayNode layoutSpecThatFits:] should never return an empty value. One way this is caused is by calling -[super layoutSpecThatFits:] which is not currently supported.); return [[ASLayoutSpec alloc] init]; }这段代码位于 Source/ASDisplayNodeLayoutSpec.mm。它有两层含义一是子类必须覆写此方法二是千万不要在覆写中调用[super layoutSpecThatFits:]否则会触发断言。3.3 两种声明布局的方式除了覆写layoutSpecThatFits:ASDisplayNode还提供了layoutSpecBlock属性允许以 block 方式声明布局而不必创建子类Source/ASDisplayNodeLayoutSpec.hnode.layoutSpecBlock ^ASLayoutSpec *(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { // 返回一个包含全部子节点位置的 ASLayoutSpec };源码中有两条约束值得注意二者不可混用实现layoutSpecThatFits:的节点不能再设置layoutSpecBlock反之亦然否则触发异常断言见 Source/ASDisplayNodeLayoutSpec.mm。调用时机layoutSpecThatFits:可能在非主线程被调用因此该方法内不应执行 UI 线程相关操作但可在此处完成昂贵的预计算并用 ivar 缓存中间结果接口注释。calculateLayoutLayoutSpec:的实际执行流程Source/ASDisplayNodeLayoutSpec.mm还包括若节点既无layoutSpecBlock也未覆写layoutSpecThatFits:则退化为调用calculateSizeThatFits:做手工尺寸计算将 layout spec 标记为不可变isMutable NO——layout spec 在layoutSpecThatFits:中创建并允许修改一旦交回框架便禁止再改向 spec 子树传播 trait collection对返回的 layout 执行filteredNodeLayoutTree展平并支持 RTL从右到左镜像翻转。3.4 Pending Layout计算结果先缓存绘制时再消费此时尚未发生任何绘制。layout spec 此刻被贡献到一个名为_pendingDisplayNodeLayout的数据存储中ASDisplayNode基类上的实例变量等到下一次绘制操作时才被真正实现。可以在-[ASDisplayNode(Layout) layoutThatFits:parentSize:]中看到 pending layout 的捕获逻辑ASLayout *layout [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; as_log_verbose(ASLayoutLog(), Established pending layout for % in %s, self, sel_getName(_cmd)); _pendingDisplayNodeLayout ASDisplayNodeLayout(layout, constrainedSize, parentSize, version);对应源码在 Source/ASDisplayNodeLayout.mm。该方法的完整逻辑是三级缓存查找若_calculatedDisplayNodeLayout对当前约束、父尺寸与 layout version 有效直接复用否则若_pendingDisplayNodeLayout有效复用 pending否则重新计算并把结果写入_pendingDisplayNodeLayout。四、从布局计算到渲染四阶段流程布局计算与绘制之间隔着一次重要的中间步骤ASDisplayNode背后的 UIKit 对象已被放入 UIKit 层级——表现为UIView或折叠collapsed成CALayer。这正是 Texture 布局 spec 准备就绪、到它们真正用于给 UIKit 对象设定尺寸之间的衔接阶段。layout spec 真正参与渲染的位置是 UIKit 的layoutIfNeeded阶段对应-[ASDisplayNode(UIViewBridge) layoutIfNeeded]实现见 Source/Private/ASDisplayNodeUIViewBridge.mm。在该阶段pending layout 被消费用于确定即将显示节点的 frame 与 bounds。尺寸计算与摆放过程包含多个步骤核心方法之一是-[ASDisplayNode(Layout) _layoutSublayouts]好奇的读者可以顺藤摸瓜查看其调用方。下图展示的是布局应用rendering阶段pending layout 被消费、_layoutSublayouts等核心方法参与执行的调用栈综上Texture 松散地遵循 UIKit 系统、以不同阶段推进布局与渲染ASDisplayNode与ASLayout初始化两者可以完全独立于 UIKit 存在。此时创建一次 pending layout 计算。UIView/CALayer初始化遵循 UIKit 惯例在设定尺寸之前先创建用于显示的 UIKit 对象。此时它们已处于 UIKit 层级中从而触发后续的布局树流程。UIView/CALayer布局遵循 UIKit 的递归操作。这一步与第 1 步的布局计算截然不同——它纯粹是消费已准备好的 pending layout并应用到UIView/CALayer上完成尺寸设定。渲染必要时对CALayer进行栅格化rasterizeUIKit 层级随后被 UIKit 绘制到屏幕。五、数据结构支撑ASLayout 与 ASLayoutSpec5.1 ASLayout布局树的节点ASLayout是布局树中的一个节点代表创建它的ASLayoutElement的尺寸与位置Source/Layout/ASLayout.h。核心属性size当前布局的尺寸position在父布局中的位置默认是ASPointNull{NAN, NAN}作为子布局时必须为有效值sublayouts子布局数组其中每个子布局都必须有非空 positionframe由size与position计算出的有效 frame。几个值得注意的实现细节布局的size会被向上取整到像素值ASCeilPixelValue非法尺寸在 Release 配置下会被强制归零Source/Layout/ASLayout.mmASCalculateLayout与ASCalculateRootLayout是面向layoutElement的安全布局计算入口会针对 nil 节点做防护Source/Layout/ASLayout.h**布局展平flattening**由filteredNodeLayoutTree完成如果布局已经展平则直接复用否则通过 DFS 把 layout spec 节点摊平仅保留ASDisplayNode类型的子布局并把 spec 子节点的绝对位置逐级累加Source/Layout/ASLayout.mm。这也是布局结果能够直接指导 UIKit 对象摆放的原因。5.2 ASLayoutSpec不可变的布局描述ASLayoutSpec是一个不可变对象用于描述一种布局设计上松散地受到 React 启发Source/Layout/ASLayoutSpec.h。其子类覆盖了常见的布局需求例如ASStackLayoutSpec水平/垂直堆叠ASInsetLayoutSpec内边距ASAbsoluteLayoutSpec固定位置ASRatioLayoutSpec宽高比ASBackgroundLayoutSpec/ASOverlayLayoutSpec背景与覆盖层ASWrapperLayoutSpec包装一个或多个子元素Source/Layout/ASLayoutSpec.h。每个ASLayoutSpec必须作用于至少一个子节点。基类负责持有子节点child属性适用于单子节点的 spec如ASInsetLayoutSpecchildren数组适用于多子节点的 spec如ASStackLayoutSpec。setChild:atIndex:用于设定多子节点 spec 中的附加子节点——例如ASBackgroundLayoutSpec对外暴露background属性内部实则是调用setChild:atIndex:接口注释。可变性约束在实现中强制执行layout spec 创建时isMutable YES交回框架后被置为NO此后任何属性修改都会触发断言Source/Layout/ASLayoutSpec.mm。此外ASLayoutSpec还提供 ASCII 艺术风格的调试输出asciiArtString用于在控制台可视化布局树。5.3 样式属性style 与自动失效ASLayoutElementStyle承载节点的尺寸与布局选项包括width/height、minWidth/maxWidth、preferredSize、layoutPosition、flex 系列属性flexGrow、flexShrink、flexBasis、alignSelf、spacingBefore/After等属性常量定义见 Source/Layout/ASLayoutElement.h。一个关键的联动机制当样式属性变化时ASLayoutElementStyleDelegate的回调propertyDidChange:会被触发进而调用[self setNeedsLayout]Source/ASDisplayNodeLayout.mm。这保证了对style的任何修改都会自动使相关布局失效并在下一次布局传递中重算无需手动触发。六、小结与进一步探索理解 Texture 布局体系的核心脉络可以浓缩为一条链路layoutSpecThatFits:声明布局 →calculateLayoutThatFits:选择引擎 → 递归计算生成ASLayout树 → 展平并缓存为_pendingDisplayNodeLayout→ UIKitlayoutIfNeeded阶段消费 pending layout →_layoutSublayouts将尺寸与位置应用到 UIView/CALayer → 渲染。想要进一步深入可以在当前仓库中查看以下材料官方文档原文docs/_docs/development/layout-specs.md示例工程含PhotoWithOutsetIconOverlay等十余个布局案例examples/LayoutSpecExamples布局核心协议Source/Layout/ASLayoutElement.h布局 spec 基类Source/Layout/ASLayoutSpec.h 与 Source/Layout/ASLayoutSpec.mm布局结果对象Source/Layout/ASLayout.h 与 Source/Layout/ASLayout.mm节点布局计算与缓存Source/ASDisplayNodeLayout.mm 与 Source/ASDisplayNodeLayoutSpec.mm各 spec 的单元测试与快照测试位于 Tests/例如 Tests/ASLayoutSpecTests.mm、Tests/ASAbsoluteLayoutSpecSnapshotTests.mm、Tests/ASStackLayoutSpecSnapshotTests.mm可对照参考每种布局规格的预期行为。赞分享移动开发UI组件【免费下载链接】TextureSmooth asynchronous user interfaces for iOS apps.项目地址https://gitcode.com/gh_mirrors/te/Texture点击查看免费下载相关推荐LingBot-Video-MoE-30B-A3B高级配置详解Flow UniPC调度器与参数调优LingBot Video MoE 30B A3B高级配置详解Flow UniPC调度器与参数调优 想要充分发挥LingBot Video MoE 30B A终极指南Azul布局引擎从TAFFY到WebRender的完整渲染流程终极指南Azul布局引擎从TAFFY到WebRender的完整渲染流程 Azul是一个基于Rust构建的现代桌面GUI框架它采用 功能响应式编程模型 和 硬桌面应用前端跨平台Texture 布局组合实战深入理解 layoutSpecThatFits: 方法与 ASLayoutSpec 声明式布局Texture 布局组合实战深入理解 layoutSpecThatFits: 方法与 ASLayoutSpec 声明式布局 本文是 TextureAsync移动开发UI组件上一篇Git Credential Manager 应对 GitHub 令牌认证新规升级迁移指南与 PAT 手动配置方案下一篇如何平稳升级PySOT从旧版本迁移到新版本的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考