编码和解码字符串
String.utf8 提供 UTF-8 字节,Data 负责编码:
import Foundation
let enc = Data("héllo wörld".utf8).base64EncodedString()
print(enc) // aMOpbGxvIHfDtnJsZA==
if let data = Data(base64Encoded: enc),
let s = String(data: data, encoding: .utf8) {
print(s) // héllo wörld
}
解码路径上有两个可选值。对于无效的 Base64,Data(base64Encoded:) 为 nil;当字节不是有效的 UTF-8 时(例如 [0xFF, 0xFE]),String(data:encoding:) 为 nil。如果你宁愿得到替换字符也不要 nil,请使用 String(decoding: data, as: UTF8.self)。
URL 安全的 Base64
Foundation 不认识 URL 安全字母表,所以需要自己转换字符:
let d = Data("<<???>>".utf8)
let b64url = d.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
print(b64url) // PDw_Pz8-Pg
func base64URLDecode(_ s: String) -> Data? {
var b = s.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
b += String(repeating: "=", count: (4 - b.count % 4) % 4)
return Data(base64Encoded: b)
}
print(String(decoding: base64URLDecode("PDw_Pz8-Pg")!, as: UTF8.self)) // <<???>>
补填充这一步必不可少:Data(base64Encoded: "aGk") 返回 nil。JWT、通过 Apple 登录的身份令牌以及许多推送通知载荷都使用这种不带填充的格式。详见 Base64 与 Base64URL 的区别 和 填充详解,或者用 JWT 解码器 查看令牌。
校验:解码何时返回 nil
每种无效输入的结果都一样,都是 nil,没有任何错误信息。下面这些都返回了 nil:
| 输入 | 问题 |
|---|---|
PDw_Pz8-Pg== | URL 安全字符 |
aGk | 缺少填充 |
aGk=!! | 填充后有无关字符 |
aGk== | 填充过多 |
a | 不可能的长度 |
"aGVs\nbG8K" | 换行符 |
" aGk=" | 开头有空格 |
有一种情况能通过:aGl=(填充位不为零)会解码为 hi。因此有效性检查就是 Data(base64Encoded: s) != nil。当你需要知道字符串为什么失败时,Base64 校验工具 会报告位置和原因,错误指南 逐一解释了每种错误。
对于带换行或零散杂字符的输入,.ignoreUnknownCharacters 选项会跳过字母表之外的一切字符:
Data(base64Encoded: "aGVs\nbG8K", options: .ignoreUnknownCharacters)
// 6 bytes: "hello\n"
只对你信任的 MIME 或 PEM 正文使用它。对任意输入使用会掩盖错误,因为 URL 安全字符也会被跳过,结果悄悄地出错。
换行选项
base64EncodedString(options:) 可以在 64 或 76 个字符处换行,行尾默认是 CRLF:
let x = Data(String(repeating: "x", count: 60).utf8)
x.base64EncodedString(options: .lineLength76Characters)
// "eHh4eHh4...eHh4\r\neHh4" (76 chars, CRLF, rest)
x.base64EncodedString(options: .lineLength64Characters)
// 64 chars, "\r\n", 16 chars (PEM-style)
要使用 Unix 行尾,就把长度选项只与 .endLineWithLineFeed 组合:[.lineLength76Characters, .endLineWithLineFeed]。不加任何选项时,输出为单行,JSON、URL 和请求头需要的正是这种格式。对于电子邮件,MIME Base64 编码器 展示了 76 字符的布局。
编码和解码文件
let url = URL(fileURLWithPath: "hello.txt")
let b64 = try Data(contentsOf: url).base64EncodedString()
print(b64) // aGVsbG8K
try Data(base64Encoded: "aGVsbG8K")!.write(to: URL(fileURLWithPath: "copy.txt"))
在 playground 里强制解包没问题;在应用中,请用 guard let 处理解码出的 Data 并报告错误。
分块处理大文件
Foundation 没有流式 Base64 编码器。按 3 的倍数大小分块读取文件,这样就不会有填充落在中间:
func encodeFile(from src: URL, to dst: URL) throws {
_ = FileManager.default.createFile(atPath: dst.path, contents: nil)
let input = try FileHandle(forReadingFrom: src)
let output = try FileHandle(forWritingTo: dst)
defer { try? input.close(); try? output.close() }
while let chunk = try input.read(upToCount: 3 * 64 * 1024), !chunk.isEmpty {
try output.write(contentsOf: Data(chunk.base64EncodedString().utf8))
}
}
用这种方式编码一个 300 KB 的测试文件,得到的字符串与一次调用编码的结果相同。read(upToCount:) 和 write(contentsOf:) 需要 macOS 10.15.4 / iOS 13.4 或更高版本。为什么输出约为输入的 4/3,见 体积开销指南。
常见陷阱
- 期待错误信息:
Data(base64Encoded:)只会说“不行”。调试时请记录输入长度和开头几个字符。 - Data URI 前缀:来自 Web 视图的字符串常以
data:image/png;base64,开头。解码前在第一个逗号处截断,否则结果是nil。 String(describing: data):打印Data显示的是6 bytes之类的内容,而不是数据本身。请使用base64EncodedString()或String(decoding:as:)。- 把 Base64 当作安全存储:机密信息请放在钥匙串(Keychain)中;Base64 任何人都能读。见 编码与加密的区别。
常见问题
为什么 Data(base64Encoded:) 返回 nil?
对于任何无效输入,这个构造器都返回 nil 而不抛出错误:缺少填充、URL 安全的 - 或 _、空格、换行符或 data: 前缀。请先修正输入;对于分行的输入,可以传入 options: .ignoreUnknownCharacters。
Swift 原生支持 Base64URL 吗?
Foundation 没有 URL 安全选项。编码后把 + 换成 -、/ 换成 _,并去掉 =;解码前反向替换并补回填充。
在 Swift 中如何把 UIImage 转成 Base64?
先拿到字节,例如 image.pngData() 或 image.jpegData(compressionQuality: 0.8),再对结果调用 .base64EncodedString()。要检查输出,可以把它粘贴到 Base64 转图片 中。