需要使用 Base64 格式吗?那么本网站正好适合您!使用我们的在线工具对数据进行编码或解码,便捷好用。

Kotlin 中的 Base64 解码:完整指南

你正盯着一个死活读不出来的值:SGVsbG8sIFdvcmxkIQ==。一串字母和数字,偶尔混进一个 +/,末尾通常还挂着一两个 =。这就是 Base64,而本指南要讲的是如何把它还原回原本的样子 - 一句话、一张图片、一个证书、一坨二进制 - 用 Kotlin 的方式。深入之前先花一句话温习一下:Base64 把每三个字节打包成四个字符,取自一张 64 符号的字母表,结尾一小截 = 填充标记出真实数据在哪里停止。格式的完整巡礼在首页,所以这里只用一句话,外加一句:因为四个字符装的是三个字节的东西,文本形式比原始数据大约长出三分之一。

Kotlin 的好消息是:你根本不需要任何包。标准库自带 Base64 实现已经很多年了;自 Kotlin 2.2 起它完全稳定,并且运行在 Kotlin 能运行的每一个平台上,从你笔记本上的 JVM 到 Android 手机,再到 Node.js,再到 WASI 边缘函数。下面所有内容都用你项目自带的 Kotlin 就能跑。

先说好消息:你到底需要什么

Gradle 里没有叫 base64 的构件可以添加,没有 NuGet 式的包,也没有 npm 模块。你要的类是 kotlin.io.encoding.Base64,它本身就是 Kotlin 标准库的一部分。只要你会写 println,你就能解码 Base64。一个 Kotlin 项目里有三个 API 能干 Base64 的活,选对哪个是第一个真正的决定:

API 它运行在哪里 何时选它
kotlin.io.encoding.Base64 所有 Kotlin 平台:JVM、Android、JS、Native、Wasm 默认之选。自 Kotlin 2.2 起稳定,多平台,现代 API
java.util.Base64 仅限 JVM(Java 8+;Android 上需 API 26+) 本来就活在 Java 互操作地带、只做 JVM 的代码库
android.util.Base64 仅限 Android(API 8+) 遗留 Android 代码,或者你确实需要它的标志常量

有两条版本备注值得知道。第一,标准库的这个类最早出现在 Kotlin 1.8.20(2023 年 4 月),身后挡着 @ExperimentalEncodingApi 这道门;Kotlin 2.0.20 带来了 withPadding 旋钮和严格的填充规则,Kotlin 2.2.0(2025 年 6 月)让 API 稳定下来并加上了 PEM 实例。所以在 Kotlin 2.2 或更新版本上 - 包括当前稳定线 2.4.x - 你可以零注解地使用本指南的一切。第二,如果你的项目钉在 1.8 到 2.1 之间的某个 Kotlin 版本,同一个类存在但被标记为实验性,编译器不允许你在函数上不加 @OptIn 注解就用它。

有一个安装陷阱已经坑掉过不止一个下午:Debian 和 Ubuntu 仓库里的 kotlin 包是 1.3.31 版本,比标准库 Base64 API 出现得还早,所以它连本文的一个例子都编译不了。改从 GitHub 上的 Kotlin 发布页或 SDKMAN 拿编译器,在 Gradle 项目里则显式钉住插件:

plugins {
  kotlin("jvm") version "2.4.10"
}

第一次解码:两行代码,一个字节结果

整个仪式装得进两条语句,而经典的 TWFu 字符串是个不错的起点:

import kotlin.io.encoding.Base64
fun main() {
  val packed = "TWFu"
  val bytes = Base64.decode(packed)
  println(bytes.decodeToString())  // Man
}

慢慢读这段,因为里面藏着三个设计决定。第一,不带任何 .DefaultBase64.decode(...) 不是笔误:Default 是这个类的伴生对象,所以直接在类上调函数,就是在 Base64.Default 上调它的简写。你在较旧的教程里也会看到 Base64.Default.decode(...),意思完全一样。第二,这一点看起来不起眼,其实更重要:decode 交给你的是一枚 ByteArray,绝不是 String。载荷可能是一张 JPEG、一个 X.509 证书或一句话,API 拒绝去猜是哪一个,所以字节到文本的那一跳是单独的、刻意的一步。第三,字符集的决定就住在那一步里,而大多数"我的 Base64 解出来全是乱码"的 bug 就出生在那里。我们马上就到那一步;先来一次往返,证明解码是忠实的:

