上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Swift
代码

Swift 中的 Base64

Swift 借助 Foundation 的 Data 处理 Base64:一个方法负责编码,一个可失败构造器负责解码。下面的代码片段在 Linux 上的 Swift 6.1 中运行过,它使用与苹果平台相同的 Foundation API。

编码和解码字符串

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 转图片 中。

继续阅读