前言

做应用开发,总有些"看不见但离不开"的基础工具:把一段文本编码成 Base64 塞进请求体,把字符串转成字节数组做协议封包,或者用一个缓存把频繁访问的数据兜住、减少重复计算。这些活儿在其他语言里往往依赖第三方库,而在 HarmonyOS NEXT 里,@ohos.util 已经把它们打包成一套标准能力,开箱即用,零权限、纯本地、无网络依赖。

@ohos.util 是 ArkTS 标准库中最实用却最容易被忽视的模块之一。它提供了 Base64 编解码(Base64Helper)、文本与字节互转(TextEncoder / TextDecoder)、LRU 缓存(LRUCache)、有理数(RationalNumber)、区间(ScopeHelper)、类型检测(types)等一整套工具类。

本文将聚焦其中最常用的三块——Base64 编解码、UTF-8 字节编码、LRU 缓存——用一个可交互的"数据编解码实验室"页面把它们串起来:输入文本实时编码成 Base64 并往返校验、查看中英文的 UTF-8 字节展开、用可视化的方式演示 LRU 缓存的命中与淘汰。全文含完整可运行代码,适合中级开发者系统掌握这套基础工具。


一、@ohos.util 概览

引入方式(Kit 化写法):

import { util } from '@kit.ArkTS';

util 命名空间下有几个高频类:

作用
util.Base64Helper Base64 编码 / 解码,支持同步与异步
util.TextEncoder 字符串 → UTF-8 / UTF-16 字节数组
util.TextDecoder 字节数组 → 字符串
util.LRUCache<K,V> 最近最少使用缓存,自动淘汰
util.RationalNumber 有理数(分数)运算
util.ScopeHelper 区间范围判断
util.types 运行时类型检测(isArrayBuffer 等)

本文实验室页面会重点用到前三个 + LRUCache。


二、Base64 编解码

2.1 为什么需要 Base64

Base64 把任意二进制数据用 64 个可打印字符(A-Z、a-z、0-9、+、/)表示出来,主要用途是:在只能传输文本的通道里携带二进制数据。比如把图片嵌进 JSON、在 URL 里传递二进制 token、在邮件协议里附带附件。它不是加密,只是一种"可打印化"的编码。

需要注意:Base64 处理的是字节,而不是字符。所以对一段文本做 Base64,必须先把文本按某种字符编码(通常 UTF-8)转成字节数组,再对字节数组做 Base64。这一步在很多初学者那里容易搞混。

2.2 核心 API

const base64 = new util.Base64Helper();

// 编码:Uint8Array → Base64 字符串
const b64: string = base64.encodeToStringSync(bytes);

// 解码:Base64 字符串 → Uint8Array
const back: Uint8Array = base64.decodeSync(b64);

Base64Helper 还有异步版本 encodeToString(src)decode(src)(返回 Promise),以及带编码选项的重载(如 util.Type.BASIC / util.Type.MIME / util.Type.URL_SAFE)。本文用同步版即可。

2.3 完整编码流程

在实验室页面里,一次完整的 Base64 往返如下:

// 文本 → UTF-8 字节 → Base64 字符串
private encodeBase64(): void {
  const encoder = new util.TextEncoder();
  const bytes: Uint8Array = encoder.encodeInto(this.inputText);
  this.base64Result = this.base64.encodeToStringSync(bytes);

  // 再解码回文本,验证往返一致
  const back: Uint8Array = this.base64.decodeSync(this.base64Result);
  const decoder = util.TextDecoder.create('utf-8');
  this.decodedResult = decoder.decodeToString(back);
}

这段代码把整个链路走了一遍:

  1. TextEncoder.encodeInto() 把字符串编成 UTF-8 字节数组;
  2. Base64Helper.encodeToStringSync() 把字节数组转成 Base64 字符串;
  3. 解码时反向:decodeSync() 得回字节数组,TextDecoder.decodeToString() 还原成文本。

运行后,输入"HarmonyOS 鸿蒙",你会得到一串 Base64,再解码又能一字不差地还原回来——这就是编解码"可逆"的直观验证。