import kotlin.io.encoding.Base64
fun main() {
  val original = "Hello, World!".encodeToByteArray()
  val packed = Base64.encode(original)
  val back = Base64.decode(packed)
  println(packed)                    // SGVsbG8sIFdvcmxkIQ==
  println(back.contentEquals(original))  // true
}

四个方案,四种性格

这个类从不被实例化;你从四个现成的实例里挑一个,而每个解码时脾气都不一样:

import kotlin.io.encoding.Base64
fun main() {
  val data = "Hello?".encodeToByteArray()
  println(Base64.Default.encode(data))  // SGVsbG8/
  println(Base64.UrlSafe.encode(data))  // SGVsbG8_
  println(Base64.Mime.encode(data))     // SGVsbG8/
  println(Base64.Pem.encode(data))      // SGVsbG8/
}
实例 字母表 它如何解码
Base64.Default A-Z a-z 0-9 + / 严格:字母表外的任何字符都抛出异常;要求有填充
Base64.UrlSafe A-Z a-z 0-9 - _ 严格,但针对的是 URL 字母表;输入里出现 +/ 就抛出异常
Base64.Mime A-Z a-z 0-9 + / 宽容:忽略换行符和其他非字母表字符,但 = 填充之后不许再跟任何东西;要求有填充
Base64.Pem A-Z a-z 0-9 + / 宽容,规则与 Mime 相同;这是同一字母表的 PEM/PKI 风味

宽容与严格这条分界线,是整篇文章里最值得记进脑子的东西。DefaultUrlSafe 把任何外来字符都当成案发现场,立即抛出异常。MimePem 对换行、空格和零散的标点只是耸耸肩 - 因为真实的邮件和证书文件里装的就是这些 - 但它们也不是无限的宽容:一旦有数据字符出现在填充之后,连它们也会抛出异常。稍后的失败图鉴里你会看到确切的错误消息。

性格带来的另一个后果:一个方案读不了另一个方案的输出。把一个 base64url token 喂给 Base64.Default- 不在它的字母表里,于是你得到 IllegalArgumentException: Invalid symbol '-'(55) at index ...。拿不准一个字符串从哪来时,挑与生产者匹配的方案,而不是挑合你心情的那个。

URL 安全的 Base64 与 JWT

标准字母表里有俩字符,只要数据一踏上 URL 就会惹祸。查询串里的 + 在被任何东西读取之前,通常早就被重新解释成空格了;/ 是路径分隔符,在 URL 段里压根不能出现。RFC 4648 第 5 节的做法是交换字母表最后两个符号:+ 变成 -/ 变成 _。你听到最多的名字是 base64url,在 Kotlin 里它就是 Base64.UrlSafe

base64url 最大的消费者是 JSON Web Token。紧凑形式的 JWT 由三段 base64url 用点号连接:header.payload.signature。RFC 7515 规定这些部分是 不带填充的 base64url,这是它与朴素字母表的第二处不同,不只是字符不同。下面这个 token 正在被拆开检查:

import kotlin.io.encoding.Base64
fun main() {
  val token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
  val (header, payload, signature) = token.split(".")
  val lenient = Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
  println(lenient.decode(header).decodeToString())
  // {"alg":"HS256"}
  println(lenient.decode(payload).decodeToString())
  // {"sub":"1234567890","name":"John Doe"}
  println(signature.length)  // 43
}

有两处值得注意。token 的各段都不带填充,但开箱即用的 Base64.UrlSafe 要求有填充,所以 withPadding(PRESENT_OPTIONAL) 这一行在干实事:带填充和不带填充的输入它都收。split(".") 加上解构,也只是普通 Kotlin 在做格式要求它做的事。一条严肃的警告:拆开 JWT 是为了一个 token,不是为了信任它。解码之后头部和载荷都只是普通数据;只有经过验证的签名才说明 token 是真的,而那需要一个真正的 JWT 库,不是手写的字符串拆分。

填充模式与严格度旋钮

在 Kotlin 里,填充不是 Base64 的固定事实,而是一个设置。每个实例都携带一个 PaddingOption,四个预设实例都从 PRESENT 起步,withPadding 会递给你一个换了设置的新实例,原来的原封不动。下面逐个选项过一遍这个旋钮:

