惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
Vercel News
Vercel News
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
Jina AI
Jina AI
B
Blog
Recorded Future
Recorded Future
MyScale Blog
MyScale Blog
I
InfoQ
aimingoo的专栏
aimingoo的专栏
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
爱范儿
爱范儿
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
博客园 - Franky
Schneier on Security
Schneier on Security
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
H
Hacker News: Front Page
Cloudbric
Cloudbric
D
DataBreaches.Net
B
Blog RSS Feed
P
Palo Alto Networks Blog
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
I
Intezer
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyberwarzone
Cyberwarzone
F
Fortinet All Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
K
Kaspersky official blog
Forbes - Security
Forbes - Security

博客园 - y丶innocence

临时111 RecyclerView 数据多时无法滑动:ConstraintLayout 约束高度修复笔记 Android Kotlin OkHttp3 WebSocket 长连接与 Gson 数据解析系统笔记 Android + Kotlin + OkHttp WebSocket 相关概念与使用流程笔记(TLS/证书 + 鉴权/会话) AI对话导出markdown格式流程 代理转发 分享文件 安卓导出笔记(未整理) java&kotlin listener 0.7 动画 0.4 View 工作流程 0.3 view 滑动冲突 13. Jetpack 0. 安卓开发艺术探索参考资料 12. Material Design 7. 持久化技术 5. Fragment java 基础 4. UI 开发 3. Activity 2.2 Kotlin 面向对象 2.3 Kotlin高级 2.1 Kotlin基础 1. Android简介 [OpenJudge] 反正切函数的应用 (枚举)(数学) [OpenJudge] 摘花生 (模拟)
面向全球的app的excel导出和kotlin IO原理
y丶innocence · 2026-04-15 · via 博客园 - y丶innocence

学习笔记:全球 App 的数据导出与 IO 流深度解析

一、 核心概念扫盲:Stream 与 Writer 的本质区别

Java/Kotlin 的 IO 体系分为两套完全不同的顶层逻辑,理解它们是组合流的前提:

1. Stream(字节流):InputStream / OutputStream

  • 本质:面向底层二进制的搬运工。它眼里没有文字,只有 0~255 的数字(字节)。
  • 适用场景:一切文件的本质都是字节。图片、视频、音频、ZIP 压缩包,甚至文本文件在硬盘上也是字节。
  • 特点:它是个纯粹的管道,不懂任何编码规则。如果你直接用 FileOutputStream.write("中文".toByteArray()),它只是把当前系统默认编码转出的字节盲写进去,换台电脑可能就乱码了。

2. Writer / Reader(字符流):Writer / Reader

  • 本质:面向人类文字的处理器。它眼里处理的是 Char(Unicode 字符,如 'A'、'中'、'😊')。
  • 适用场景:专门用来处理纯文本。
  • 特点:它自己不能直接连硬盘,它必须“骑”在字节流上面。

3. 编码桥梁:OutputStreamWriter / InputStreamReader

  • 本质:这是一个适配器。它左半边接字符(懂 Unicode),右半边接字节流(懂二进制)。
  • 作用:当你指定 Charsets.UTF_8 时,你写进去的 "中文",会被它精确地按照 UTF-8 规则翻译成特定的字节序列(28个字节),再交给底层的 Stream。

二、 CSV 与 XLSX 导出的字符集策略

核心原则:全球化的 App,底层最终落到硬盘上的字节必须是 UTF-8。

1. CSV 导出:UTF-8 + 必须加 BOM

  • 痛点:CSV 是裸奔的纯文本,没有文件头声明编码。Windows 下的 Excel 双击打开时,会按系统默认编码(如中文系统是 GBK)强行解析 UTF-8,导致全球字符(中日韩、Emoji等)全部乱码。
  • BOM 的作用:BOM(Byte Order Mark)是 UTF-8 的专属标识符(十六进制 EF BB BF)。加上它,Excel 读到前三个字节就会明白:“这是 UTF-8”,从而正确显示。
  • 代码体现:向 Writer 写入的第一个字符必须是 \uFEFF(BOM 的 Unicode 转义形式)。

2. XLSX 导出:UTF-8 + 绝对不能加 BOM

  • 本质:XLSX 根本不是文本文件,它是一个 ZIP 压缩包(内部打包了一堆 XML 文件)。
  • 为什么不能加 BOM:ZIP 格式有严格的文件头要求(必须以 50 4B 即 PK 开头)。如果在最外层强行塞入 BOM 字节,直接破坏了 ZIP 结构,导致 Excel 报错“文件已损坏”。
  • 如何声明编码:依赖 XLSX 内部的 XML 文件头声明:<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

