☰
Texture 占位图(Placeholder)机制完全指南:placeholderFadeDuration、placeholderImage 与 ASNetworkImageNode 默认图实践
2026/9/26 15:39:23 网站建设 项目流程
  • 移动开发
  • UI组件

【免费下载链接】Texture

Smooth asynchronous user interfaces for iOS apps.

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

本文面向使用 AsyncDisplayKit/Texture 的 iOS 开发者,系统讲解节点占位图(Placeholder)机制的完整用法:从ASDisplayNode的placeholderImage子类钩子、placeholderEnabled与placeholderFadeDuration的配置语义,到ASNetworkImageNode.defaultImage的持久默认图,再到neverShowPlaceholders的同步显示策略。读完你将能独立实现线程安全、可淡出的占位图,并正确区分"瞬态占位图"与"持久默认图"两种设计。

为什么需要占位图:异步渲染的"空窗期"

Texture 的核心卖点是异步渲染:节点的内容(图片、文本、图层内容)通常不在主线程同步绘制,而是在后台线程完成后再提交。这就产生了一个问题——从节点被布局、进入视图层级,到内容真正显示出来,之间存在一段"空窗期"。如果不做任何处理,用户看到的将是一块空白区域,滚动时甚至会出现明显的"闪白"。

占位图(Placeholder)正是为填补这段空窗期而生的机制:它是一张覆盖在节点内容之上的图片,在节点内容尚未完成显示时充当"骨架",内容一旦就绪便立即淡出,给用户一种内容"流畅浮现"的观感。这一概念最早由 Facebook Paper 团队提出,Texture 仓库中的 Placeholders 示例工程 完整演示了该机制的落地实现。

占位图核心 API 与实现原理

任何ASDisplayNode子类都可以通过实现-placeholderImage方法来提供占位图。其使用分两步:

  1. 开启占位图能力:设置.placeholderEnabled = YES;
  2. (可选)配置淡出时长:设置.placeholderFadeDuration。

这两个属性都定义在 ASDisplayNode.h 中:

  • placeholderEnabled:是否在节点内容显示完成前,用占位图覆盖节点。默认值为 NO。
  • placeholderFadeDuration:节点内容显示完成后,占位图淡出所花费的时间(NSTimeInterval,单位秒)。默认值为 0 秒,即内容就绪时占位图立即移除、不做淡出动画。

在 ASDisplayNode.mm 中,基类给出了placeholderImage的默认实现:

- (UIImage *)placeholderImage { // Subclass hook return nil; }

也就是说,基类默认不生成任何占位图。子类需要覆写此方法,并返回一张与实际内容尺寸匹配的图片。

底层调用链:占位图层如何被创建与销毁

从源码看,占位图并非直接画在节点上,而是通过一个独立的CALayer(_placeholderLayer)叠加实现,其关键逻辑集中在 ASDisplayNode.mm:

  • _locked_shouldHavePlaceholderLayer:判断是否需要占位图层,条件是placeholderEnabled == YES且节点确实实现了显示流程(_implementsDisplay,即节点实现了drawRect、displayWithParameters:或栅格化子树三者之一,见 ASDisplayNode.mm)。这意味着"纯容器节点"(自身不绘制内容)开启placeholderEnabled是无效的。
  • _locked_setupPlaceholderLayerIfNeeded:创建_placeholderLayer,并将其zPosition设为9999.0(代码注释明确说明:不能设为CGFLOAT_MAX,以免需要其他内容覆盖在占位图之上时被永久遮挡)。随后懒加载调用[self placeholderImage]获取占位图,若图片带capInsets(可拉伸图片),走ASDisplayNodeSetResizableContents拉伸填充,否则直接以节点的contentsScale设置contents。
  • 占位图尺寸:通过_placeholderLayer.frame = self.threadSafeBounds时刻与节点 bounds 保持一致(ASDisplayNode.mm),因此绘制占位图时应使用节点的calculatedSize作为画布尺寸。

淡出动画的执行条件

占位图销毁逻辑位于 ASDisplayNode.mm:当整个层级显示完成(_pendingDisplayNodes为空)后,若占位图层仍存在且不应持久保留,则执行清理。淡出动画有两个触发前提:

if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { [CATransaction begin]; [CATransaction setCompletionBlock:cleanupBlock]; [CATransaction setAnimationDuration:_placeholderFadeDuration]; _placeholderLayer.opacity = 0.0; [CATransaction commit]; } else { cleanupBlock(); }

即:placeholderFadeDuration > 0且节点处于可见 interface state 时,才执行CATransaction淡出动画;否则占位图被直接移除。另外,调用clearContents会同时清空_placeholderLayer.contents并释放缓存的_placeholderImage(ASDisplayNode.mm),便于在内存紧张时回收资源。

实战一:实现自定义占位图

使用calculatedSize绘制占位图

占位图需要与节点最终显示尺寸一致,因此官方文档明确要求:图像绘制时使用节点的.calculatedSize属性(即测量阶段确定的最终尺寸)。下面以仓库中的 Placeholders 示例 为例,该示例通过usleep人为制造 0.5 秒的绘制延迟,模拟慢速内容加载场景:

@implementation SlowpokeImageNode - (instancetype)init { if (self = [super init]) { self.placeholderEnabled = YES; self.placeholderFadeDuration = 0.1; } return self; } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { if (constrainedSize.width > 0.0) { return CGSizeMake(constrainedSize.width, constrainedSize.width / kASDKLogoAspectRatio); } else if (constrainedSize.height > 0.0) { return CGSizeMake(constrainedSize.height * kASDKLogoAspectRatio, constrainedSize.height); } return CGSizeZero; } - (UIImage *)placeholderImage { CGSize size = self.calculatedSize; if (CGSizeEqualToSize(size, CGSizeZero)) { return nil; } UIGraphicsBeginImageContext(size); [[UIColor whiteColor] setFill]; [[UIColor colorWithWhite:0.9 alpha:1] setStroke]; UIRectFill((CGRect){CGPointZero, size}); UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointZero]; [path addLineToPoint:(CGPoint){size.width, size.height}]; [path stroke]; [path moveToPoint:(CGPoint){size.width, 0.0}]; [path addLineToPoint:(CGPoint){0.0, size.height}]; [path stroke]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } @end

关键点拆解:

  • 在init中开启placeholderEnabled并设置placeholderFadeDuration = 0.1,节点一进入层级就开始显示占位图;
  • placeholderImage中先检查calculatedSize是否为零,避免在尺寸未知时生成无效图片;
  • 利用UIGraphicsBeginImageContext绘制白色底 + 灰色对角线的"加载骨架"样式,作为内容未就绪时的视觉提示。

线程安全:不要在占位图中使用imageNamed:

官方文档中有一处极易踩坑的警告:-placeholderImage方法可能在后台线程被调用,因此该方法必须是线程安全的。特别要注意,使用 image assets 时-[UIImage imageNamed:]不是线程安全的,应当改用-[UIImage imageWithContentsOfFile:]:

