尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Texture 手动布局完全指南:calculateSizeThatFits 与 layout 的实战用法

发布时间:2026/9/26 3:15:02

资讯中心
01
ARTICLE

Texture 手动布局完全指南:calculateSizeThatFits 与 layout 的实战用法

Texture 手动布局完全指南:calculateSizeThatFits 与 layout 的实战用法
移动开发UI组件【免费下载链接】TextureSmooth asynchronous user interfaces for iOS apps.项目地址https://gitcode.com/gh_mirrors/te/Texture点击查看免费下载TextureAsyncDisplayKit以自动布局Layout Specs闻名但并非所有场景都适合用它。当你的节点层级复杂、需要精确控制子节点坐标或希望保留 UIKit 时代的手写布局习惯时Texture 依然提供了完整的手动布局Manual Layout能力。本文以 layout2-manual-layout.md 为主线从 UIKit 传统布局的痛点出发逐步讲解 Texture 手动布局的核心方法calculateSizeThatFits:与layout的用法、线程模型与缓存机制并结合源码深入剖析其背后的测量缓存原理。一、为什么还要手动布局UIKit 布局方式的两大痛点在进入 Texture 的 API 之前先回顾 UIKit 中自定义视图的经典做法。一个同时容纳文本视图与图片视图的自定义UIView通常需要实现两个方法-sizeThatFits:计算视图在给定尺寸约束下应有的内容尺寸-layoutSubviews按计算好的尺寸为子视图设置 frame。一个典型的实现如下Objective-C- (CGSize)sizeThatFits:(CGSize)size { // size the image CGSize imageSize [_imageView sizeThatFits:size]; // size the text view CGSize maxTextSize CGSizeMake(size.width - imageSize.width, size.height); CGSize textSize [_textView sizeThatFits:maxTextSize]; // make sure everything fits CGFloat minHeight MAX(imageSize.height, textSize.height); return CGSizeMake(size.width, minHeight); } - (void)layoutSubviews { CGSize size self.bounds.size; // convenience // size and layout the image CGSize imageSize [_imageView sizeThatFits:size]; _imageView.frame CGRectMake(size.width - imageSize.width, 0.0f, imageSize.width, imageSize.height); // size and layout the text view CGSize maxTextSize CGSizeMake(size.width - imageSize.width, size.height); CGSize textSize [_textView sizeThatFits:maxTextSize]; _textView.frame (CGRect){ CGPointZero, textSize }; }这段代码存在两个明显问题1. 重复测量double sizing子视图在sizeThatFits:中测了一次在layoutSubviews中又测了一次。虽然这里的算术足够简单廉价但一旦换成文本排版这类昂贵操作代价会被放大。2. 主线程阻塞sizeThatFits:与layoutSubviews都运行在主线程。而 UILabel、UITextView 的尺寸计算成本很高它们会拖慢滚动帧率、造成卡顿。直观的改进方案是缓存子视图尺寸比如引入_imageSize、_textSize两个 ivar。但正如原文档指出的缓存方案很快会失控——文本内容一旦变化就必须重新计算尺寸随之而来的是大量样板代码与状态同步负担。更激进的方案是借助dispatch_async()把测量搬到后台线程。然而 UIKit 官方文档明确要求视图操作必须发生在主线程Manipulations to your application’s user interface must occur on the main thread. Thus, you should always call the methods of the UIView class from code running in the main thread of your application. The only time this may not be strictly necessary is when creating the view object itself but all other manipulations should occur on the main thread.于是有人尝试绕开 UILabel / UITextView手工搭建 TextKit 排版栈在后台完成测量。这几乎是把系统排版引擎重写一遍——工作量巨大且一旦 iOS 更新改变 UITextView 的排版行为这些手工代码就会失效。更何况TextKit 本身也不是线程安全的。这正是 Texture 手动布局 API 存在的理由。二、Texture 手动布局的核心两个方法两套线程Texture 手动布局由ASDisplayNode的两个可覆写方法构成二者分工明确分别运行在不同线程上。calculateSizeThatFits:后台线程完成昂贵测量- [ASDisplayNode calculateSizeThatFits:]这是测量阶段。子类需要基于传入的constrainedSize约束尺寸返回节点应有的固有内容尺寸。该方法在后台线程调用因此适合在其中执行昂贵的尺寸计算——这正是解决 UIKit 主线程阻塞痛点的关键。从源码注释可以确认这一点。ASDisplayNodeSubclasses.h 中明确说明Subclasses that override should expect this method to be called on a non-main thread.并且源码特别强调覆写该方法即意味着节点承诺使用手动布局因此必须同时覆写-layout来布局所有子节点或子视图。同时该方法不应被外部直接调用测量入口应使用-layoutThatFits:或-layoutThatFits:parentSize:。layout主线程只做搬运工作- [ASDisplayNode layout]这是布局阶段由视图的-layoutSubviews触发在主线程调用。按照设计意图这里应当只做最少量的工作——利用测量阶段缓存的尺寸为各个子节点设置 frame不再执行任何昂贵的测量。在 ASDisplayNodeSubclasses.h 中-layout被描述为由视图的 layoutSubviews 在主线程调用子类覆写它以布局所有子节点或子视图。原文档还补充说明在测量与布局之后可以在layout中对那些不参与自动布局系统、未被layoutSpecThatFits:引用的节点做进一步布局。线程模型小结方法运行线程职责成本calculateSizeThatFits:后台线程测量子节点、缓存中间结果昂贵操作都放这里layout主线程利用缓存尺寸设置 frame尽量轻量这套分工与 UIKit 的最大区别在于测量不再占用主线程且测量结果被缓存layout阶段可以直接复用。三、完整示例手动布局一个图片 文本节点原文档给出了一个完整的 Objective-C 示例将第一节的 UIKit 视图改写为 Texture 节点。需要引入子类扩展头文件#import AsyncDisplayKit/AsyncDisplayKitSubclasses.h节点实现如下// perform expensive sizing operations on a background thread - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { // size the image CGSize imageSize [_imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; // size the text node CGSize maxTextSize CGSizeMake(constrainedSize.width - imageSize.width, constrainedSize.height); CGSize textSize [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, maxTextSize)].size; // make sure everything fits CGFloat minHeight MAX(imageSize.height, textSize.height); return CGSizeMake(constrainedSize.width, minHeight); } // do as little work as possible in main-thread layout - (void)layout { // layout the image using its cached size CGSize imageSize _imageNode.calculatedSize; _imageNode.frame CGRectMake(self.bounds.size.width - imageSize.width, 0.0f, imageSize.width, imageSize.height); // layout the text view using its cached size CGSize textSize _textNode.calculatedSize; _textNode.frame (CGRect){ CGPointZero, textSize }; }逐段拆解这段代码测量阶段后台线程通过ASSizeRangeMake(CGSizeZero, constrainedSize)构造一个从零到约束尺寸的ASSizeRange交给_imageNode layoutThatFits:用剩余宽度作为文本节点的最大约束再次调用layoutThatFits:取两者高度最大值作为整节点高度返回。布局阶段主线程直接读取_imageNode.calculatedSize与_textNode.calculatedSize——这些尺寸已经在测量阶段缓存无需再次计算镜像位于右侧文本占据左侧剩余空间与 UIKit 版本布局完全一致。关于ASSizeRange它由min与max两个CGSize组成见 ASLayout.h 与 ASLayoutElement.h用于描述至少多大、至多多大的约束区间。ASSizeRangeMake(CGSizeZero, constrainedSize)表示允许尺寸从 0 到约束上限之间自由取值。四、layoutThatFits: 与 calculatedSize测量缓存的底层原理-layoutThatFits:是理解手动布局的关键。它类似于-sizeThatFits:但带有副作用它会缓存测量结果供后续快速访问。这正是-layout可以瞬间完成的原因。缓存结果通过两个只读属性暴露calculatedSizeCGSize最近一次测量得到的尺寸calculatedLayoutASLayout最近一次测量得到的完整布局对象它对手动布局的节点包装了calculateSizeThatFits:返回的尺寸。在 ASDisplayNodeSubclasses.h 中对calculatedLayout的说明值得细读只有对节点调用过-layoutThatFits:calculatedLayout才会被设置手动布局模式下你必须在-calculateSizeThatFits:的实现里对子节点调用-layoutThatFits:对于使用自动布局layoutSpecThatFits:的节点ASLayoutSpec 会自动对所有子节点调用-layoutThatFits:基类的-layout实现也会自动利用子节点的calculatedLayout通常无需手动访问该属性。所以对图片 文本示例而言-layout中读取的_imageNode.calculatedSize、_textNode.calculatedSize正是测量阶段-layoutThatFits:写入缓存的成果。缓存触发条件的细节-layoutThatFits:机制只有在需要新的测量时才会真正调用calculateSizeThatFits:。原文档明确列举了两个条件约束尺寸constrained size发生变化节点没有实现layoutSpecThatFits:即处于手动布局模式。这一点在源码层面也有印证ASDisplayNode.mm 中calculateLayoutThatFits:restrictedToSize:relativeToParentSize:会先解析节点的style.size与父尺寸将结果与传入约束求交集ASSizeRangeIntersect再分发到实际的计算方法。而 ASDisplayNodeSubclasses.h 则说明默认的calculateLayoutThatFits:会依据子类覆写情况调用layoutSpecThatFits:或calculateSizeThatFits:二者之一。从源码结构看缓存机制由布局版本号layout version驱动ASDisplayNode.mm 中invalidateCalculatedLayout会递增_layoutVersion、清空_unflattenedLayout从而标记旧缓存失效下一次测量才会重新计算。五、失效与缓存维护invalidateCalculatedSize 的三种场景手动布局节点在测量阶段缓存了尺寸随之而来的问题是内容变了怎么办原文档给出的答案是Nodes should call[self invalidateCalculatedSize]when necessary并列举了三条编写手动布局节点时必须牢记的准则1. 递归测量所有子节点calculateSizeThatFits:必须递归地测量全部子节点通过-layoutThatFits:完成。如上文所述-layoutThatFits:机制只会在需要新测量时约束变化、未实现layoutSpecThatFits:才调用-calculateSizeThatFits:。2. 昂贵预计算放入测量阶段缓存中间结果任何其他昂贵的预布局计算都应放在-calculateSizeThatFits:中完成并把有用的中间结果缓存在 ivar 里供-layout阶段复用。3. 必要时调用失效方法当节点内容发生变化、旧尺寸不再有效时必须主动使缓存失效。原文档举例ASTextNode在其attributedString属性改变时会自动失效计算尺寸。需要指出的是源码中的实际方法名是-invalidateCalculatedLayout见 ASDisplayNodeSubclasses.h 的声明原文档写作invalidateCalculatedSize是早期版本的命名。两者含义相同使节点此前测量并缓存的布局失效强制下一次测量重新计算。其实现位于 ASDisplayNode.mm核心动作是递增布局版本号并清空未扁平化布局缓存。另外 ASDisplayNode.mm 显示setNeedsLayout内部也会调用invalidateCalculatedLayout。仓库中真实调用失效方法的例子可参考 ASEditableTextNode.mm当UITextView的文本内容变化textViewDidChange:时会调用[self invalidateCalculatedLayout]注释明确写着Invalidate, as our calculated size depends on the textviews seeded text——这正是原文档所述准则在真实组件中的落地。六、手动布局 vs 自动布局何时选择哪一种原文档在结尾明确给出立场自动布局优先应作为大多数场景的首选手动布局仅在特殊需求下使用。结合仓库源码可以补充两者的边界自动布局Layout Specs实现layoutSpecThatFits:或提供layoutSpecBlock声明式描述布局。ASLayoutSpec 自动测量子节点、基类-layout自动利用缓存布局你几乎不需要手写 frame。ASDisplayNodeSubclasses.h 明确指出自动布局节点通常无需访问calculatedLayout。手动布局Manual Layout覆写calculateSizeThatFits:与layout命令式地测量与摆放。适合精确控制坐标、需要复用既有 UIKit 布局思路、或对布局过程有特殊要求的场景。值得注意的约束子类必须在layoutSpecThatFits:与calculateSizeThatFits:中至多覆写其一。源码中有一处断言会检查这一点见 ASDisplayNodeSubclasses.h若子类两种方法都覆写会触发如下断言提示Subclass % must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:这意味着手动布局 自动布局两种模式是互斥的选择一种就要贯彻到底。七、总结Texture 的手动布局把 UIKit 的测量 布局两段式流程提升到了新的层次calculateSizeThatFits:在后台线程执行昂贵的测量彻底摆脱主线程阻塞layout在主线程只做最轻量的 frame 设置layoutThatFits:缓存测量结果calculatedSize/calculatedLayout让布局阶段零成本复用内容变化时通过invalidateCalculatedLayout主动失效缓存保证尺寸始终新鲜。这套 API 与自动布局Layout Specs共用同一套测量基础设施只不过把声明式描述换成了命令式计算。原文档的立场依然成立能自动布局就自动布局但当你需要精确控制、或者手头有一段成熟的 UIKit 布局逻辑需要迁移时手动布局是一条完全可行、且线程安全的后路。赞分享移动开发UI组件【免费下载链接】TextureSmooth asynchronous user interfaces for iOS apps.项目地址https://gitcode.com/gh_mirrors/te/Texture点击查看免费下载相关推荐G6 布局 API 完全指南setLayout / getLayout / layout / stopLayout 与布局配置实战G6 布局 API 完全指南setLayout / getLayout / layout / stopLayout 与布局配置实战 G6 antv/g6数据可视化前端图表库G6 布局 API 完全指南setLayout / getLayout / layout / stopLayout 与组合布局实战G6 布局 API 完全指南setLayout / getLayout / layout / stopLayout 与组合布局实战 导读 本文围绕 G6An数据可视化前端图表库Vuetify 应用布局系统Application Layout完全指南outside-in 原理、组件排序与动态布局实战Vuetify 应用布局系统Application Layout完全指南outside in 原理、组件排序与动态布局实战 Vuetify 内置了一套名为前端UI组件上一篇Beads 的 bd human 命令实战指南让人工介入点一目了然下一篇5分钟跑通eSearch离线OCR古籍竖排文字识别新手教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。