易错点:直接对字符串做 Base64 是不行的,encodeToStringSync 只接受 Uint8Array。必须先经 TextEncoder 转字节。反过来解码得到的也是字节,要再用 TextDecoder 才能变回可读文本。


三、UTF-8 字节编码

3.1 TextEncoder / TextDecoder

TextEncoder 负责把字符串编码成字节,TextDecoder 负责反向解码。它们是理解字符编码的最佳教具。

// 编码
const encoder = new util.TextEncoder();          // 默认 UTF-8
const bytes: Uint8Array = encoder.encodeInto('鸿蒙A1');

// 解码
const decoder = util.TextDecoder.create('utf-8'); // 指定编码
const text: string = decoder.decodeToString(bytes);

注意早期 API 里 TextEncoderencode() 方法已被标记 deprecated,推荐用 encodeInto();TextDecoder 用静态工厂 create() 构造,配合 decodeToString() 得到字符串。

3.2 观察多字节编码

实验室页面里,我们把编码后的字节数组同时用十进制和十六进制展示出来:

// 文本 → UTF-8 字节数组的十进制/十六进制展示
private encodeUtf8(): void {
  const encoder = new util.TextEncoder();
  const bytes: Uint8Array = encoder.encodeInto(this.utf8Input);
  this.byteCount = bytes.length;

  let dec: string = '';
  let hex: string = '';
  for (let i = 0; i < bytes.length; i++) {
    dec += bytes[i].toString();
    hex += bytes[i].toString(16).padStart(2, '0').toUpperCase();
    if (i < bytes.length - 1) {
      dec += ' ';
      hex += ' ';
    }
  }
  this.decList = dec;
  this.hexList = hex;
}

输入"鸿蒙A1"跑一遍,你会观察到一个重要现象:

  • 字母 A、数字 1 各占 1 个字节(它们在 ASCII 范围内);
  • 汉字"鸿""蒙"各占 3 个字节(UTF-8 对中日韩汉字通常用 3 字节表示)。

所以"鸿蒙A1"这 4 个字符,编码后是 8 个字节(3 + 3 + 1 + 1)。这直观解释了为什么"字符长度"不等于"字节长度"——在做数据库字段长度限制、网络封包、字符串截断时,这个区别至关重要。很多"中文乱码""字符串被截断成半个汉字"的 Bug,根源都在这里。


在这里插入图片描述
在这里插入图片描述

四、LRUCache 缓存

4.1 什么是 LRU

LRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰策略:当缓存满了、需要腾地方时,优先淘汰"最久没被访问过"的那一项。它的假设很朴素——最近用过的东西,接下来大概率还会用;很久没碰的,大概率也不会再碰。

util.LRUCache 把这套策略封装成了一个泛型容器,你只管 put(放入)、get(取出),容量控制和淘汰它全帮你搞定,还内置了命中率统计。

4.2 核心 API

// 创建容量为 3 的缓存
const lru = new util.LRUCache<string, string>(3);

lru.put('A', '苹果');      // 放入
const v = lru.get('A');    // 取出(会把 A 标记为"最近使用")
lru.contains('A');         // 是否包含某 key(不影响使用顺序)
lru.remove('A');           // 移除
lru.clear();               // 清空
lru.updateCapacity(5);     // 动态调整容量

// 统计
lru.length;                // 当前元素个数
lru.getCapacity();         // 容量上限
lru.getMatchCount();       // 累计命中次数
lru.getMissCount();        // 累计未命中次数
lru.getPutCount();         // 累计放入次数

其中命中/未命中统计特别适合用来教学:get 一个存在的 key,getMatchCount 加一;get 一个不存在(或已被淘汰)的 key,getMissCount 加一。这让"缓存到底有没有起作用"变得可量化。

4.3 实验室里的封装

在页面里,我们把 LRUCache 实例作为普通成员持有,并在每次操作后把统计数据同步到 @State,以触发界面刷新:

private base64: util.Base64Helper = new util.Base64Helper();
private lru: util.LRUCache<string, string> = new util.LRUCache(3);