三、 IO 流组合的深度剖析(为什么这么用?)

这是 IO 设计模式中经典的装饰器模式,像套娃一样,每一层解决一个特定问题。

场景 A:CSV 导出(纯文本的层层包装)

你的代码:BufferedWriter(OutputStreamWriter(FileOutputStream(file), Charsets.UTF_8))

  1. FileOutputStream(file) —— 建立物理管道
    • 为什么用:没它就没文件。它在操作系统层面打开了一个通往硬盘的通道,只允许字节通过。
  2. OutputStreamWriter(..., Charsets.UTF_8) —— 装上“翻译引擎”
    • 为什么用:因为你不想手动把字符串转成 ByteArray。把它套在 FileOutputStream 外面,它就接管了通道入口。你只管扔字符串进去,它保证按 UTF-8 标准翻译成字节。
  3. BufferedWriter(...) —— 装上“内存加速器”
    • 为什么用:性能救星。如果没有它,你每写一个字段(哪怕是一个逗号),OutputStreamWriter 就要立刻翻译并让操作系统往硬盘写一次。磁盘 I/O 是极其慢的。BufferedWriter 内部默认开了 8KB 的内存缓存,你写的数据先全塞内存里,攒够了一块儿往下扔,性能提升成百上千倍。
    • 附加价值:提供了 newLine() 方法,不用你去管 Windows 是 \r\n 还是 Mac 是 \n

场景 B:XLSX 导出(二进制结构的拼装)

你的代码:ZipOutputStream(FileOutputStream(file))

  1. FileOutputStream(file) —— 建立物理管道(同上)
  2. ZipOutputStream(...) —— 装上“压缩打包引擎”
    • 为什么用:因为你要生成 XLSX(ZIP格式)。如果你直接往 FileOutputStream 写 XML 字符串,它就变成了一个 .xml 文件,而不是 .xlsx。
    • 它的作用:ZipOutputStream 继承自字节流,它拦截你写出的所有字节,在内存中按照 ZIP 算法进行压缩,并自动帮你生成复杂的 ZIP 文件头、中央目录结构,最后把打包好的 ZIP 字节流交给底层的 FileOutputStream。
    • 注意:它是字节流,你不能直接 zos.write("字符串")

🎯 场景 B 的关键延伸:XLSX 里面怎么写文本?

因为 ZipOutputStream 是字节流,当你要往 XLSX 里写入具体的 XML 内容时,必须在它内部再套一层字符流桥梁:

ZipOutputStream(FileOutputStream(file)).use { zos ->
    zos.putNextEntry(ZipEntry("xl/worksheets/sheet1.xml")) // 准备打包某个内部文件
    // 此时必须在 zos 上面再套一层 Writer 来写字符串!
    OutputStreamWriter(zos, Charsets.UTF_8).use { writer ->
        writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") // 声明编码,不加 BOM!
        writer.write("<worksheet>...</worksheet>")
    }
    zos.closeEntry()
}

对比总结:CSV 是“字符流包字节流”的一根通杆;XLSX 是“字节流包字节流(ZIP),内部局部再包字符流”的复杂结构。

四、 Kotlin 的链式 use:极简且绝对正确的资源释放

你的写法:BufferedWriter(...).use { bw -> ... }

这是 Kotlin 中最标准、最优雅的写法。只需要外层包一个 use,底层原理极其精妙:

· use 的本质:它是一个内联高阶函数,无论 Lambda 里面是正常跑完还是抛出异常,退出前都会在 finally 块中自动调用最外层 BufferedWriter 的 close() 方法。
· 为什么只包一层 use 就够了?(关键机制:传递性关闭)
Java/Kotlin IO 规范有严格的契约:外层装饰器关闭时,必须自动调用内层被装饰者的 close()。
当触发 bw.close() 时,源码逻辑是:先自动调用 flush() 把缓冲区数据强制推下去 -> 然后调用 OutputStreamWriter.close() -> 再调用 FileOutputStream.close()。

因此,手动嵌套写 fos.use { osw.use { bw.use { } } } 在底层是完全冗余的。单层链式包装 + 单个 use,既清爽又绝不会发生句柄泄露。