- (UIImage *)placeholderImage { // ❌ 错误:imageNamed: 在后台线程调用存在崩溃风险 // return [UIImage imageNamed:@"placeholder"]; // ✅ 正确:imageWithContentsOfFile: 是线程安全的 NSString *path = [[NSBundle mainBundle] pathForResource:@"placeholder" ofType:@"png"]; return [UIImage imageWithContentsOfFile:path]; }

如果占位图需要按 trait collection 区分,Texture 在 UIImage+ASConvenience.mm 中提供了线程安全的as_imageNamed:系列方法(内部使用NSCache缓存,不依赖imageNamed:的全局缓存),可作为替代方案。

用 UIImage+ASConvenience 快速生成占位图

对于最常见的两种占位图需求——圆角纯色块和直角方块——官方文档推荐直接使用UIImage+ASConvenience分类,无需手写绘制代码。核心 API 定义在 UIImage+ASConvenience.mm:

+ (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius cornerColor:(UIColor *)cornerColor fillColor:(UIColor *)fillColor borderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth roundedCorners:(UIRectCorner)roundedCorners scale:(CGFloat)scale;

参数说明(源码层面的实现细节):

  • cornerRadius:圆角半径,作为可拉伸图片的capInsets依据(内部按(cornerRadius * 2) + 1生成基础尺寸,见 UIImage+ASConvenience.mm);
  • cornerColor:圆角区域填充色,若传入[UIColor clearColor]会被当作"无背景色"处理(源码在 UIImage+ASConvenience.mm 有显式判断);
  • fillColor:主体填充色,不透明时使用kCGBlendModeCopy混合模式加速绘制;
  • borderColor/borderWidth:可选描边,描边路径会向内缩进borderWidth / 2,确保描边完全落在填充区域内;
  • roundedCorners:控制哪些角需要圆角(UIRectCorner位掩码);
  • scale:输出图片的 scale,传0.0表示使用当前屏幕 scale(各便捷重载默认值)。

该方法返回可拉伸图片(resizableImageWithCapInsets:resizingMode:),绘制圆角头像、卡片占位等场景时既能保持圆角,又能任意拉伸。示例用法:

- (UIImage *)placeholderImage { return [UIImage as_resizableRoundedImageWithCornerRadius:8.0 cornerColor:nil fillColor:[UIColor colorWithWhite:0.95 alpha:1.0] borderColor:nil borderWidth:1.0]; }

内置节点的占位图行为

ASImageNode:占位色与默认占位实现

ASImageNode已经内置了一套占位图机制:它覆写了placeholderImage,用placeholderColor填充整个calculatedSize区域(见 ASImageNode.mm),默认占位色由ASDisplayNodeDefaultPlaceholderColor()提供——一种white:0.95的浅灰色(见 ASDisplayNodeExtras.mm)。

placeholderColor属性定义在 ASImageNode.h 中,且设置非 nil 的placeholderColor会自动开启placeholderEnabled(见 ASImageNode.mm 的 setter 实现)。因此图片节点最简单的占位方案是:

imageNode.placeholderEnabled = YES; imageNode.placeholderColor = [UIColor colorWithWhite:0.95 alpha:1.0]; imageNode.placeholderFadeDuration = 0.3;

这一用法在 Kittens 示例 中有真实体现:示例中图片节点设置placeholderFadeDuration = .5并使用ASDisplayNodeDefaultPlaceholderColor()作为占位色,加载期间显示纯色占位,加载完成后 0.5 秒淡出。类似的,ASTextNode与ASMapNode也使用ASDisplayNodeDefaultPlaceholderColor()作为默认背景色(分别见 ASTextNode.mm 与 ASMapNode.mm)。

ASNetworkImageNode:defaultImage 持久默认图

除了占位图,ASNetworkImageNode还提供.defaultImage属性。两者的本质区别在于生命周期:

  • 占位图(placeholder)是瞬态的:节点内容显示完成即被移除(或淡出),只服务于"加载空窗期";
  • 默认图(defaultImage)是持久的:只要节点的.URL为nil,或者 URL 加载失败,默认图就会一直显示。

defaultImage的定义与语义说明见 ASNetworkImageNode.h,其行为由 ASNetworkImageNode.mm 中的多处逻辑保证:

  • 设置defaultImage时立即显示(ASNetworkImageNode.mm);
  • 调用setURL:且 URL 为 nil(或需要重置)时,图像被替换回_defaultImage(ASNetworkImageNode.mm);
  • 取消下载、清空图像时同样回落到_defaultImage(ASNetworkImageNode.mm)。

官方文档给出的实践建议是:

头像(avatars)适合用defaultImage(用户缺失头像时持久显示一个默认头像);照片(photos)适合用placeholderImage(加载期间显示骨架,加载完成后淡出,避免遮挡真实内容)。

ASNetworkImageNode *avatarNode = [[ASNetworkImageNode alloc] init]; avatarNode.defaultImage = [UIImage imageNamed:@"default_avatar"]; avatarNode.URL = [NSURL URLWithString:@"https://example.com/avatar.png"]; // URL 为 nil 或加载失败时,一直显示 default_avatar

进阶:neverShowPlaceholders 同步显示策略

占位图解决了"空白闪烁",但某些场景下我们希望内容第一时间就以最终形态呈现,完全跳过占位图。Texture 为此提供了neverShowPlaceholders开关,分别定义在 ASCellNode.h 与 ASDKViewController.h。

其原理是强制节点同步(阻塞当前线程)完成整个子树的显示:

  • 在ASCellNode中,进入可见状态时若neverShowPlaceholders == YES,会调用recursivelyEnsureDisplaySynchronously:同步渲染整个节点树(见 ASCellNode.mm);
  • 在ASDKViewController中,viewDidLayoutSubviews阶段会执行同样的同步显示(见 ASDKViewController.mm),并通过ensureDisplayed保证布局完成后再触发;
  • 与之配套的ensureDisplayed概念在 ASDisplayNode+Beta.h 的注释中有说明:启用ASCellNode.neverShowPlaceholders后,导航操作会被阻塞在重显示(redisplay)完成之前,从而保证新页面出现时内容已完整就绪。

适用场景与代价:neverShowPlaceholders适合对内容即时性要求极高、且内容渲染成本可控的场景(如关键页面首屏、转场目标页)。代价是同步渲染会占用主线程时间——如果内容很重,可能造成卡顿,因此需要在"瞬时占位体验"与"同步渲染耗时"之间权衡。一般不建议对包含大量图片、复杂布局的列表开启此选项。

占位图与默认图的选择决策

维度placeholderImage / placeholderColordefaultImage(ASNetworkImageNode)
本质瞬态:内容就绪即移除/淡出持久:URL 为 nil 或加载失败时持续显示
生命周期跟随节点显示流程跟随 URL / 下载状态
典型用途照片加载骨架、纯色/圆角色块头像缺省图、错误兜底图
关键配置placeholderEnabled+placeholderFadeDuration.defaultImage+.URL
线程要求placeholderImage实现必须线程安全无特殊要求(赋值在主线程进行)
底层实现独立_placeholderLayer叠加,zPosition 9999直接作为_image的回落值

一句话总结:占位图解决"加载过程"的观感,默认图解决"没有内容"的兜底。两者可以同时使用——例如头像节点既设置defaultImage兜底缺图,又开启placeholderEnabled让加载期间有骨架过渡。

扩展阅读

  • 官方文档原文:placeholder-fade-duration.md
  • 完整可运行示例:Placeholders 示例工程,含SlowpokeImageNode.m的慢速加载与占位图绘制实现
  • 占位图核心源码:ASDisplayNode.mm(占位图层生命周期)、ASImageNode.mm(内置纯色占位)、ASNetworkImageNode.mm(默认图回落逻辑)
  • 占位图生成工具:UIImage+ASConvenience.mm(可拉伸圆角图片)、ASDisplayNodeExtras.mm(默认占位色)
  • 同步显示策略:ASCellNode.mm、ASDKViewController.mm
  • 真实使用案例:Kittens 示例(图片节点占位色 + 0.5s 淡出)、CatDealsCollectionView 示例
  • 移动开发
  • UI组件

【免费下载链接】Texture

Smooth asynchronous user interfaces for iOS apps.

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

相关推荐

上一篇:最直观的U-Net模型解析:用Netron实现动态图可视化全流程
下一篇:解决ScrapeGraphAI中Playwright常见错误:从安装到高级调试指南

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询