news 2026/9/26 3:14:58

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Texture 手动布局完全指南:calculateSizeThatFits 与 layout 的实战用法
  • 移动开发
  • UI组件

【免费下载链接】Texture

Smooth asynchronous user interfaces for iOS apps.

项目地址:https://gitcode.com/gh_mirrors/te/Texture
点击查看免费下载

Texture(AsyncDisplayKit)以自动布局(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 主线程阻塞痛点的关键。

从源码注释可以确认这一点。ASDisplayNode+Subclasses.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,不再执行任何昂贵的测量。

在 ASDisplayNode+Subclasses.h 中,-layout被描述为"由视图的 layoutSubviews 在主线程调用,子类覆写它以布局所有子节点或子视图"。原文档还补充说明:在测量与布局之后,可以在layout中对那些不参与自动布局系统、未被layoutSpecThatFits:引用的节点做进一步布局。

线程模型小结

方法运行线程职责成本
calculateSizeThatFits:后台线程测量子节点、缓存中间结果昂贵操作都放这里
layout主线程利用缓存尺寸设置 frame尽量轻量

这套分工与 UIKit 的最大区别在于:测量不再占用主线程,且测量结果被缓存,layout阶段可以直接复用。

三、完整示例:手动布局一个"图片 + 文本"节点

原文档给出了一个完整的 Objective-C 示例,将第一节的 UIKit 视图改写为 Texture 节点。需要引入子类扩展头文件:

#import <AsyncDisplayKit/AsyncDisplayKit+Subclasses.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可以瞬间完成的原因。

缓存结果通过两个只读属性暴露:

  • calculatedSize(CGSize):最近一次测量得到的尺寸;
  • calculatedLayout(ASLayout):最近一次测量得到的完整布局对象,它对手动布局的节点包装了calculateSizeThatFits:返回的尺寸。

在 ASDisplayNode+Subclasses.h 中,对calculatedLayout的说明值得细读:

  • 只有对节点调用过-layoutThatFits:,calculatedLayout才会被设置;
  • 手动布局模式下,你必须在-calculateSizeThatFits:的实现里对子节点调用-layoutThatFits:;
  • 对于使用自动布局(layoutSpecThatFits:)的节点,ASLayoutSpec 会自动对所有子节点调用-layoutThatFits:,基类的-layout实现也会自动利用子节点的calculatedLayout,通常无需手动访问该属性。

所以对"图片 + 文本"示例而言,-layout中读取的_imageNode.calculatedSize、_textNode.calculatedSize,正是测量阶段-layoutThatFits:写入缓存的成果。

缓存触发条件的细节:-layoutThatFits:机制只有在需要新的测量时才会真正调用calculateSizeThatFits:。原文档明确列举了两个条件:

  1. 约束尺寸(constrained size)发生变化;
  2. 节点没有实现layoutSpecThatFits:(即处于手动布局模式)。

这一点在源码层面也有印证:ASDisplayNode.mm 中calculateLayoutThatFits:restrictedToSize:relativeToParentSize:会先解析节点的style.size与父尺寸,将结果与传入约束求交集(ASSizeRangeIntersect),再分发到实际的计算方法。而 ASDisplayNode+Subclasses.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(见 ASDisplayNode+Subclasses.h 的声明),原文档写作invalidateCalculatedSize是早期版本的命名。两者含义相同:使节点此前测量并缓存的布局失效,强制下一次测量重新计算。其实现位于 ASDisplayNode.mm,核心动作是递增布局版本号并清空未扁平化布局缓存。另外 ASDisplayNode.mm 显示,setNeedsLayout内部也会调用invalidateCalculatedLayout。

仓库中真实调用失效方法的例子可参考 ASEditableTextNode.mm:当UITextView的文本内容变化(textViewDidChange:)时,会调用[self invalidateCalculatedLayout],注释明确写着"Invalidate, as our calculated size depends on the textview's seeded text"——这正是原文档所述准则在真实组件中的落地。

六、手动布局 vs 自动布局:何时选择哪一种

原文档在结尾明确给出立场:自动布局优先,应作为大多数场景的首选;手动布局仅在特殊需求下使用。结合仓库源码可以补充两者的边界:

  • 自动布局(Layout Specs):实现layoutSpecThatFits:(或提供layoutSpecBlock),声明式描述布局。ASLayoutSpec 自动测量子节点、基类-layout自动利用缓存布局,你几乎不需要手写 frame。ASDisplayNode+Subclasses.h 明确指出自动布局节点通常无需访问calculatedLayout。
  • 手动布局(Manual Layout):覆写calculateSizeThatFits:与layout,命令式地测量与摆放。适合精确控制坐标、需要复用既有 UIKit 布局思路、或对布局过程有特殊要求的场景。

值得注意的约束:子类必须在layoutSpecThatFits:与calculateSizeThatFits:中至多覆写其一。源码中有一处断言会检查这一点(见 ASDisplayNode+Subclasses.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组件

【免费下载链接】Texture

Smooth asynchronous user interfaces for iOS apps.

项目地址:https://gitcode.com/gh_mirrors/te/Texture
点击查看免费下载
上一篇:Beads 的 bd human 命令实战指南:让人工介入点一目了然
下一篇:5分钟跑通eSearch离线OCR:古籍竖排文字识别新手教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 3:13:37

Claude Code 模板实战:用可复用工作流提升 AI 编码的一致性与效率

说到底&#xff0c;Claude Code 这类 AI 编码工具本身已经不算新鲜了&#xff0c;真正让团队拉开效率差距的&#xff0c;是那些藏在 CLAUDE.md、slash command 和 agent 配置里的一套套模板。有人把 Claude Code 当成一次性聊天框&#xff0c;用完就忘&#xff1b;有人却把它当…

作者头像 李华
网站建设 2026/9/26 3:13:12

小林coding-agent面试篇

目录 1Agent 推理模式有哪些&#xff1f;ReAct 是啥&#xff1f;具体是怎么实现的&#xff1f; 2什么是推理模式&#xff1f; 3ReAct、Plan-and-Execute、Reflection 三种范式有什么核心区别&#xff1f;实际项目中该如何选型&#xff1f; 4设计范式和推理模式的区别&#…

作者头像 李华
网站建设 2026/9/26 3:13:09

汽车故障时间与类别预测实战 从 Kaggle 结构化赛题理解预测性维护建模

这个 Kaggle 赛题表面上是入门练习,实际对应的是典型的车辆故障预测场景。任务核心并非单纯做一次分类提交,而是从结构化运行数据中识别故障发生规律,判断故障类型,并尽量贴近故障发生时点,这与车队运维、售后预警和预测性维护中的真实问题高度一致。 文章围绕赛题理解、…

作者头像 李华
网站建设 2026/9/26 3:13:08

莫斯科公寓价格预测实战 从 Kaggle 房价回归题理解结构化建模

这道 Kaggle 赛题表面上是一道入门级房价预测题,实际很适合用来训练结构化数据项目中的关键能力。任务目标是根据房屋属性估计莫斯科公寓价格,评价采用 RMSLE,建模重点自然落在长尾分布处理、相对误差控制和稳定验证流程上,而不是单纯追求复杂模型。 更重要的是,这类题目…

作者头像 李华
网站建设 2026/9/26 3:12:51

ArcMap 批量掩膜效率翻倍:用 TaoToken 统一 Key 打通 arcpy 脚本配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华