@State matchCount: number = 0;
@State missCount: number = 0;
@State cacheSize: number = 0;
@State refreshFlag: number = 0;

private refreshStats(): void {
  this.matchCount = this.lru.getMatchCount();
  this.missCount = this.lru.getMissCount();
  this.cacheSize = this.lru.length;
  this.refreshFlag++;
}

private putCache(item: DataItem): void {
  this.lru.put(item.key, item.value);
  this.addLog('put(' + item.key + ') 若已满则淘汰最久未用项');
  this.refreshStats();
}

private getCache(item: DataItem): void {
  const v: string | undefined = this.lru.get(item.key);
  if (v === undefined) {
    this.addLog('get(' + item.key + ') → 未命中 MISS');
  } else {
    this.addLog('get(' + item.key + ') → 命中 「' + v + '」');
  }
  this.refreshStats();
}

这里有一个 ArkTS 状态管理的实用技巧。LRUCache 是个普通对象,它内部的变化(比如淘汰)不会被框架自动侦测。为了让"某个 key 是否还在缓存"能实时反映到 UI,我们引入一个自增的 refreshFlag,并在判断函数里"读一下"它:

// 借助 refreshFlag 触发重算,通过 contains 判断某 key 是否仍在缓存
private inCache(key: string): boolean {
  const flag: number = this.refreshFlag;
  return flag >= 0 && this.lru.contains(key);
}

由于 inCache 读取了 @State refreshFlag,每当 refreshStats()refreshFlag++,所有调用 inCache 的 UI 都会被重新求值,从而实时显示缓存的最新状态。这是"用一个哨兵状态变量驱动非响应式数据刷新"的常见套路。

4.4 可视化淘汰过程

页面用一排色块展示 5 个样本 key(A/B/C/D/E)当前是否在缓存里:在缓存的显示为高亮的青色,已淘汰的显示为灰色。

Row() {
  ForEach(this.samples, (item: DataItem) => {
    Column() {
      Text(item.key)
        .fontColor(this.inCache(item.key) ? '#FFFFFF' : '#94A3B8')
        .fontWeight(FontWeight.Bold)
      Text(item.value)
        .fontColor(this.inCache(item.key) ? '#CCFBF1' : '#CBD5E1')
    }
    .layoutWeight(1)
    .backgroundColor(this.inCache(item.key) ? '#0F766E' : '#E2E8F0')
    .borderRadius(8)
  })
}

配合下方的"放入"和"查询"两排按钮,你可以亲手复现 LRU 的经典行为。以容量 3 为例:

  1. 依次点"放 A"“放 B”“放 C” → 缓存装满 [A, B, C];
  2. 点"放 D" → 缓存已满,淘汰最久未用的 A,变成 [B, C, D],色块 A 变灰;
  3. 点"查 A" → 未命中(missCount +1),因为 A 已被淘汰;
  4. 点"查 B" → 命中(matchCount +1),同时 B 被标记为最近使用;
  5. 此时再"放 E" → 淘汰的是 C(而不是 B,因为刚查过 B),变成 [B, D, E]。

第 5 步是理解 LRU 的关键:get 操作也会刷新"最近使用"的时间戳。正因为第 4 步查询了 B,B 才在第 5 步的淘汰中幸免。你还可以点"容量 +""容量 -"动态调整容量,观察扩容后不再淘汰、缩容后立即淘汰的效果。


五、页面整体结构

整个实验室用一个三段式的 Tab 结构组织:

@Entry
@Component
struct EncodeLabPage {
  @State activeTab: number = 0;