选项 不带填充的输入 带正确填充的输入
PRESENT(到处都是默认) 抛出异常 解码
ABSENT 解码 抛出异常
PRESENT_OPTIONAL 解码 解码
ABSENT_OPTIONAL 解码 解码
import kotlin.io.encoding.Base64
fun main() {
  val data = "Hello".encodeToByteArray()
  println(Base64.Default.withPadding(Base64.PaddingOption.ABSENT).encode(data))
  // SGVsbG8
  println(Base64.Default.encode(data))
  // SGVsbG8=
  val eitherWay = Base64.Default.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
  println(eitherWay.decode("SGVsbG8").decodeToString())   // Hello
  println(eitherWay.decode("SGVsbG8=").decodeToString())  // Hello
}

解码这一侧,PRESENT_OPTIONAL 是你的安全网:它说的是"我不知道发信人垫没垫填充,而我打算继续工作"。其他组合的错误消息出乎意料地有帮助,所以当严格解码器遇到错误输入时,你一眼就能认出它们:PRESENT 下缺少填充产生 The padding option is set to PRESENT, but the input is not properly paddedABSENT 下出现填充产生 The padding option is set to ABSENT, but the input has a pad character at index 7。有一个行为值得单独点名,因为它总会让人意外:像 SGVsbG8== 这样的双重填充不是"多来一点但没关系"。第一个 = 结束了数据,第二个出现在本该是数据的位置,所以哪怕最宽容的解码器也会拒收它。

这里还藏着一段版本史。如果你接手了针对 1.8.x 实验性 API 编写的代码,记住旧的 decode 对带填充和不带填充的输入都照收。Kotlin 2.0.20 里,Default 搬到了严格的 PRESENT 规则下,于是曾经好用的无填充输入,在升级到那个点版本之后就会抛出异常。修复只要一行:withPadding(Base64.PaddingOption.PRESENT_OPTIONAL),或者在解码前把输入归一化。

从字节到文本:字符集与 Unicode

拿到你的 ByteArray 之后,问题是它意味着什么。如果载荷是文本,默认答案是 decodeToString(),它把字节按 UTF-8 解释,在每个平台上都能工作。对于现代 API、邮件和网络数据这些常见情况,这是你唯一需要的,表情符号也算在内:

import kotlin.io.encoding.Base64
fun main() {
  val original = "héllo 😀"
  val packed = Base64.encode(original.encodeToByteArray())
  println(packed)                                  // aMOpbGxvIPCfmIA=
  println(Base64.decode(packed).decodeToString())  // héllo 😀
}

不过,发送方一旦用了 UTF-8 以外的东西,字符集的决定就落在你头上。Kotlin 内置的文本转换是刻意只做 UTF-8:decodeToString() 没有字符集参数,带字符集参数的字符串转字节函数也不存在。在 JVM 上你要降到平台的字符集 API,它是诚实而明确的:

import kotlin.io.encoding.Base64
import java.nio.charset.Charset
fun main() {
  val latinOne = "héllo".toByteArray(Charsets.ISO_8859_1)
  val packed = Base64.encode(latinOne)
  println(packed)  // aOlsbG8=
  val asUtf8 = Base64.decode(packed).decodeToString()
  val asLatin = String(Base64.decode(packed), Charsets.ISO_8859_1)
  println(asUtf8)   // h?llo(é 字节不是有效的 UTF-8)
  println(asLatin)  // héllo
  val byName = String(Base64.decode(packed), Charset.forName("ISO-8859-1"))
  println(byName)   // héllo
}

中间那行的 ? 不是字体问题;它是 U+FFFD,Unicode 替换字符,替一个构不成合法 UTF-8 的字节顶岗。解码后看到一排这样的字符,说明你的载荷没事 - 是你的字符集假设出事了。还有一个会咬人的不对称也值得注意:编码一侧有 JVM 扩展 toByteArray(charset);解码一侧对应的构造器是 String(bytes, charset)。两者都不收字符集名字;要按名字找得用 Charset.forName("..."),它会给编出来的名字抛 UnsupportedCharsetException,所以配置值里的拼写错误会快速失败,而不是默默换成另一种编码。

