前言做应用开发,总有些看不见但离不开的基础工具:把一段文本编码成 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}fromkit.ArkTS;util命名空间下有几个高频类:类作用util.Base64HelperBase64 编码 / 解码,支持同步与异步util.TextEncoder字符串 → UTF-8 / UTF-16 字节数组util.TextDecoder字节数组 → 字符串util.LRUCacheK,V最近最少使用缓存,自动淘汰util.RationalNumber有理数(分数)运算util.ScopeHelper区间范围判断util.types运行时类型检测(isArrayBuffer 等)本文实验室页面会重点用到前三个 LRUCache。二、Base64 编解码2.1 为什么需要 Base64Base64 把任意二进制数据用 64 个可打印字符(A-Z、a-z、0-9、、/)表示出来,主要用途是:在只能传输文本的通道里携带二进制数据。比如把图片嵌进 JSON、在 URL 里传递二进制 token、在邮件协议里附带附件。它不是加密,只是一种可打印化的编码。需要注意:Base64 处理的是字节,而不是字符。所以对一段文本做 Base64,必须先把文本按某种字符编码(通常 UTF-8)转成字节数组,再对字节数组做 Base64。这一步在很多初学者那里容易搞混。2.2 核心 APIconstbase64newutil.Base64Helper();// 编码:Uint8Array → Base64 字符串constb64:stringbase64.encodeToStringSync(bytes);// 解码:Base64 字符串 → Uint8Arrayconstback:Uint8Arraybase64.decodeSync(b64);Base64Helper还有异步版本encodeToString(src)、decode(src)(返回 Promise),以及带编码选项的重载(如util.Type.BASIC/util.Type.MIME/util.Type.URL_SAFE)。本文用同步版即可。2.3 完整编码流程在实验室页面里,一次完整的 Base64 往返如下:// 文本 → UTF-8 字节 → Base64 字符串privateencodeBase64():void{constencodernewutil.TextEncoder();constbytes:Uint8Arrayencoder.encodeInto(this.inputText);this.base64Resultthis.base64.encodeToStringSync(bytes);// 再解码回文本,验证往返一致constback:Uint8Arraythis.base64.decodeSync(this.base64Result);constdecoderutil.TextDecoder.create(utf-8);this.decodedResultdecoder.decodeToString(back);}这段代码把整个链路走了一遍:TextEncoder.encodeInto()把字符串编成 UTF-8 字节数组;Base64Helper.encodeToStringSync()把字节数组转成 Base64 字符串;解码时反向:decodeSync()得回字节数组,TextDecoder.decodeToString()还原成文本。运行后,输入HarmonyOS 鸿蒙,你会得到一串 Base64,再解码又能一字不差地还原回来——这就是编解码可逆的直观验证。易错点:直接对字符串做 Base64 是不行的,encodeToStringSync只接受Uint8Array。必须先经TextEncoder转字节。反过来解码得到的也是字节,要再用TextDecoder才能变回可读文本。三、UTF-8 字节编码3.1 TextEncoder / TextDecoderTextEncoder负责把字符串编码成字节,TextDecoder负责反向解码。它们是理解字符编码的最佳教具。// 编码constencodernewutil.TextEncoder();// 默认 UTF-8constbytes:Uint8Arrayencoder.encodeInto(鸿蒙A1);// 解码constdecoderutil.TextDecoder.create(utf-8);// 指定编码consttext:stringdecoder.decodeToString(bytes);注意早期 API 里TextEncoder的encode()方法已被标记 deprecated,推荐用encodeInto();TextDecoder用静态工厂create()构造,配合decodeToString()得到字符串。3.2 观察多字节编码实验室页面里,我们把编码后的字节数组同时用十进制和十六进制展示出来:// 文本 → UTF-8 字节数组的十进制/十六进制展示privateencodeUtf8():void{constencodernewutil.TextEncoder();constbytes:Uint8Arrayencoder.encodeInto(this.utf8Input);this.byteCountbytes.length;letdec:string;lethex:string;for(leti0;ibytes.length;i){decbytes[i].toString();hexbytes[i].toString(16).padStart(2,0).toUpperCase();if(ibytes.length-1){dec ;hex ;}}this.decListdec;this.hexListhex;}输入鸿蒙A1跑一遍,你会观察到一个重要现象:字母A、数字1各占1 个字节(它们在 ASCII 范围内);汉字鸿蒙各占3 个字节(UTF-8 对中日韩汉字通常用 3 字节表示)。所以鸿蒙A1这 4 个字符,编码后是8 个字节(3 3 1 1)。这直观解释了为什么字符长度不等于字节长度——在做数据库字段长度限制、网络封包、字符串截断时,这个区别至关重要。很多中文乱码字符串被截断成半个汉字的 Bug,根源都在这里。四、LRUCache 缓存4.1 什么是 LRULRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰策略:当缓存满了、需要腾地方时,优先淘汰最久没被访问过的那一项。它的假设很朴素——最近用过的东西,接下来大概率还会用;很久没碰的,大概率也不会再碰。util.LRUCache把这套策略封装成了一个泛型容器,你只管put(放入)、get(取出),容量控制和淘汰它全帮你搞定,还内置了命中率统计。4.2 核心 API// 创建容量为 3 的缓存constlrunewutil.LRUCachestring,string(3);lru.put(A,苹果);// 放入constvlru.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,以触发界面刷新:privatebase64:util.Base64Helpernewutil.Base64Helper();privatelru:util.LRUCachestring,stringnewutil.LRUCache(3);StatematchCount:number0;StatemissCount:number0;StatecacheSize:number0;StaterefreshFlag:number0;privaterefreshStats():void{this.matchCountthis.lru.getMatchCount();this.missCountthis.lru.getMissCount();this.cacheSizethis.lru.length;this.refreshFlag;}privateputCache(item:DataItem):void{this.lru.put(item.key,item.value);this.addLog(put(item.key) 若已满则淘汰最久未用项);this.refreshStats();}privategetCache(item:DataItem):void{constv:string|undefinedthis.lru.get(item.key);if(vundefined){this.addLog(get(item.key) → 未命中 MISS);}else{this.addLog(get(item.key) → 命中 「v」);}this.refreshStats();}这里有一个 ArkTS 状态管理的实用技巧。LRUCache 是个普通对象,它内部的变化(比如淘汰)不会被框架自动侦测。为了让某个 key 是否还在缓存能实时反映到 UI,我们引入一个自增的refreshFlag,并在判断函数里读一下它:// 借助 refreshFlag 触发重算,通过 contains 判断某 key 是否仍在缓存privateinCache(key:string):boolean{constflag:numberthis.refreshFlag;returnflag0this.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 为例:依次点放 A“放 B”“放 C” → 缓存装满 [A, B, C];点放 D → 缓存已满,淘汰最久未用的 A,变成 [B, C, D],色块 A 变灰;点查 A → 未命中(missCount 1),因为 A 已被淘汰;点查 B → 命中(matchCount 1),同时 B 被标记为最近使用;此时再放 E → 淘汰的是 C(而不是 B,因为刚查过 B),变成 [B, D, E]。第 5 步是理解 LRU 的关键:get操作也会刷新最近使用的时间戳。正因为第 4 步查询了 B,B 才在第 5 步的淘汰中幸免。你还可以点容量 容量 -动态调整容量,观察扩容后不再淘汰、缩容后立即淘汰的效果。五、页面整体结构整个实验室用一个三段式的 Tab 结构组织:EntryComponentstruct EncodeLabPage{StateactiveTab:number0;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.activeTab0){this.base64Tab()}elseif(this.activeTab1){this.utf8Tab()}else{this.lruTab()}}}.layoutWeight(1).scrollBar(BarState.Off)}}}三个 Tab 各自封装成Builder方法(base64Tab/utf8Tab/lruTab),互不干扰。结果卡片、统计盒子、Tab 按钮都抽成可复用的Builder,让主体逻辑保持清爽:BuilderresultCard(title:string,content:string,color:string){Column(){Text(title).fontColor(color).fontWeight(FontWeight.Bold)Text(content.length0?content:点击上方按钮生成结果).fontColor(content.length0?#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用熟,你就不必再为编解码和缓存去引入额外依赖了。