☰
基于 NukeUI 的 LazyImageView 完全指南:UIKit/AppKit 异步图片加载视图的 API 与内部实现解析
2026/9/25 2:32:21 网站建设 项目流程
  • 移动开发
  • 图像处理

【免费下载链接】Nuke

Image loading system

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

LazyImageView是 NukeUI 为 UIKit 和 AppKit 提供的「懒加载」图片视图,它是 SwiftUI 侧LazyImage的对应物,用一套等价 API 将ImagePipeline的缓存、预取、任务合并、渐进式解码与请求优先级能力带到命令式 UI 中。本文以 NukeUI 官方文档(Documentation/NukeUI.docc/Extensions/LazyImageView-Extensions.md)的 API 清单为主线,结合 LazyImageView.swift 源码与 LazyImageViewTests.swift、LazyImageViewLifecycleTests.swift 测试用例,逐项解析其初始化、加载、占位/失败视图、回调与生命周期行为,读完即可在自己的 UIKit/AppKit 项目中直接落地使用。

LazyImageView 是什么:NukeUI 中 LazyImage 的 UIKit/AppKit 对应物

NukeUI 模块提供两种核心视图(见 NukeUI.md):

  • LazyImage——SwiftUI 视图;
  • LazyImageView——UIKit / AppKit 视图。

LazyImageView是LazyImage的命令式 UI 对应版本,拥有等价的 API 集合。与直接对UIImageView调用loadImage(with:options:into:)全局函数(原 NukeExtensions 模块,Nuke 14 起并入 NukeUI)不同,加载状态的所有权归属不同:扩展函数把占位图、失败图和过渡动画作为参数随每次调用传入,由调用方管理;而LazyImageView自己持有这些状态——占位视图、失败视图、过渡动画都在视图上配置一次,之后只需赋一个url即可(参见 Demo/Essentials/UIKitViewsDemo.swift 中两种方式的对比说明)。

从源码看(Sources/NukeUI/LazyImageView.swift第 29-30 行),类声明为@MainActor public final class,且整个文件被#if !os(watchOS)包裹,因此在 iOS、tvOS、macOS、visionOS 上可用,watchOS 不可用;内部根据平台条件编译选择UIKit或AppKit类型。

官方文档给出的最小使用示例(源码注释中的用法):

let imageView = LazyImageView() imageView.placeholderView = UIActivityIndicatorView() imageView.priority = .high imageView.pipeline = customPipeline imageView.onCompletion = { _ in print("Request completed") } imageView.url = URL(string: "https://example.com/image.jpeg")

初始化:init(frame:)与init(coder:)

LazyImageView提供两个初始化入口,分别对应代码创建和从 storyboard/nib 解档:

  • init(frame: CGRect)——代码创建时使用;
  • init(coder: NSCoder)——视图从 storyboard 或 nib 解档时由系统调用。

两个初始化器最终都走到私有方法didInit()(源码 LazyImageView.swift),它完成三项关键配置:

  1. 创建并挂载内部AnimatedImageView(初始为isHidden = true),并调用pinToSuperview()使其铺满;
  2. 生成一个默认占位视图:一个背景色为secondarySystemBackground的普通平台视图(macOS 上设置 layer 背景色,iOS 上设置backgroundColor);
  3. 设置默认过渡动画transition = .fadeIn(duration: 0.33)。

测试 LazyImageViewLifecycleTests.swift 中的defaultConfiguration完整验证了这些默认值:过渡动画为fadeIn(0.33)、placeholderViewPosition == .fill、failureViewPosition == .fill、showPlaceholderOnFailure == false、isProgressiveImageRenderingEnabled == true、isResetEnabled == true、priority == nil、processors == nil、request == nil、imageTask == nil,且默认占位视图已作为子视图加入层级、处于可见状态。LazyImageViewTests.swift 还通过NSKeyedArchiver/NSKeyedUnarchiver模拟从 nib 解档,验证 coder 路径下视图配置完整。

