FirebaseStorage for Apple 平台演进史:从 10.0.0 纯 Swift 重写到 12.19.0 的版本迭代全解析
【免费下载链接】firebase-ios-sdkFirebase SDK for Apple App Development项目地址: https://gitcode.com/GitHub_Trending/fi/firebase-ios-sdk
本文以 FirebaseStorage/CHANGELOG.md 为时间主线,结合 firebase-ios-sdk 仓库中的
FirebaseStorage/Sources源码与FirebaseStorage/Tests测试,系统梳理 Cloud Storage for Firebase 的 Apple 平台 SDK(iOS、macOS、tvOS、watchOS、App Clip、App Extension)从 Objective-C 到 Swift、从异步回调到async/await、从弱安全默认到强传输层安全的历史演进。读完本文,你将掌握 FirebaseStorage 各版本的破坏性变更(Breaking Changes)、关键修复背后的实现原理、重试/并发/安全机制的工作方式,以及如何在自己的工程中规避这些历史问题。
一、版本脉络总览:一条从 ObjC 到 Swift 的重写之路
FirebaseStorage(ObjC 时代名为FirebaseStorage/FirebaseStorageSwift)在 firebase-ios-sdk 中的演进大致可分为四个阶段,CHANGELOG 的版本号序列完整记录了这条路线:
| 阶段 | 版本区间 | 关键标志 |
|---|---|---|
| 早期开源(ObjC) | 1.0.4 – 3.8.1 | 上传/下载任务、list API、watchOS/tvOS 支持、目录上传错误 |
| 稳定收敛期 | 7.0.0 – 8.15.0 | useEmulator()、实例缓存、List API 修复、FIRStorageErrorDomain弃用 |
| 纯 Swift 重写 | 9.0.0 – 10.24.0 | FirebaseStorageSwift 合并、async/await 回移、进度跟踪、App Extension 支持 |
| 安全与健壮性深化 | 11.0.0 – 12.19.0 | Swift/NSError 双错误模型、传输层安全强化、Swift 6 并发合规、模拟器/App Clip 专项修复 |
其中10.0.0是分水岭:CHANGELOG 明确记录"FirebaseStorage is now completely implemented in Swift",FirebaseStorageInternalCocoaPod 停产,StorageMetadata.storageReference被弃用(它从未真正实现,一直返回nil),并在12.0.0中正式移除。
二、10.0.0 纯 Swift 重写:API 与错误模型的范式转移
2.1 抛异常 vsfatalError:ObjC 时代的遗产
CHANGELOG 10.0.0 记录:"Storage APIs that previously threw an Objective-C exception now generate a SwiftfatalError"。这从源码中可以找到清晰的实现痕迹。在 Storage.swift 中,reference(forURL:)在处理非法 URL 时:
@objc open func reference(forURL url: String) -> StorageReference { do { let path = try StoragePath.path(string: url) ... } catch let StoragePathError.storagePathError(message) { fatalError(message) } catch { fatalError("Internal error finding StoragePath: \(error)") } }而新增的 reference(for:) 则改为抛出真正的Error(StorageError.pathError或StorageError.bucketMismatch),这正是 10.0.0 记录的新 API:open func reference(for url: URL) throws -> StorageReference,等价于旧的reference(forURL:),但以 Swift 错误代替崩溃。
路径解析的核心在 StoragePath.swift:path(string:)只接受三种 scheme——gs://、http://、https://,其余一律抛出StoragePathError。HTTP(S) 形式必须满足http[s]://<host>/v0/b/<bucket>/o/<path/to/object>[?token=...]的结构,这是解析从downloadURL拿到的签名下载链接的基础。
2.2 重试超时的语义:maxOperationRetryTime统一
3.0.0 记录了一个容易踩坑的语义调整:maxOperationRetryTime现在同样作用于getMetadata(completion:)与updateMetadata(completion:)——此前它们误用了上传/下载的重试超时。当前源码中三个超时的默认值定义在 Storage.swift:
maxUploadRetryTime:默认 600 秒(10 分钟)maxDownloadRetryTime:默认 600 秒(10 分钟)maxOperationRetryTime:默认 120 秒(2 分钟)
底层通过 computeRetryInterval(fromRetryTime:) 把"用户视角的总重试时长"翻译成 GTMSessionFetcher 的"单次重试间隔"——由于 GTMSessionFetcher 的重试从 1 秒开始指数翻倍,该方法累加1 + 2 + 4 + ...直到超过目标总时长,再返回最后一次的间隔值。
2.3 上传分块大小:10.5.0 的uploadChunkSizeBytes
10.5.0 新增"限制上传分块大小"的能力,对应 Storage.swift 的:
/// Values less than 256K (262144) will be rounded up to 256K. Values /// above 256K will be rounded down to the nearest 256K multiple. The default is no maximum. @objc public var uploadChunkSizeBytes: Int64 = .max默认Int64.max表示不分块限制;设置时会以 256K 为粒度取整。该值在 StorageUploadTask.swift 中被传入GTMSessionUploadFetcher的chunkSize,直接决定可恢复上传(resumable upload)每个分片的大小。
三、并发模型演进:串行队列、实例缓存与多 App 死锁
3.1 10.3.0:回归专用串行队列
10.3.0 记录了一个由 10.0.0 引入的回归——上传/下载从(并发的)全局队列改回专用串行队列。当前实现中,每个Storage实例持有:
// Must be a serial queue. dispatchQueue = DispatchQueue(label: "com.google.firebase.storage")(见 Storage.swift)。所有任务(上传、下载、元数据、list、delete)均在该串行队列上enqueue,回调则统一投递到callbackQueue(默认为主队列,可配置,见 Storage.swift)。这保证了任务状态机(StorageTaskState)的串行访问语义。
3.2 10.2.0:多 FirebaseApp 死锁修复与实例缓存
10.2.0 修复了"多个 FirebaseApp 实例时非默认 Storage 实例死锁"的问题,并修复了maxSize下载超限的竞态。随后的 12.12.1 又修正了InstanceCache的键控问题——之前仅按 bucket 键控,导致多个命名 App 共享同一存储桶时拿到错误 App 的 Auth 上下文。
当前 Storage.swift 的InstanceCache已改为"\(app.name)|\(bucket)"组合键,并用os_unfair_lock保护,确保每个(App, bucket)组合只有一个Storage实例。这也与 8.2.0 记录的"实例缓存:重复调用Storage.storage()返回同一实例并保留相同设置"一脉相承。注意Storage实例本身是 @objc 类且"非线程安全,但可从任意线程访问"(见文件头注释)。
3.3 maxSize 下载超限:源码中的双重防护
10.2.0 提到的下载尺寸竞态,在 StorageReference.swift 的checkSizeOverflow中可以看到双重检查:成功回调与.progress回调都会比对task.progress.totalUnitCount/completedUnitCount与maxSize,一旦超出即产生StorageError.downloadSizeExceeded(total:maxSize:)(错误码 -13032),并调用task.cancel(withError:)中止下载。
四、传输层安全与模拟器:从明文 HTTP 到显式拒绝
这是 12.x 系列最值得关注的演进,CHANGELOG 用两条记录串起了一个完整的安全策略升级:
- 12.17.0:使用模拟器时执行更严格的传输层安全——连接非 loopback 主机且走 HTTP 时,不再附加 Auth/AppCheck token。
- 12.19.0:在 12.17.0 的基础上更进一步——"当存在 token 时,指向本地模拟器但走非 loopback HTTP 连接的请求,现在会显式失败并返回
unauthenticated错误,而不是静默丢弃 token 后继续执行"。
4.1 实现证据:StorageTokenAuthorizer
决策逻辑集中在 StorageTokenAuthorizer.swift:
let scheme = request?.url?.scheme?.lowercased() let isHttps = scheme == "https" let host = request?.url?.host?.lowercased() ?? "" let isLoopback = host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "[::1]" #if DEBUG let shouldAttachTokens = isHttps || isLoopback || allowInsecureTokenAttachment() #else let shouldAttachTokens = isHttps || isLoopback #endif后续(第 84-104 行)在 token 已附加但shouldAttachTokens == false且 scheme 为http时,除了剥离Authorization与X-Firebase-AppCheck头,还会构造StorageError.unauthenticated错误并记录警告日志I-STR000002("Refusing to send Auth and AppCheck tokens over HTTP to non-loopback host.")。这就是 12.19.0 行为变化的代码落地:错误被显式返回,请求不再"带病"继续。
4.2 12.19.0 新增:allowInsecureTokenAttachment(仅 DEBUG)
为满足"真机调试走 HTTP"的合法场景,12.19.0 新增了 DEBUG-only 属性,定义在 Storage.swift:
#if DEBUG @nonobjc public var allowInsecureTokenAttachment: Bool = false #endif它通过StorageFetcherService的闭包注入到 StorageTokenAuthorizer,并仅在#if DEBUG下参与shouldAttachTokens判定。注意它在 ObjC 侧不可见(@nonobjc),且默认关闭——这是刻意设计的防泄漏闸门。
4.3 模拟器连接的正确姿势:useEmulator(host:port:)
CHANGELOG 8.0.0 首次引入useEmulator()。当前实现见 Storage.swift:必须在首次创建 StorageReference 之前调用,否则直接fatalError;同时把scheme切到http。在StorageFetcherService中,启用模拟器后还会打开allowLocalhostRequest与allowedInsecureSchemes = ["http"](见 StorageFetcherService.swift),因此"localhost / 127.0.0.1"这类 loopback 主机始终被允许明文传输 token。也正因如此,12.17.0/12.19.0 的新策略只影响非 loopback的 HTTP 场景(例如用局域网 IP 在真机调试)。
4.4 8.5.0 的 emulator HTTP 修复
历史上 8.5.0 修复了"Storage 无法通过 http 连接本地模拟器"的问题,当前代码通过allowLocalhostRequest = true与允许httpscheme 维持了该能力。
五、任务系统的健壮性:取消竞态、回调存活、POSIX 错误
5.1 12.17.0:取消竞态与 Swift 6 并发合规
12.17.0 修复了 Storage 任务取消时的竞态条件,并提升StorageTask的 Swift 6 严格并发合规性。从源码看,任务的全部可变状态(state、metadata、error、progress)都通过stateLock(NSLock,见 StorageTask.swift)保护,StorageUploadTask的pause()/cancel()/resume()均先加锁判定合法状态再操作底层GTMSessionUploadFetcher(见 StorageUploadTask.swift)。StorageTask与StorageUploadTask均声明为@unchecked Sendable。
5.2 12.17.0:putFile在 iOS 模拟器的POSIX errno 40 (EMSGSIZE)
CHANGELOG 指出:putFile在 iOS 模拟器上可能因后台会话(background session)的 QUIC bug 报EMSGSIZE。对应的处理逻辑在 StorageUploadTask.swift:
#if targetEnvironment(simulator) uploadFetcher.useBackgroundSession = false #else if !GULAppEnvironmentUtil.supportsBackgroundURLSessionUploads() { uploadFetcher.useBackgroundSession = false } #endif即模拟器上强制关闭后台会话;同时 POSIX 错误现在会被包装成带明确message的StorageError.unknown——这一格式化逻辑见 StorageError.swift:"POSIX errno \(serverError.code) (\(serverError.localizedDescription))"。仓库中还有专门的 StoragePOSIXErrorTest.swift 覆盖此类错误的格式化。
5.3 任务回调的存活校验(7.3.0 与 3.6.1 的延续)
CHANGELOG 中反复出现回调类修复:7.3.0"回调前校验 block 是否存活"、3.6.1"罕见情况下回调被调用多次"、2.0.1"回调失效后崩溃"。当前 StorageReference.swift 的startAndObserveUploadTask采用"回调只调用一次、调用后置空(completionMetadata = nil)"的策略,从机制上杜绝了重复回调;下载侧getData(maxSize:completion:)同样在.success/.failure回调后清空completionData。
5.4 3.8.0 / 3.7.0:拒绝上传目录
3.8.0/3.7.0 为putFile:增加了目录检测。当前实现为 StorageUploadTask.swift 的contentUploadError():通过fileURL.resourceValues(forKeys: [.isRegularFileKey])校验目标是否为常规文件,不是则返回StorageError.unknown,提示"Ensure file URL is not a directory, symbolic link, or invalid url"。
六、上传下载 API 的完整能力矩阵(含 async/await)
CHANGELOG 10.11.0 为putDataAsync、putFileAsync、writeAsync增加了进度跟踪能力。这组 API 全部定义在 AsyncAwait.swift,均以withCheckedThrowingContinuation包装传统回调实现:
| async/await API | 包装的回调 API | 进度回调参数 | 返回 |
|---|---|---|---|
data(maxSize:) | getData(maxSize:completion:) | 无 | Data |
putDataAsync(_:metadata:onProgress:) | putData(_:metadata:completion:) | onProgress: ((Progress?) -> Void)? | StorageMetadata |
putFileAsync(from:metadata:onProgress:) | putFile(from:metadata:completion:) | onProgress: ((Progress?) -> Void)? | StorageMetadata |
writeAsync(toFile:onProgress:) | write(toFile:completion:) | onProgress: ((Progress?) -> Void)? | URL |
list(maxResults:)/list(maxResults:pageToken:) | list(maxResults:completion:)系列 | 无 | StorageListResult |
当传入onProgress时,实现会observe(.progress)并把snapshot.progress透传给调用方;上传/下载的进度数据来自底层GTMSessionUploadFetcher的sendProgressBlock(见 StorageUploadTask.swift),通过NSProgress的completedUnitCount/totalUnitCount表达。
6.1 下载两种模式的选择
- 内存下载
getData(maxSize:):文档明确提示会按maxSize分配内存,大文件应改用write(toFile:)(见 StorageReference.swift)。 - 文件下载
write(toFile:):流式落盘,返回本地URL。
6.2 9.0.0 的合并与 8.5.0 的回移
9.0.0 将FirebaseStorageSwift的所有 API 并入FirebaseStorage,要求开发者把import FirebaseStorageSwift全部替换为import FirebaseStorage;同时把StorageReference的 async/await API回移到 iOS 13(8.5.0 起这些 API 由 Xcode 自动生成,9.0.0 改为手写实现以支持更老系统)。StorageErrorDomain全局变量仅对 Swift 保留(9.0.0),ObjC 侧的FIRStorageErrorDomain在 8.15.0 标记弃用。
七、错误模型:Swift 枚举与 NSError 的共存(11.0.0)
11.0.0 是一次需要留意的潜在破坏性变更:"Swift 错误枚举新增了部分参数,同时补全了 NSError 分支,但 NSError 侧无破坏。"当前 StorageError.swift 定义了完整的StorageError枚举,StorageErrorDomain = "FIRStorageErrorDomain",StorageErrorCode的错误码(负数)也一并列出:
| 错误码 | 枚举 case | 典型触发场景 |
|---|---|---|
| -13000 | .unknown | 后端未知错误、POSIX 错误包装 |
| -13010 | .objectNotFound | 对象不存在(后端 404) |
| -13011 | .bucketNotFound | 存储桶不存在 |
| -13012 | .projectNotFound | 项目不存在 |
| -13013 | .quotaExceeded | 配额超限(后端 402) |
| -13020 | .unauthenticated | 未认证(后端 401 / 模拟器 HTTP 拒发 token) |
| -13021 | .unauthorized | 无权限(后端 403) |
| -13030 | .retryLimitExceeded | 超过最大重试时长 |
| -13031 | .nonMatchingChecksum | 校验和不匹配 |
| -13032 | .downloadSizeExceeded | 下载超过maxSize |
| -13040 | .cancelled | 用户取消任务 |
| -13050 | .invalidArgument | 参数非法(如maxResults越界) |
| -13051 | .bucketMismatch | URL 桶与实例桶不一致 |
| -13052 | .internalError | 内部错误 |
| -13053 | .pathError | 路径解析失败 |
StorageErrorCode.error(withServerError:ref:)(StorageError.swift)负责把 GCS 后端 HTTP 状态码映射为上述错误,并保留NSUnderlyingErrorKey、ResponseErrorDomain、ResponseErrorCode、bucket、object等上下文——这正是 10.7.0"通过NSUnderlyingErrorKey提供服务端错误"的实现。
八、List API 的演变与边界条件修复
- 3.3.0新增
StorageReference.list()与listAll()。 - 7.0.0修复含
+号路径的列表问题,并将 Swift API 从list(withMaxResults:)重命名为list(maxResults:)。 - 7.4.0防止第二次
listAll回调。 - 3.7.0修复根位置调用
listAll()的崩溃。
当前 StorageReference.swift 的listAll(completion:)内部通过pageToken循环分页直至取完,并在结束处主动断开闭包引用环(paginatedCompletion = nil);list(maxResults:pageToken:)强制校验1 ≤ maxResults ≤ 1000,越界返回.invalidArgument(第 389-392 行)。listAll与list均要求 Firebase Rules Version 2。
九、平台能力边界:App Clip、App Extension、模拟器与后台会话
CHANGELOG 有三条记录专门处理"受限运行环境":
- 10.24.0:
putFile/putFileAsync在 App Extension 中可用——App Extension 不使用后台会话配置。 - 11.13.0:
putFile在 App Clip 中同样可用,行为与 App Extension 一致。 - 12.17.0:iOS 模拟器上
putFile因 QUIC bug 报EMSGSIZE,模拟器强制关闭后台会话。
三者的统一实现都在 StorageUploadTask.swift:模拟器或环境不支持后台 URLSession 上传时,useBackgroundSession = false,从而规避后台会话在这些受限环境的兼容性问题。
十、其他值得注意的修复清单(按主题归类)
URL 与路径解析
- 12.19.0:存储桶名含特殊字符时,构造下载 URL 失败或产生非法路径的问题(PR #16529)。
- 12.17.0:解析畸形
gs://URL 时可能崩溃;Storage.reference(forURL:)对 HTTP 下载 URL 的校验增强(PR #16243)。 - 3.0.3:修复包含分号的文件名无法上传的问题;1.0.4 修复包含
+的文件名上传问题。字符白名单可见 StorageUploadTask.swift 的GCSEscapedString。
依赖与构建
- 10.5.0:要求将
GTMSessionFetcher更新到 ≥ 3.1.0(修复 Storage 返回 500 时无限重试的问题);10.0.0 起要求 GTMSessionFetcher ≥ 2.1。 - 9.2.0:导入 FirebaseStorage 不再暴露内部 FirebaseCore API(源码中通过
internal import FirebaseCoreExtension实现,见 Storage.swift)。 - 7.1.0:podspec 移除显式的 MobileCoreServices 链接。
- 7.0.0:删除全局
FIRStorageVersionString,改用FirebaseVersion()/FIRFirebaseVersion()。 - 12.9.0:修复 Xcode 26.2 引入的 "weak never mutated" 编译警告。
初始化与缓存
- 11.1.0:修复 Storage 初始化时的潜在数据竞争。
- 10.10.0:修复 Storage 实例的潜在内存泄漏。
- 8.2.0:实例缓存,
Storage.storage()重复调用返回同一实例。 - 3.4.0:防止开发者误写
Storage()直接调用构造器——会触发断言失败而非后续崩溃。
元数据
- 2.1.0:
FIRStorageMetadata增加md5Hash;2.0.2 允许将自定义元数据属性置nil以清除;2.0.1 在 NSDictionary 表示中增加size。 - 10.1.0:修复 10.0.0 回归——
putFile传入的 metadata 未正确初始化;以及模拟器返回空 JSON metadata 字段时的处理回归。
平台支持
- 3.6.0 增加 watchOS 支持;2.1.2 增加 tvOS 社区支持(其后进入官方支持)。
十一、版本升级行动清单(面向开发者)
综合以上演进,升级 FirebaseStorage 时建议按此清单核对:
- 10.0.0+:
import FirebaseStorage(不再有FirebaseStorageSwift);reference(forURL:)的非法输入会触发fatalError,请改用try reference(for:)。 - 12.0.0:移除对
StorageMetadata.storageReference的引用(它从未实现,恒为nil)。 - 11.0.0:Swift 错误枚举可能新增关联参数,
catch分支若按旧参数解构需重新编译确认。 - 12.17.0 / 12.19.0:模拟器测试若使用非 loopback 主机(如局域网 IP)+ HTTP,会收到
unauthenticated错误;DEBUG 构建可临时设置storage.allowInsecureTokenAttachment = true缓解,切勿用于生产或 HTTPS 之外的真实流量。 - GTMSessionFetcher:确保依赖 ≥ 3.1.0(CocoaPods 执行
pod update,SPM 执行 File → Packages → Update to latest Packages),避免 500 无限重试。 - App Clip / App Extension / 模拟器:
putFile在这些环境会自动退化为非后台会话,无需额外配置。 - 并发:所有开发者回调默认在主队列(
callbackQueue),可在Storage实例上重定向;任务状态机是线程安全的,但Storage实例本身需避免并发修改其可变属性。
十二、进一步阅读
- 核心 API 实现:Storage.swift、StorageReference.swift、StorageUploadTask.swift、StorageDownloadTask.swift
- 路径解析与错误模型:StoragePath.swift、StorageError.swift、StorageTaskState.swift
- 安全与传输:StorageTokenAuthorizer.swift、StorageFetcherService.swift
- 并发与 async/await 扩展:StorageTask.swift、AsyncAwait.swift
- 测试佐证:StorageIntegration.swift、StorageAsyncAwait.swift、StoragePOSIXErrorTest.swift、StorageAuthorizerTests.swift、StoragePutFileTests.swift
说明:本文所有版本号、行为变化均以仓库内 FirebaseStorage/CHANGELOG.md 为准,源码证据对应
FirebaseStorage/Sources目录下的实现文件;文中"可以推断"类结论均来自对上述源码与测试的直接阅读。
【免费下载链接】firebase-ios-sdkFirebase SDK for Apple App Development项目地址: https://gitcode.com/GitHub_Trending/fi/firebase-ios-sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考