既然身在字节世界,再说一个 Kotlin 特有的陷阱:Char 是一个 16 位的值,对它调 toByte() 会悄悄只保留低八位。如果你从字符手工拼字节,"中".first().code.toByte() 给你 45,一个和这个字符毫无关系的数字。正确的路永远是 encodeToByteArray(),它做真正的编码工作 - 同一个字符是三个 UTF-8 字节,它的 Base64 形式是 5Lit。让标准库去编码,永远别手工把字符塞进字节。

文件、子串与大输入

Base64 数据在内存里不总是一根规整的字符串。有时它是一个文件、一个更大响应里的切片,或者大到没法一次装下。Kotlin 把三扇门都给你。

文件是最无趣的情况,好得不能再好:读字节,解码,完事。两套标准文件 API 都行,用你项目已经在用的那套:

import java.io.File
import kotlin.io.encoding.Base64
import kotlin.io.path.Path
import kotlin.io.path.readBytes
fun main() {
  val fromFile = File("payload.b64").readBytes()
  println(Base64.decode(fromFile.decodeToString()).size)  // 解码出的字节数
  val fromPath = Path("payload.b64").readBytes()
  println(Base64.decode(fromPath.decodeToString()).size)  // 同一个数字
}

子串是 CharSequence 重载大显身手的地方。decode 接受任何带起止索引的字符序列,所以你可以直接把一个长响应体的切片交给它,不用先把切片复制出来:

import kotlin.io.encoding.Base64
fun main() {
  val body = "prefix junk SGVsbG8= trailing junk"
  val bytes = Base64.decode(body, 12, 20)
  println(bytes.decodeToString())  // Hello
}

如果你已经知道输出大小、想复用缓冲区,decodeIntoByteArray 会写进你选定的目标数组,并告诉你写入了多少字节。给它的缓冲区太小,它会抛 IndexOutOfBoundsException,消息里还带着所需的容量,这个错误顺便就是你的尺寸提示。

对于 JVM 上真正的大流,还有第三扇门:流式解码器。它们仍标记为实验性 - 所以有那个 opt-in 注解 - 而且只存在于 JVM,但它们一边读一边解码,而不是把一切都装进内存:

import java.io.ByteArrayInputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
@OptIn(ExperimentalEncodingApi::class)
fun main() {
  val stream = ByteArrayInputStream("SGVsbG8gV29ybGQh".toByteArray())
  stream.decodingWith(Base64.Default).use {
    println(it.readBytes().decodeToString())  // Hello World!
  }
}

两个实用细节。扩展函数住在包的顶层,所以按名字导入(星号导入也行,但按名字更友好)。而且解码器把填充当成硬性刹车:如果底层流在 Base64 段之后还在继续,从解码流里读取会在 = 处结束,剩下的字节仍留在原流里可用。对于那些把 Base64 别在其他东西前面的格式,这就很干净。

实战:HTTP API 与 JSON 请求体

JSON 装不了原始字节 - 它是文本协议 - 所以需要搬运二进制(图片、证书、任意字节块)的 API 几乎总是把它包进 Base64,塞在字符串字段里。套路是:解析 JSON,取字段,解码。有了官方序列化库,JSON 这部分离你只有两个注解的距离:

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
@Serializable
data class ImageResponse(val name: String, val data: String)
fun main() {
  val body = """{"name":"icon.png","data":"iVBORw0KGgo="}"""
  val response = Json.decodeFromString<ImageResponse>(body)
  val bytes = Base64.decode(response.data)
  println("${response.name}: ${bytes.size} bytes")  // icon.png: 8 bytes
}

这个例子需要序列化插件和库,往构建里加一次:

plugins {
  kotlin("jvm") version "2.4.10"
  kotlin("plugin.serialization") version "2.4.10"
}
dependencies {
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
}

没有库也一样能在原始字符串上玩转这个思路,写快脚本时很顺手:用 substringBetween 把字段抠出来再解码。坑都是 API 上熟悉的那些:字段可能是一整个 data URL(带 data:image/png;base64, 前缀,本文后面会处理),载荷可能经过 MIME 换行包裹,编码后的载荷可能比原始二进制大约三分之一,所以在大型响应上盯紧你的内存预算。

实战:邮件与 MIME 换行输入

