鸿蒙新特性实战:@ohos.buffer 打造二进制缓冲区实验室
前言
高级应用开发迟早会碰到"二进制"这堵墙:解析一个自定义网络协议、读取一个二进制文件头、把结构化数据打包成紧凑的字节流、计算校验和、处理图片/音频的原始数据……这些场景里,你面对的不再是字符串,而是一个个字节(byte)。用字符串拼接去处理二进制,既低效又极易出错。
在 Node.js 生态里,Buffer 是处理二进制数据的事实标准。HarmonyOS NEXT 的 ArkTS 标准库把这套能力搬了过来——@ohos.buffer 提供了一个与 Node.js Buffer 高度兼容的二进制缓冲区实现:创建、读写各种数值类型、编码转换(hex / base64 / utf8)、切片、拷贝、比较,一应俱全,零权限、纯本地。
本文用一个可交互的"二进制缓冲区实验室"页面,把 buffer 最核心的两块能力讲透:文本与缓冲区的双向转换(hex / base64)、以及数值的多类型编码与大端/小端字节序对比。全文含完整可运行代码,并会把"字节序"这个二进制世界里最容易踩坑的概念讲清楚。适合想进军协议开发、二进制解析的中级开发者。
一、Buffer 是什么
Buffer 本质是一段固定长度的原始字节序列,可以把它理解成一个"字节数组",但它提供了远比数组丰富的读写接口:你可以往里按 Int8、UInt16、Int32、Float 等类型写入数值,也可以把它整体转成 hex 字符串、base64 字符串或 UTF-8 文本。
它和前面介绍过的 TextEncoder/Uint8Array 有什么区别?Uint8Array 只能逐字节存取,而 Buffer 在其之上封装了按数值类型和字节序读写的能力——这正是处理二进制协议的关键。一个 4 字节的整数怎么摆放在内存里、高位在前还是低位在前,Buffer 都用明确的 API 帮你控制。
引入方式:
import { buffer } from '@kit.ArkTS';
二、创建缓冲区
2.1 从字符串创建
const buf = buffer.from('Hi鸿蒙', 'utf8');
buffer.from(string, encoding) 把字符串按指定编码转成缓冲区。encoding 支持 'utf8'、'hex'、'base64'、'ascii'、'latin1' 等。
2.2 分配指定大小
const buf = buffer.alloc(4); // 4 字节,全部填 0
const buf2 = buffer.alloc(4, 0xff); // 4 字节,全部填 0xff
buffer.alloc(size, fill?) 分配固定长度的缓冲区并可选填充。写入数值前通常先用它按类型大小开辟空间。
2.3 常用属性与转换
buf.length // 字节数
buf.toString('hex') // 转十六进制字符串
buf.toString('base64') // 转 base64
buf.toString('utf8') // 转文本
三、实战:文本 ↔ 缓冲区
实验室第一个 Tab 演示文本与缓冲区的双向转换。
3.1 文本 → 缓冲区
@State text: string = 'Hi鸿蒙';
@State hexOut: string = '';
@State base64Out: string = '';
@State lenOut: number = 0;
// 文本 → 缓冲区,输出 hex / base64 / 字节数
private encodeText(): void {
const buf = buffer.from(this.text, 'utf8');
this.hexOut = this.formatHex(buf.toString('hex'));
this.base64Out = buf.toString('base64');
this.lenOut = buf.length;
}
输入 Hi鸿蒙,你会看到:
- 字节数 8:
H、i各 1 字节,鸿、蒙各 3 字节(UTF-8); - hex:
48 69 E9 B8 BF E8 92 99——前两个是 ASCII 的 H(0x48)、i(0x69),后六个是两个汉字的 UTF-8 编码; - base64:
SGnpuL/oktk=。
为了让 hex 更易读,我们把连续的十六进制串每字节插一个空格:
private formatHex(hex: string): string {
let out: string = '';
for (let i = 0; i < hex.length; i += 2) {
out += hex.substring(i, i + 2).toUpperCase();
if (i + 2 < hex.length) {
out += ' ';
}
}
return out;
}
3.2 缓冲区 → 文本(反向解码)
@State hexIn: string = '48 69';
@State decodeOut: string = '';
@State decodeError: string = '';
// hex 字节串 → 缓冲区 → 文本
private decodeHex(): void {
this.decodeError = '';
try {
const clean: string = this.hexIn.replace(/\s/g, '');
if (clean.length % 2 !== 0) {
this.decodeError = 'hex 长度必须为偶数';
return;
}
const buf = buffer.from(clean, 'hex');
this.decodeOut = buf.toString('utf8');
} catch (e) {
this.decodeError = '非法的 hex 字符串';
}
}
这里做了两层健壮性处理:先用正则 replace(/\s/g, '') 去掉用户输入里的空格,再校验长度必须为偶数(每字节两个 hex 字符),最后用 try-catch 兜住非法字符。输入 48 69 会解码回 Hi。这种"hex 转文本"在调试协议报文时特别常用——抓包工具给你一串 hex,你想知道它对应什么内容。
四、实战:数值编码与字节序
这是本文的重点,也是 Buffer 相比普通字节数组最有价值的能力。
4.1 为什么数值需要"编码"
一个整数 300 在内存里怎么存?它超过了一个字节(0~255)的范围,需要 2 个字节。300 的十六进制是 0x012C,拆成两个字节就是 01 和 2C。但问题来了:这两个字节谁在前?
- 大端序(Big-Endian, BE):高位字节在前 →
01 2C; - 小端序(Little-Endian, LE):低位字节在前 →
2C 01。
这就是"字节序"(Endianness)。不同 CPU 架构、不同协议规定了不同的字节序。网络协议(TCP/IP)约定用大端(所以大端又叫"网络字节序"),而 x86、ARM 等主流 CPU 内部用小端。如果发送方和接收方对字节序的理解不一致,同一段字节会被解读成完全不同的数值——这是二进制协议里最经典的 bug 来源。
4.2 多类型 + 双字节序编码
实验室让你选择数据类型和输入数值,同时用大端和小端两种方式编码,直观对比:
interface NumType {
label: string;
size: number;
}
private numTypes: NumType[] = [
{ label: 'Int8', size: 1 },
{ label: 'UInt8', size: 1 },
{ label: 'Int16', size: 2 },
{ label: 'UInt16', size: 2 },
{ label: 'Int32', size: 4 },
{ label: 'UInt32', size: 4 },
{ label: 'Float', size: 4 }
];
// 同一数值分别用大端 / 小端写入,对比字节序
private encodeNumber(): void {
this.numError = '';
const t: NumType = this.numTypes[this.typeIndex];
const isFloat: boolean = t.label === 'Float';
const v: number = isFloat ? parseFloat(this.numInput) : parseInt(this.numInput, 10);
if (isNaN(v)) {
this.numError = '请输入有效数值';
return;
}
try {
const be = buffer.alloc(t.size);
const le = buffer.alloc(t.size);
switch (t.label) {
case 'Int8':
be.writeInt8(v, 0); le.writeInt8(v, 0); break;
case 'UInt8':
be.writeUInt8(v, 0); le.writeUInt8(v, 0); break;
case 'Int16':
be.writeInt16BE(v, 0); le.writeInt16LE(v, 0); break;
case 'UInt16':
be.writeUInt16BE(v, 0); le.writeUInt16LE(v, 0); break;
case 'Int32':
be.writeInt32BE(v, 0); le.writeInt32LE(v, 0); break;
case 'UInt32':
be.writeUInt32BE(v, 0); le.writeUInt32LE(v, 0); break;
case 'Float':
be.writeFloatBE(v, 0); le.writeFloatLE(v, 0); break;
default:
break;
}
this.beHex = this.formatHex(be.toString('hex'));
this.leHex = this.formatHex(le.toString('hex'));
} catch (e) {
this.numError = '数值超出 ' + t.label + ' 的表示范围';
this.beHex = '';
this.leHex = '';
}
}
Buffer 为每种数值类型都提供了成对的 BE/LE 写入方法:
| 类型 | 大端方法 | 小端方法 | 字节数 |
|---|---|---|---|
| Int8 / UInt8 | writeInt8 |
(无字节序之分) | 1 |
| Int16 / UInt16 | writeInt16BE |
writeInt16LE |
2 |
| Int32 / UInt32 | writeInt32BE |
writeInt32LE |
4 |
| Float | writeFloatBE |
writeFloatLE |
4 |
| Double | writeDoubleBE |
writeDoubleLE |
8 |
每个方法签名是 writeXxx(value, offset),offset 是写入起始位置,单缓冲区从 0 开始。
4.3 观察实验结果
输入 300、选 Int16,你会看到:
BE 大端:01 2C
LE 小端:2C 01
字节顺序正好相反。切到 Int32,同样是 300:
BE 大端:00 00 01 2C
LE 小端:2C 01 00 00
现在 4 个字节,大端把有效字节放在末尾,小端放在开头。再试 Int8 输入 100,BE 和 LE 都是 64——因为单字节没有"顺序"可言,这也是表格里 Int8/UInt8 不分 BE/LE 的原因。
试试输入超范围的值,比如 Int8 输入 300(超过 127 上限),writeInt8 会抛异常,被 catch 捕获后提示"数值超出 Int8 的表示范围"。这演示了类型的取值边界:Int8 是 -128~127,UInt8 是 0~255,Int16 是 -32768~32767,以此类推。
再试 Float 输入 3.14,你会看到它被编码成 IEEE 754 单精度浮点的 4 个字节,BE 与 LE 同样顺序相反。这解释了浮点数在二进制里的存储方式。


