AsyncMux

适用于现代 Swift 客户端应用程序的异步缓存和多路复用层

目录

简介

Swift AsyncMux 实用程序套件为网络对象提供了一个基于 Swift 结构化并发 (async/await) 的缓存/多路复用层。 AsyncMux 是一个较旧的基于回调的库的演变版本,可在此处获取:here

以下是 Multiplexer 实用程序涵盖的场景

场景 1: 执行一个异步块(通常是网络调用),并将结果返回给一个或多个调用者。例如,您的应用程序的各个部分可能在程序启动时同时请求用户的个人资料;您希望确保网络请求只执行一次,然后将结果返回给请求该对象的所有应用程序部分。我们称之为多路复用

此外,还提供结果在内存中缓存一段时间。除非经过一定的生存时间 (TTL),否则对多路复用器的后续调用可能会返回缓存的结果,在这种情况下,会透明地进行新的网络调用。

场景 2: 如果您的应用程序在没有网络连接的情况下启动,则允许应用程序至少显示一些内容。这可以通过将网络对象缓存在磁盘上来实现,以便它们可以在应用程序重新启动后继续存在,并且通过让多路复用器在发生连接错误时自动返回缓存的结果,而不管它们的 TTL。

为每个多路复用器提供“软”和“硬”刷新功能。

场景 3: 为不可变对象(例如媒体文件和文档)提供文件下载、多路复用和磁盘缓存。

Multiplexer(多路复用器)

Multiplexer<T> 是一个用于客户端应用程序的异步缓存工具。每个 multiplxer 实例可以管理类型为 T: Codable & Sendable 的一个对象的检索、多路复用和缓存,因此最好将应用程序中的每个多路复用器实例定义为单例。另请注意,Multiplexer 本身是一个 actor,因此它的所有公共方法都是异步的。

对于每个多路复用器单例,您定义一个实现对象异步检索的块,这可能是例如对后端系统的网络请求。

多路复用器单例保证只会进行一次获取/检索操作,并且随后会将内存缓存的对象返回给其 request() 方法的调用者,除非缓存的对象根据 defaultTTL 设置(当前设置为 30 分钟)过期。此外,Multiplexer 可以将对象存储在磁盘上 - 请参阅 MuxRepository,以及下面关于 request() 的讨论。

假设您有一个 UserProfile 结构和一个用于从后端检索当前用户的个人资料对象的方法,其签名如下所示

class Backend {
    static func fetchMyProfile() async throws -> UserProfile {
        // ...
    }
}

那么,多路复用器单例的实例化将如下所示

let myProfile = Multiplexer<UserProfile>(onFetch: {
    try await Backend.fetchMyProfile()
})

或者更短

let myProfile = Multiplexer(onFetch: Backend.fetchMyProfile)

现在您可以使用 myProfile 通过调用 request() 方法来获取个人资料对象,如下所示

try {
    let profile = try await myProfile.request()
}
catch {
    print("Coudn't retrieve user profile:", error)
}

首次调用时,request() 会调用您的 onFetch 块,将其异步返回给调用者(或抛出错误),并将结果缓存在内存中。随后对 request() 的调用将立即返回存储的对象。

最重要的是,request() 可以处理多个同时调用,并确保一次只启动一个 onFetch 操作。

缓存

默认情况下,Multiplexer<T> 可以将对象作为 JSON 文件存储在本地缓存目录中。要启用此功能,请在多路复用器的构造函数中提供一个唯一的 cacheKey

let myProfile = Multiplexer<UserProfile>(cacheKey: "MyProfile") {
    try await Backend.fetchMyProfile()
}

如果您的 onFetch 由于连接问题而失败,则即使 TTL 过期后,多路复用器也可以重复使用存储在磁盘上的对象。您还可以通过在您的 onFetch 方法中抛出 SilencableError 来告诉多路复用器忽略该错误并获取缓存的对象。

磁盘存储方法目前是硬编码的,但在该库的未来版本中可以覆盖。

在运行时,您可以使用以下方法之一使缓存的对象失效

有关每种方法的更详细说明,请参见源文件 Multiplexer.swift

MultiplexerMap<K, T>

MultiplexerMap<K, T> 在许多方面与 Multiplexer<T> 相似,不同之处在于它维护一个相同类型的对象字典。一个例子是例如您的社交应用中的用户个人资料对象。在内部,多路复用器映射是一个多路复用器字典,其代码由同一个 actor 执行。

K 泛型参数应符合 LosslessStringConvertible & Hashable & Sendable。字符串可转换性要求是因为它简化了磁盘缓存程序存储对象的工作。

上面给出的 Multiplexer 的示例将如下所示。首先,假设您有一种方法可以通过用户 ID 检索用户个人资料

class Backend {
    static func fetchUserProfile(id: String) async throws -> UserProfile {
        // ...
    }
}