邮件是一个 7 位文本世界,RFC 2045 对二进制附件的回答是带着一转的 Base64:编码输出必须换行包裹,任何一行不得超过 76 个字符。如果你收到过被当成文本的附件,它看起来就像一列缩进的 Base64,原因在此。对于恰好是这种输入,Base64.Mime 才是对路的解码器,因为它一路忽略换行符和其他非字母表字符:

import kotlin.io.encoding.Base64
fun main() {
  val wrapped = "SGVs\nbG8=\r\n"
  println(Base64.Mime.decode(wrapped).decodeToString())  // Hello
  val withJunk = "Y@{mFz!Z!TY}0"
  println(Base64.Mime.decode(withJunk).decodeToString())  // base64
}

这份宽容是真的,但有边界。输入被换行包裹、再撒上一两个空格,没问题。但只要在最后一个 = 后面添一个数据字符,连 Mime 也会抛:Symbol 'e'(145) at index 7 is prohibited after the pad character。还要记住 Mime 仍要求填充在场且正确;一个连缺失填充都照吞的 MIME 解码器就是自找麻烦。对付杂乱邮件载荷的实用菜谱是先用 Mime 解码,如果它抛了,看消息 - 它会精确告诉你哪个符号、在哪个索引、违反了规则。

实战:图片与 data URL

data URL 是网页把文件直接内联进文档的方式:一个媒体类型、一个 base64, 标记和载荷,全塞进一个字符串。浏览器、CSS 和内嵌 UI 对小资源偏爱它们 - 图标、头像、占位图 - 因为不用发第二个请求。格式长这样:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==

在 Kotlin 里解码一个,是一次字符串操作加一次 Base64 解码。前缀没什么秘密;逗号之后全是载荷:

import kotlin.io.encoding.Base64
fun main() {
  val dataUrl = "data:image/png;base64,iVBORw0KGgo="
  val mediaType = dataUrl.substringBefore(";")
  val packed = dataUrl.substringAfter("base64,")
  val bytes = Base64.decode(packed)
  println(mediaType)          // data:image/png
  println(bytes.size)         // 8
  println(bytes.contentToString())  // [-119, 80, 78, 71, ...]
}

第一个字节 -119(即 0x89),后面跟着 PNG 三个字母,就是识别 PNG 文件的魔数。解码后检查头四个或八个字节,是确认 data URL 里真有它前缀所声称内容的便宜办法。两个诚实的提醒:Base64 会让体积大约增加三分之一,所以 data URL 是你拿体积去换一次网络往返的交易;而对任何大的东西,通常更划算的是从真实 URL 提供文件,让缓存去干活。

实战:配置、环境变量与数据库

只要二进制值必须搭乘一条只走文本的通道,Base64 就会出现在配置文件和环境变量里:properties 文件里嵌的一枚小图标,容器环境变量里存的一个 token,因为 schema 比正经的二进制类型出现得早而被停放在文本列里的一坨字节。解码一侧到处都是同样两步 - 读文本,解码:

import kotlin.io.encoding.Base64
fun main() {
  val line = "icon: UE5HREFUQQ=="
  val packed = line.substringAfter("icon: ").trim()
  val bytes = Base64.decode(packed)
  println(bytes.decodeToString())  // PNGDATA
  val fromEnv: String? = System.getenv("MY_ICON_B64")
  if (fromEnv != null) {
    println(Base64.decode(fromEnv).size)
  }
}

这一整片区域的坑一句话装得下:Base64 不是加密。它是运输手法,不是锁。谁都不该读到一个 Base64 值就以为里面的数据藏起来了;它离可见只差一个函数调用,而且在你写的每一行日志里都可见。如果某个值是敏感的,让它从头到尾保持敏感 - 密钥库、加密列、你的技术栈提供什么就用什么 - 只把 Base64 用来让字节穿过文本,而不是用来保护它们。

实战:命令行

最古老的用例:把命令行上的一个 Base64 团变成文件。一个完整的工具只要八行 Kotlin,因为重活是标准库干的。用 Kotlin 编译器编译一次,它永远归你:

import java.io.File
import kotlin.io.encoding.Base64
fun main(args: Array<String>) {
  val packed = if (args.isNotEmpty()) args[0] else readlnOrNull().orEmpty()
  val bytes = Base64.decode(packed.trim())
  File("decoded.bin").writeBytes(bytes)
  println("Wrote ${bytes.size} bytes to decoded.bin")
}