发起与取消加载:url、request、cancel()、reset()

url 与 request:赋值即开始加载

  • url: URL?——便捷属性,setter 内部把 URL 包装成ImageRequest(url:)再赋给request;赋nil等价于request = nil(源码 LazyImageView.swift)。
  • request: ImageRequest?——核心入口,didSet直接触发load(request)(第 228-230 行),赋值后立即启动下载。用request可以附带处理器、优先级、选项等完整配置。

对应的行为测试:urlPropertyReflectsRequest 验证赋值url后request?.url同步反映;settingURLToNilClearsRequest(第 96-104 行)验证置空后两者均为 nil;nilRequestProducesFailure(第 67-83 行)验证request = nil会以ImagePipeline.Error.imageRequestMissing失败结束且不触发任何网络任务。

load 内部流程:缓存优先,重置、占位、任务一气呵成

私有方法load(_:)(源码 LazyImageView.swift)展示了完整的加载编排:

  1. 断言必须在主线程调用;
  2. cancel()取消当前任务;
  3. 若请求为 nil,执行重置并以.imageRequestMissing失败;
  4. 若视图级设置了processors且请求自身未带处理器,则把视图级处理器写入请求;若设置了priority,同样写入请求;
  5. 先查内存缓存:pipeline.cache[request]命中且非预览(!image.isPreview)时,跳过重置直接同步显示,构造cacheType: .memory的ImageResponse并走成功回调,不启动任何 pipeline 任务——这正是「内存命中同步显示」的根源;
  6. 否则执行重置、隐藏占位视图逻辑,随后调用pipeline.loadImage(with:progress:completion:)启动真实加载,把返回的ImageTask存入imageTask并触发onStart。

测试佐证:memoryCacheHitDisplaysImageSynchronously 验证命中后imageTask == nil且图片已显示;memoryCacheHitReportsCacheTypeMemory(第 133-143 行)验证响应cacheType == .memory;memoryCacheHitCompletesSynchronouslyWithoutStarting 验证事件序列仅为["success", "completion"],onStart不触发、dataLoader.createdTaskCount == 0。

cancel() 与 reset():任务取消与视图复位

  • cancel()(源码第 271-274 行):调用imageTask?.cancel()并置 nil,不清理已显示的图片。适合需要中止请求但保留当前内容的场景。
  • reset()(源码第 240-260 行):取消当前请求,清除图片(对imageView调用prepareForReuse()并隐藏),移除自定义图片视图,隐藏占位与失败视图——即「取消并准备复用」的完整动作。deinit中也会自动调用imageTask?.cancel()(第 180-182 行),视图释放即取消请求。

测试覆盖:cancelClearsImageTask(LazyImageViewTests.swift)、resetCancelsAndClearsImage(第 380-394 行)、newRequestCancelsPreviousTask(第 396-406 行,赋值新 url 自动取消旧任务)、viewDeallocCancelsTask(第 408-421 行)、resetWhileLoadingCancelsRequestAndHidesPlaceholder(LazyImageViewLifecycleTests.swift)、cancelledRequestDeliversNoCallbacks(第 324-348 行,取消后所有回调零触发)。

请求选项:priority、processors、pipeline

这三个属性用于覆盖请求默认行为:

  • priority: ImageRequest.Priority?——默认 nil。支持动态修改:didSet中直接调用imageTask?.priority = priority,对已在途任务即时生效(源码 LazyImageView.swift)。测试 priorityAppliedOnStart 与priorityChangedDynamically(第 437-448 行)分别验证了启动前设置与加载中动态改优先级均生效。
  • processors: [any ImageProcessing]?——默认 nil。视图级处理器只会在请求自身request.processors.isEmpty时写入;请求自带处理器时以请求的为准(源码第 290-292 行)。测试 processorsFromRequestTakePrecedenceOverViewProcessors 验证请求携带的p2覆盖视图级p1;viewProcessorsAreUsedForMemoryCacheLookup(LazyImageViewLifecycleTests.swift)验证视图级处理器同样参与内存缓存键查找。
  • pipeline: ImagePipeline——默认.shared。可替换为自定义 pipeline(如配置了不同dataLoader或缓存策略的实例)。测试customPipelineUsed(LazyImageViewTests.swift)验证请求确实走自定义 pipeline 而非共享实例。