此外,MultiplexerMap 单例可以定义如下

let userProfiles = MultiplexerMap(onFetch: Backend.fetchUserProfile)

并在应用程序中这样使用

try {
    let profile = try await userProfiles.request(key: "user_8cJOiRXbugFccrUhmCX2")
}
catch {
    print("Coudn't retrieve user profile:", error)
}

Multiplexer 一样,MultiplexerMap 定义了自己的方法 refresh()clear()save()。此外,对于 refresh()clear(),还有这些方法的版本,它们将对象键作为参数。

在内部,MultiplexerMap 维护一个 Multiplexer 对象映射,这意味着通过其 ID 获取和缓存每个对象是独立完成的。

有关每种方法的更详细说明,请参见源文件 MultiplexerMap.swift

MuxRepository(多路复用器仓库)

MuxRepository 是异步静态函数的集合,可用于集中式操作,例如对应用程序中所有已注册的多路复用器实例执行 clearAll()saveAll()。您应该在创建多路复用器实例时通过提供唯一的 cacheKey 来注册每个实例。

默认情况下,MultiplexerMultiplexerMap 接口不将对象存储在磁盘上。如果您想保留对象以确保它们在应用程序重新启动后继续存在,请确保在将应用程序发送到后台时调用 MuxRepository.saveAll()就像在演示应用程序中显示的那样

MuxRepository.clearMemory() 丢弃所有存储在内存中的对象。在处理来自 OS 的内存警告时很有用(请参阅演示应用程序中的示例用法)。

MuxRepository.clearAll() 丢弃所有内存和磁盘对象。例如,当用户退出系统并且您需要确保在内存或磁盘中不留下与给定用户相关的数据痕迹时,这非常有用。

AsyncMedia(异步媒体)

AsyncMedia 是一个完全静态的接口,它为被认为是不可变的大型文件提供下载、多路复用和缓存工具。

与其他多路复用器一样,AsyncMedia 确保对于每个远程 URL,无论同时调用 request(url:) 的次数如何,只有一个下载过程处于活动状态。

下载过程基于流式传输,这意味着每个下载过程使用的内存是固定的,并且不依赖于文件大小。

您使用 AsyncMedia.request(url:) 请求的每个文件都会被下载并本地存储在应用程序的缓存目录中;然后异步返回本地文件 URL。随后对同一 URL 调用 request(url:) 将立即返回本地文件 URL。

如果可用,请使用 cachedValue(url:) 获取缓存对象的本地文件 URL。

请注意,远程文件被假定为不可变的,因此不维护生存时间,即假定该文件可以无限期地存储在本地缓存中。另请注意,如果操作系统需要释放空间,它可以擦除应用程序的缓存目录,这通常应该在应用程序未运行时发生。此外,您可以通过调用 AsyncMedia.clear() 来清除媒体缓存目录。

演示应用程序包括一个完整的软件包,用于对由 AsyncMedia 支持的图像进行内存 LRU 缓存,以及使用这两者的 RemoteImage UI 组件。实际上,RemoteImage 是 SwiftUI 的 AsyncImage 的替代方案,此外还可以本地缓存不可变图像。

实验性功能

Zip: Zip<T> 允许将两个或多个并行异步操作组合成一个,并在它们可用时立即接收所有操作的结果。执行结果作为 T 数组返回。有关示例用法,请参见 演示应用程序的 ContentView 中的函数 reload()

此外,一系列全局函数 zip(...) 为固定数量的异步调用(最多 5 个)提供了快捷方式。

请参阅:Zip.swift

MultiRequester: 如果您的后端支持多 ID 请求(例如 /profiles/[id1,id2]),则 MultiRequester 可以与现有的 MultiplexerMap 对象结合使用,以将单个和多个请求合并到同一个缓存基础设施中。 通过 MultiRequester 的 request(...) 方法发出的多 ID 请求可以更新与其链接的映射,还可以重用映射存储的缓存值。 因此,无论对象是通过单个端点还是通过多 ID 端点请求的,都将在本地缓存对象; 另一方面,多 ID 请求可以通过重用已缓存的对象并从后端请求较少的 ID(甚至没有)来节省带宽。

请参阅:MultiRequester.swift

注意:这些实验性功能不能保证在未来版本中兼容,或者它们不会被完全删除。

构建和链接

您可以要么使用提供的 SPM 包定义,要么克隆此存储库并将其用作您项目中的 git 子模块。

如果您想尝试演示,请启动 AsyncMux.xcworkspace 文件。 演示应用程序中使用的天气 API:Open Meteo

AsyncMux 框架没有任何第三方依赖项。

祝您编码愉快!

AsyncMux v2.1

AsyncMux v2.0

作者

AsyncMux 由 Hovik Melikyan 开发。