带参数运行处理一次性值,或者把一个文件管道进去做批处理:程序有第一个参数就读它,没有就退回标准输入。这里的 trim() 在默默值班,因为 shell 参数和粘贴进来的值总爱带着零碎空白,严格解码器会拒收它们。如果你的载荷是 base64url,把 Base64.decode 换成 Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL).decode,这个工具也就准备好接 token 了。

解码失败图鉴

本指南里的每个解码器失败时只抛两种异常类型,而每条消息都具体到能告诉你到底哪里错了。这是完整的地图,附标准库产生的确切消息:

情形 异常 消息(原文照发)
字母表外的字符(空格、换行、别的方案的符号) IllegalArgumentException Invalid symbol ' '(40) at index 5
填充之后的数据字符 IllegalArgumentException Symbol 'e'(145) at index 7 is prohibited after the pad character
选项是 PRESENT 时缺少填充 IllegalArgumentException The padding option is set to PRESENT, but the input is not properly padded
选项是 ABSENT 时填充在场 IllegalArgumentException The padding option is set to ABSENT, but the input has a pad character at index 7
索引超出源的范围 IndexOutOfBoundsException startIndex: 0, endIndex: 100, size: 8
startIndex 大于 endIndex IllegalArgumentException startIndex: 3 > endIndex: 2
decodeIntoByteArray 的目标缓冲区太小 IndexOutOfBoundsException The destination array does not have enough capacity, destination offset: 0, destination size: 2, capacity needed: 8

注意前两行的模式:消息会点名肇事的符号、括号里它的数值代码、以及它的索引。这是调试礼物。当解码在生产上抛出异常时,记下输入的前几十个字符和消息里的索引,你几乎总能几秒内揪出元凶 - 不管它是粘贴进来的换行、被截断的载荷,还是溜进标准解码器的 base64url 字符串。

专门咬 Kotlin 开发者的坑

  • 假设载荷是文本。decode 返回 ByteArray 是故意的。因为"它八成是文本"就对 JPEG 调 decodeToString(),得到的是一整面替换字符墙。转换之前先弄清这些字节是什么。
  • 复制粘贴的空白。默认解码器是严格的,而从聊天消息或日志里搬来的值几乎总是带着尾随换行或前导空格。解码前先 trim,或者走 Mime 解码,要么接受 IllegalArgumentException 并处理它。
  • 从实验时代升级而来。为 1.8.x 实验性 API 写的代码带着 @OptIn(ExperimentalEncodingApi::class) 注解,并且依赖填充是可选项。从 2.0.20 起,同样的输入可能抛出异常。修复是 PRESENT_OPTIONAL,或者在输入到达解码器之前把它们清理干净。
  • 把方案和错误生产者配错。Base64.Default 解码 JWT 段,会败在它的 -_ 字符上;用 UrlSafe 解码标准字母表载荷,会败在 +/ 上。异常会点名确切的符号,但修复在于知道字符串从哪来。
  • 字符集的空档。decodeToString() 只做 UTF-8,没有其他编码的重载。如果发送方用了 Latin-1 或 Windows-1252,在 JVM 上就计划用 String(bytes, charset),忘了的话,U+FFFD 替换字符就是症状。
  • 发行版软件包的编译器。Debian 和 Ubuntu 上 apt install kotlin 给的是 1.3.31,早于这个 API 存在的时候。如果你的例子突然以"unresolved reference"为由拒绝编译,检查一下 PATH 上实际是哪个编译器。

解码最佳实践

  • 先解码到字节,后解释。Base64.decode 和文本转换保持为分开的两步。它让字符集显式化,让二进制载荷保持二进制,也让测试变得平凡:比较字节数组,而不是字符串。
  • 挑与生产者匹配的实例。JWT 和注定上 URL 的数据意味着 UrlSafe;邮件和 PEM 文件意味着 MimePem;其他一切从 Default 开始。宽容解码器是给已知杂乱的输入准备的,不是通用安全网。
  • 对不可信输入做一次便宜归一化。在严格解码之前 trim() 一下、在格式已知干净的地方再去掉空白,能抓到的真实失败比多少 try-catch 都多。一个带 PRESENT_OPTIONAL 回退的小助手,是处理来源不明值的好模式。
  • 分配之前先算尺寸。解码输出至多是输入长度的四分之三(四个符号装三个字节),所以快速长度检查能在解码前告诉你目标大小 - 这正是你往预分配缓冲区里填数据、或接收一个好几兆的字符串之前想要的。
  • 相信错误消息。标准库会报告符号、它的代码和它的索引。对不可信输入记下那个索引附近的输入,然后别再猜了。
  • 别为了藏东西而解码,也别为了证明什么而解码。Base64 是传输编码。它既不添秘密也不添完整性;如果你需要其中任何一个,那是密码学的活,不是解码器的活。