显示状态体系:占位视图、失败视图与过渡动画

LazyImageView最大的特点是把「加载中/失败/成功」三种显示状态内置为属性。

占位视图:placeholderImage与placeholderView

  • placeholderImage: PlatformImage?——占位图片。setter 内部将其包装成_PlatformImageView再赋给placeholderView,即图片最终也是以视图形式呈现(源码 LazyImageView.swift)。
  • placeholderView: _PlatformBaseView?——占位视图,典型用法是UIActivityIndicatorView转圈指示器。设置时旧视图被移除、新视图插入到最底层(insertSubview(newView, at: 0)),并在 iOS/tvOS/visionOS 上自动对UIActivityIndicatorView调用startAnimating()(第 414-428 行)。
  • placeholderViewPosition: SubviewPosition——占位视图的位置,默认.fill(铺满父视图)。SubviewPosition枚举只有两个成员(第 494-500 行):.fill生成四条约束,.center仅生成 centerX/centerY 两条约束;它同样影响placeholderImage(因为图片被转成视图)。
  • showPlaceholderOnFailure: Bool——默认 false。置为 true 时,加载失败后不显示失败视图,而是继续显示占位视图(第 56-58 行、handle(result:)第 352-357 行)。

测试覆盖:placeholderViewVisibleDuringLoad、placeholderHiddenAfterSuccess(第 249-259 行)、placeholderImageWrapsInImageView(第 275-278 行)、activityIndicatorPlaceholderStartsAnimating(第 294-303 行)、placeholderViewFillsTheViewByDefault(默认 4 条约束)与placeholderViewIsCenteredWhenPositionIsCenter(第 799-810 行)。

失败视图:failureImage与failureView

  • failureImage: PlatformImage?——失败图片,setter 同样包装成平台 ImageView(源码 LazyImageView.swift)。
  • failureView: _PlatformBaseView?——失败视图,默认初始为隐藏,请求失败时显示(第 450-459 行)。
  • failureViewPosition: SubviewPosition——失败视图位置,默认.fill。

失败路径的显示逻辑在handle(result:isSync:)(第 342-366 行):失败时若showPlaceholderOnFailure == true则保持占位视图可见,否则显示失败视图。测试failureViewShownOnFailure(LazyImageViewTests.swift)、failureImageIsShownOnFailure(LazyImageViewLifecycleTests.swift)、showPlaceholderOnFailureKeepsFailureViewHidden(第 452-467 行)逐一验证。

层级关系:占位视图与失败视图被插入在at: 0(最底层),图片视图在其上层,因此加载完成的图片会覆盖它们;而makeImageView生成的自定义视图位于最顶层。测试 subviewsAreLayeredBelowTheImage 精确断言了placeholderIndex < imageViewIndex、failureViewIndex < imageViewIndex且subviews.last === customView。

过渡动画:transition

transition: Transition?默认.fadeIn(duration: 0.33)(源码 LazyImageView.swift)。枚举Transition有两种:

  • .fadeIn(duration: TimeInterval)——淡入。iOS 侧用UIView.animate把imageView.alpha从 0 动画到 1,并带.allowUserInteraction选项;macOS 侧对 layer 执行透明度动画(第 477-490 行)。
  • .custom(closure: (LazyImageView, ImageContainer) -> Void)——自定义过渡,闭包在图片已显示之后、imageContainer更新之前调用(第 98-101 行)。

两个重要行为:内存缓存命中时不执行过渡(因为display中仅!isFromMemory时才运行,测试 transitionNotRunFromMemoryCache 验证);失败时不执行过渡(customTransitionIsNotRunOnFailure,LazyImageViewLifecycleTests.swift)。customTransitionRunsAfterImageIsDisplayedAndBeforeCallbacks(第 214-235 行)验证了时序:["transition", "success", "completion"]。