五、页面结构与交互
整个实验室是"双 Tab"结构,主体简洁:
@Entry
@Component
struct BufferLabPage {
@State activeTab: number = 0;
build() {
Column() {
// 标题栏
Row() { /* 返回 + 标题 + @ohos.buffer 角标 */ }
// Tab 切换
Row() {
this.tabBtn('文本缓冲', 0)
this.tabBtn('数值编码', 1)
}
Scroll() {
Column() {
if (this.activeTab === 0) {
this.textTab()
} else {
this.numberTab()
}
}
}
.layoutWeight(1)
}
}
}
三个交互点分布在两个 Tab:
- 文本 → 缓冲区:输入文本实时显示 hex / base64 / 字节数;
- hex → 文本:输入 hex 字节串反向解码回文本;
- 数值编码:选择类型 + 输入数值,同时显示大端和小端的字节表示。
页面在 aboutToAppear 里把三块都先算一遍,保证打开即有内容:
aboutToAppear(): void {
this.encodeText();
this.decodeHex();
this.encodeNumber();
}
数值输入框用 InputType.NUMBER_DECIMAL 限制只能输入数字,减少非法输入:
TextInput({ text: this.numInput })
.type(InputType.NUMBER_DECIMAL)
.onChange((v: string) => { this.numInput = v; this.encodeNumber(); })
字节序对比用两张卡片上下并列,分别用不同的强调色(BE 深蓝、LE 青色)区分,hex 用 monospace 等宽字体展示,让字节对齐一目了然。
六、几个实战要点与坑
6.1 字节序必须与协议约定一致
这是二进制开发的头号原则。解析或封装协议前,先查清协议规定的字节序(多数网络协议是大端)。读写时选对 BE/LE 方法,否则数值全错。调试时如果读出来的数值大得离谱或符号错乱,第一反应就该怀疑字节序用反了。
6.2 数值范围会溢出抛异常
writeInt8 写入超过 127 的值、writeUInt8 写入负数,都会抛异常。凡是写入外部来源的数值,都要用 try-catch 兜底,并向用户给出范围提示。
6.3 offset 别越界
writeXxx(value, offset) 的 offset + 类型字节数不能超过缓冲区长度,否则抛 RangeError。往一个 alloc(2) 的缓冲区里 writeInt32BE 会越界。分配大小要与写入类型匹配。
6.4 hex 解码要校验偶数长度
每个字节对应两个 hex 字符,输入的 hex 串长度必须是偶数。去空格后先校验 length % 2 === 0,再解码,能提前拦住大部分非法输入。
6.5 Buffer 与 Uint8Array 的关系
Buffer 底层兼容 Uint8Array,很多需要 Uint8Array 的 API(如加密、压缩模块)可以直接接收 Buffer。反过来,你也可以用 buffer.from(uint8Array) 把已有字节数组包装成 Buffer 以使用其丰富的读写方法。
七、典型应用场景
- 自定义协议封包/解包:按协议字段顺序
writeUInt16BE(长度)、writeUInt8(类型)、write(负载),或反向readXxx解析; - 二进制文件头解析:读取 PNG、WAV 等文件头的魔数、宽高、采样率等字段;
- 校验和计算:遍历缓冲区字节求和、异或,生成 checksum;
- 数据紧凑存储:把多个数值打包成最小字节流,节省存储或传输;
- 与硬件/蓝牙通信:BLE 特征值、串口数据往往是裸字节,用 Buffer 组织读写。
八、小结
本文以"二进制缓冲区实验室"为载体,系统讲解了 HarmonyOS NEXT 标准库 @ohos.buffer 的核心用法:
- 创建:
buffer.from(string, encoding)/buffer.alloc(size, fill); - 编码转换:
toString('hex' / 'base64' / 'utf8'),以及 hex 反向解码; - 数值读写:
writeInt8/writeInt16BE/LE/writeInt32BE/LE/writeFloatBE/LE等成对的大端/小端方法; - 核心概念:字节序(大端 vs 小端)——二进制协议里最容易踩坑却最关键的知识点;
- 工程要点:字节序须与协议一致、数值溢出与 offset 越界的异常处理、hex 偶数长度校验、Buffer 与 Uint8Array 的互通。
Buffer 是打开二进制世界的钥匙。掌握它,你就能从容应对协议开发、文件解析、硬件通信等所有"和字节打交道"的场景,而不必再用脆弱的字符串拼接去硬扛二进制数据。
更多推荐




所有评论(0)