Base64 如何进入 Kotlin

Base64 比 Kotlin 年长好几十年 - 给出 76 字符行规则的 MIME 规范要追溯到 1993 年,字母表本身则要追溯到 1990 年代中期的 RFC - 但 Kotlin 专属的故事短而新。kotlin.io.encoding 包在 2023 年 4 月的 Kotlin 1.8.20 中登场,带着 Base64 和三个实例 - DefaultUrlSafeMime - 挡在 @ExperimentalEncodingApi 注解之后,还有至今仍为实验性的 JVM 专属流式扩展。两年里,用它意味着每个函数里一行 opt-in,外加 API 可能挪窝的一点风险。

2025 年 6 月发布的 Kotlin 2.2.0 改变了契约。整个 API 在一个版本里全部稳定,Pem 实例加入家族(RFC 1421 的 64 字符行变体,用在 PKI 周围)。让 1.8 时代填充可选的代码在升级后需要关注的严格性,早一步到来:2.0.20 里,带着四个 PaddingOption 值的 withPadding 取代了旧的固定行为,解码器开始要求填充。2.2 这个版本还稳定了 kotlin.text 里的兄弟类 HexFormat,那个自 Kotlin 1.9 起就是实验性的十六进制格式化 API,所以字节级文本编码如今在标准库里有了安定的家。维护层面再说一句:自 Kotlin 2.4.0 起,JVM 标准库每条发布线带 18 个月支持窗口,这又多了一个理由,让运行在当前 2.4.x 线上的项目把这个 API 当作固定点,而不是移动靶。

趣闻

  • 伴生对象在干活。因为 DefaultBase64 的伴生对象,类名兼职做默认实例:Base64.decode(x)Base64.Default.decode(x) 是同一个调用。这就是本文开头那个两行例子能保持两行的原因。
  • 字符串解码在 JVM 上有速度技巧。通用解码循环在字节上工作,但 Kotlin 字符串是字符序列。JVM 实现绕开了转换:在共享循环跑起来之前,把 String 的字符重新解释为单字节 ISO-8859-1 值 - 源码注释声称这个技巧比通用路径快至多十倍,这就是 decode(String) 在长载荷上也有秒解手感的原因。
  • 包名是个暗示。这个 API 住在 kotlin.io.encoding,不在 kotlin.text,因为整点在于数据是字节 - 输入输出都是 I/O 形状,文本只是结果后来被弄成的样子。
  • 错误消息带字符的代码。Symbol 'e'(145) 报告肇事符号的八进制数值,而不只是字形。元凶是空白时特别好用:' '(40) 让你早就知道那是个空格,而不用先对它起疑。
  • PEM 到得很晚。Base64.Pem 不在最初的 1.8.20 API 里;它是随 2.2 稳定化一起登场的。如果 2023 或 2024 年的博客文章只列了三个实例,它没有错 - 只是落后了两个版本。
  • 它是按平台写的,不是委托的。标准库用 expect/actual 函数为每个目标单独实现编解码器。JVM 上甚至有一段被注释掉的优化,想把活交给 java.util.Base64,因为一个未关闭的编译器问题被禁用 - 这也是为什么 Kotlin 实现的行为在每个平台上都是参考行为。

收尾

在 Kotlin 里解码 Base64,归结为一小串刻意的选择:与数据来源匹配的实例、与发送方式匹配的填充模式、与体积匹配的缓冲区或流、与含义匹配的字符集。标准库把这四样全作为无依赖的普通函数交到你手上,而它的错误消息具体到失败本身就是一份诊断,而不是谜。反方向 - 当你是生产 Base64 的那一方,要为它选对方案、填充和换行 - 有它自己的决定和陷阱,姊妹站点上的相关文章深入讲了 Kotlin 里的 Base64 编码。

最后更新: 2026-09-08

相关文章: Kotlin 中的 Base64 编码:完整指南