渐进式渲染与重置行为

  • isProgressiveImageRenderingEnabled: Bool——默认 true。为 false 时,pipeline 产生的渐进式预览帧会被忽略、不显示(handle(preview:)第 334-340 行直接 return),但onPreview回调依然会被触发。测试 progressivePreviewsIgnoredWhenRenderingDisabled 验证预览计数大于 0 但预览期间图片从未被设置。
  • isResetEnabled: Bool——默认 true。为 true 时,每次新加载开始前清空当前图片(resetOrDefer(clearImage:)第 262-268 行)。为 false 时,重置被延迟(记录isResetNeeded = true),旧图在新图片就绪或请求失败时才被替换/清除,适合「加载中保留旧内容」的场景。测试isResetEnabledFalseKeepsImageDuringLoad(第 599-622 行)验证加载期间旧图仍在;isResetEnabledFalseDisplaysProgressivePreviewsWithoutCancellingTask(第 646-682 行)验证延迟重置应用在首个预览时不取消在途任务;deferredResetIsAppliedWhenRequestFails(LazyImageViewLifecycleTests.swift)与deferredResetKeepsImageWhenNewRequestIsCancelled(第 546-565 行)覆盖失败与取消分支。

回调体系:onStart、onProgress、onPreview、onSuccess、onFailure、onCompletion

六个回调属性(源码 LazyImageView.swift)完整反映请求生命周期:

回调触发时机参数
onStart请求开始(任务创建后)ImageTask
onProgress下载进度更新ImageTask.Progress(completed/total)
onPreview产生渐进式预览帧ImageResponse
onSuccess加载成功ImageResponse
onFailure加载失败ImagePipeline.Error
onCompletion无论成败,请求结束Result<ImageResponse, ImagePipeline.Error>

从load的 progress 闭包(第 314-329 行)可见:预览响应到达时先handle(preview:)显示再调用onPreview;只有无响应的进度回调才走onProgress。从handle(result:isSync:)(第 342-366 行)可见:成功/失败分支先处理显示与隐藏,随后清空imageTask,最后依次触发onSuccess/onFailure与onCompletion。

回调时序有严格保证,测试 successCallbacksAreDeliveredInOrder 断言成功路径为start → progress... → success → completion(首元素是 "start",尾两个是 "success"、"completion",中间全部是 "progress"),且回调触发时任务已结束(imageTask == nil)、图片已在屏、占位已隐藏;failureCallbacksAreDeliveredInOrder 断言失败路径严格为["start", "failure", "completion"]。

imageTask在回调前被清空这一点意义重大:它允许回调中直接发起新请求而不会互相干扰。测试 requestStartedFromCallbackBecomesTheCurrentTask 验证在onFailure中赋值新url(如失败回退图),新任务会成为imageTask且未被取消。

底层视图与自定义渲染:imageView、makeImageView

  • imageView: AnimatedImageView——只读属性,返回内部底层的图片视图。它之所以是AnimatedImageView,是为了让 pipeline 识别为动图(GIF、APNG、WebP、HEIC/AVIF 序列)的响应直接播放而非只显示首帧(源码 LazyImageView.swift);动图播放的控制通过AnimatedImageView.player完成。普通静态图则通过nuke_display走ImageDisplaying协议路径显示。
  • makeImageView: ((ImageContainer) -> _PlatformBaseView?)?——默认 nil。提供后,每个响应(含渐进式预览)都会调用它生成自定义视图来展示;返回 nil 则回退到默认平台视图(第 116-118 行、display第 375-386 行)。每次显示新响应前,为上一个响应创建的自定义视图会被移除(removeCustomImageView第 393-397 行),保证多响应(预览 + 最终图)不叠加。