  build() {
    Column() {
      // 标题栏
      Row() { /* 返回按钮 + 标题 + @ohos.util 角标 */ }
        .backgroundColor('#0F766E')

      // Tab 切换
      Row() {
        this.tabBtn('Base64', 0)
        this.tabBtn('UTF-8 字节', 1)
        this.tabBtn('LRU 缓存', 2)
      }

      // 内容区
      Scroll() {
        Column() {
          if (this.activeTab === 0) {
            this.base64Tab()
          } else if (this.activeTab === 1) {
            this.utf8Tab()
          } else {
            this.lruTab()
          }
        }
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
  }
}

三个 Tab 各自封装成 @Builder 方法(base64Tab / utf8Tab / lruTab),互不干扰。结果卡片、统计盒子、Tab 按钮都抽成可复用的 @Builder,让主体逻辑保持清爽:

@Builder
resultCard(title: string, content: string, color: string) {
  Column() {
    Text(title).fontColor(color).fontWeight(FontWeight.Bold)
    Text(content.length > 0 ? content : '(点击上方按钮生成结果)')
      .fontColor(content.length > 0 ? '#1F2937' : '#94A3B8')
      .fontFamily('monospace')
  }
  .backgroundColor('#FFFFFF')
  .borderRadius(10)
}

monospace 等宽字体展示编码结果,让 Base64 串和字节序列对齐更整齐,视觉上更接近"数据"的感觉。


六、几个实战要点与坑

6.1 字符 ≠ 字节

前面反复出现的这个主题,值得单独强调。在做以下场景时务必按字节而非字符处理:

  • 网络协议封包:长度字段填的是字节数;
  • 数据库 varchar 限制:UTF-8 下一个汉字算 3 字节;
  • 字符串安全截断:按字节截断可能切碎一个多字节字符,导致乱码。

TextEncoder.encodeInto() + Uint8Array.length 是获取真实字节长度的可靠手段。

6.2 Base64 处理的是字节

再次提醒:Base64 不能直接吃字符串。文本 → Base64 必须经过 TextEncoder;Base64 → 文本必须经过 TextDecoder。跳过这一步是新手最常见的错误。

6.3 LRUCache 的非响应式陷阱

LRUCache 是普通对象,增删不会自动触发 UI 刷新。要么像本文这样用一个 refreshFlag 哨兵状态驱动重算,要么维护一个 @State 镜像数组。切忌以为改了 LRUCache 界面就会自动更新。

6.4 UI 块内不能写普通语句

ArkTS 声明式语法要求 UI 描述块里只能有组件调用,不能写 let x = ... 这样的赋值语句。所有中间计算都要封装进方法,在 UI 中以方法调用(表达式)的形式使用,例如 this.inCache(item.key)


七、@ohos.util 的更多能力

除了本文覆盖的三块,util 还有不少值得一试的工具:

  • RationalNumber:有理数运算,util.RationalNumber.parseRationalNumber(1, 3) 精确表示 1/3,避免浮点误差,做比例、分数场景很有用;
  • ScopeHelper:区间判断,判断某值是否落在 [lower, upper] 范围内;
  • types:运行时类型检测,util.types.isArrayBuffer(x)isUint8Array(x) 等,处理未知数据结构时用得上;
  • generateRandomUUID():直接生成符合 RFC 4122 的 UUID 字符串,做唯一 id 非常方便;
  • format / printf 风格格式化:util.format('%s = %d', 'x', 42)

这些工具组合起来,基本覆盖了日常开发对"数据处理基础设施"的需求,而且全部内置、零依赖。


八、小结

本文以"数据编解码实验室"为载体,系统讲解了 HarmonyOS NEXT 标准库 @ohos.util 中三块最实用的能力:

  • Base64 编解码:Base64Helper.encodeToStringSync / decodeSync,配合 TextEncoder/Decoder 完成文本 ↔ Base64 的可逆往返;
  • UTF-8 字节编码:TextEncoder.encodeInto / TextDecoder.decodeToString,直观呈现"字符 ≠ 字节"这一核心概念;
  • LRU 缓存:LRUCache 的 put/get/contains/updateCapacity 与命中率统计,可视化演示了 LRU 的淘汰逻辑与"get 也算最近使用"的关键细节。

同时也覆盖了几个工程实践要点:字符与字节的区别、Base64 只处理字节、LRUCache 的非响应式刷新技巧、以及 ArkTS UI 块的语法限制。这些看似基础,却是构建稳健数据处理逻辑的地基。把 @ohos.util 用熟,你就不必再为编解码和缓存去引入额外依赖了。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