测试覆盖:makeImageViewUsedForCustomView(LazyImageViewTests.swift,自定义视图成为子视图且默认 imageView 保持未用)、makeImageViewReturningNilFallsBackToDefault(第 548-557 行)、customViewsAreNotStackedAcrossResponses(第 559-580 行)、resetRemovesCustomView(第 582-595 行)、makeImageViewReceivesTheResponseAndItsViewFillsTheView(自定义视图铺满父视图,4 条约束)。Demo 中 NukeVideo 的使用方式是让makeImageView对含视频资产的容器返回VideoPlayerView(见 Demo/Formats/VideoDemo.swift)。

实战:在 UICollectionView 中配合 Cell 复用

Demo 项目 Demo/Essentials/UIKitViewsDemo.swift 提供了可直接借鉴的完整用法。核心思路是:占位、失败图、过渡等状态只在创建 Cell 时配置一次,之后每次只需赋值url:

private final class LazyImageViewCell: UICollectionViewCell { let imageView = LazyImageView() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .secondarySystemBackground imageView.placeholderView = UIActivityIndicatorView(style: .medium) imageView.placeholderViewPosition = .center imageView.failureImage = UIImage(systemName: "exclamationmark.triangle") imageView.failureViewPosition = .center imageView.transition = .fadeIn(duration: 0.33) imageView.imageView.contentMode = .scaleAspectFill imageView.imageView.clipsToBounds = true imageView.frame = bounds imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] contentView.addSubview(imageView) } override func prepareForReuse() { super.prepareForReuse() // 取消在途请求并清除已显示图片。 imageView.reset() } }

对应控制器里,cellForItemAt只需一行cell.imageView.url = photos[indexPath.item](第 153-157 行)即可完成整格图片的加载、取消与复用处理;Demo 还特意把首个 URL 设为始终失败的地址,以直观展示失败视图。若需要按 Cell 尺寸对图片做降采样,可在ImageRequest中配置.resize(size:)处理器,以降低内存缓存中位图的开销(第 110-113 行注释)。

边界行为与陷阱小结

结合源码与测试,以下边界行为值得注意:

  1. 内存缓存命中完全同步:不触发onStart、不启动网络任务、不执行过渡动画,事件序列仅为success → completion(LazyImageViewLifecycleTests.swift)。
  2. 旧请求的迟到响应会被丢弃:即使旧响应的完成回调已派发到主队列,只要视图已被赋予新请求,迟到响应也不会覆盖新图——这是经典的 Cell 复用问题防线(lateResponseOfReplacedRequestIsDropped)。
  3. 视图释放自动取消:deinit调用imageTask?.cancel(),无需手动管理(viewDeallocCancelsTask)。
  4. 取消后零回调:被取消的请求不会触发任何onPreview/onProgress/onSuccess/onFailure/onCompletion(cancelledRequestDeliversNoCallbacks)。
  5. 主线程约束:load(_:)断言必须在主线程调用(源码 LazyImageView.swift),配合@MainActor标注,所有配置与赋值都应发生在主线程。
  6. nil 请求是同步失败:赋url = nil或request = nil会以.imageRequestMissing同步失败并显示失败视图(nilRequestFailsSynchronouslyWithoutStarting)。

综上,LazyImageView通过「属性即状态」的设计,把 UIKit/AppKit 场景下最繁琐的图片加载状态机封装成了可配置属性与六个生命周期回调,同时完整复用ImagePipeline的缓存、优先级、处理器与渐进式解码能力——这正是 NukeUI.md 中所说的「两者(LazyImage 与 LazyImageView)开箱即用地播放 GIF、APNG、WebP 及 HEIC/AVIF 序列动图」的底层支撑。

  • 移动开发
  • 图像处理

【免费下载链接】Nuke

Image loading system

项目地址:https://gitcode.com/gh_mirrors/nu/Nuke
点击查看免费下载
上一篇:如何快速部署本地大语言模型:AMD GPU用户的终极指南
下一篇:lint-staged 项目推荐

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

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

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

立即咨询