ArkTS+pako:预置字典版本错配解压诊断【鸿蒙心迹】
诊断包下载成功,字节也没少,解压却停在一个含糊的错误码上。遇到这种现象,排查清单里通常会有文件损坏、网络截断和压缩格式,预置字典很容易被漏掉。设定一个更具体的场景:应用把重复出现的日志字段放进字典,旧包使用旧字典,新版本又调整了字段。如果读取端总拿“当前默认字典”解压,升级反而可能切断历史数据的读取路径。下面的 DictBridge 只处理这一件事:让每份压缩数据找到自己的字典,并把失败原因讲清楚。
## 一、先固定一份能够对照的压缩数据本篇使用 OHPM 发布的 pako 2.1.0,类型声明为 @types/pako 2.0.0,分别通过 ohpm install pako@2.1.0 和 ohpm install @types/pako@2.0.0 --save-dev 安装。这是本次实际核对的发行组合,不代表所有 HarmonyOS SDK 都已经完成构建验证。上游网站当前展示的文档版本已变化,阅读时应回到对应发行包,尤其不要混用不同版本的选项名称。OHPM 元数据、类型声明元数据。Demo 的页面叫 DictCheckPage.ets,计算部分集中在 DictionaryCodec.ets。页面只接收状态、错误码和还原文本;字典选择与压缩算法不放进布局代码。以下代码解决生产端漏记字典版本的问题。两个字典都是本文编写的静态 ASCII 示例,任务号固定为 DB-026。typescriptimport * as pako from 'pako';interface DictionaryEntry { id: string; content: string; }interface TracePacket { taskId: string; codec: string; dictionaryId: string; payload: Uint8Array;}const V1: DictionaryEntry = { id: 'trace-v1', content: 'level=debug|module=storage|event=read|status=ok|trace='};const V2: DictionaryEntry = { id: 'trace-v2', content: 'level=info|module=network|event=request|status=ok|trace='};const SAMPLE: string = V2.content + 'DB-026';function makePacket(): TracePacket { return { taskId: 'DB-026', codec: 'zlib', dictionaryId: V2.id, payload: pako.deflate(SAMPLE, { dictionary: V2.content }) };}````dictionaryId` 和 `payload` 在同一次构造中绑定,避免先压缩、随后又读取一个可变的全局版本号。`deflate` 的 `dictionary` 选项接受本例的字符串字典;这里生成的是带 zlib 包装的数据。相关定义已对照 [pako 2.1.0 发行包](https://ohpm.openharmony.cn/ohpm/pako/-/pako-2.1.0.tgz)中的实现与类型声明。应用层对象不是网络协议的完整定义。真正写入文件或发送请求时,还要约定清单与二进制负载怎样组织,不能把 `Uint8Array` 随意转成 JSON 数组后,假定接收方自然懂得还原。是否引入字典,也要算完整成本。本例字典本身有五十六字节,如果每次发送一份二十一字节的负载,还附带整份字典与清单,总流量未必减少。这个方案适合双方能够重复使用同一字典的情况,不能用负载变短,替代对实际分发开销的判断。## 二、版本名相同,还不够预置字典可以理解为压缩双方预先共享的一段字节。压缩数据引用其中的内容,读取端必须提供相同字节才能正确还原。zlib 格式用 `FDICT` 标志表示需要字典,并保存字典的 Adler-32 校验值;它不会替应用下载字典,也不会认识 `trace-v2` 这个业务名称。[RFC 1950 的格式说明](https://www.rfc-editor.org/rfc/rfc1950)。因此需要两层约束。业务版本名负责查找资源,格式里的校验负责发现内容对不上。`trace-v2` 一旦发布,就应绑定固定内容;以后调整字段顺序、换行或编码,都生成新版本,不在原名称下覆盖文件。资源发布时还应把字典和使用它的配置视作同一批变更。先让读取端具备新字典,再允许生产端生成新版本数据;回滚时保留已经产生的数据所需资源。字典可以按需获取,但获取失败应成为可见状态,不能悄悄改用旧字典继续试,也不能把一次缺资源记成文件永久损坏。本例采用 ASCII,是为了让字符长度与字节数一致。正式字典含中文时,必须统一编码,记录的是实际字节内容,不能拿字符串长度当文件长度。同样,Adler-32 是格式校验值,不能当作可信来源证明;通过网络更新字典时,完整性和来源校验需要由应用自己的资源分发方案完成。还要把格式写明。本文不把带字典的 zlib 数据改称 gzip,也不通过开启 `raw` 跳过包装层来“修复”错误。那样已经改变了解码约定,原来的字典识别依据也会随之变化。## 三、错误码 2 不能直接翻译成“没有下载字典”诊断页保留一个原始探测入口,故意允许把错误字典交给解码器。这是为了观察库行为,正式读取入口稍后还会检查版本。下面的函数解决另一个常见误判:`push` 没抛异常,不等于已经拿到完整文本。它同时检查提交结果、错误码和最终结果类型。typescriptinterface ProbeReport { nativeCode?: number; state: string; text: string;}function probeDictionary(payload: Uint8Array, dictionary: string): ProbeReport { const decoder = new pako.Inflate({ dictionary, to: ‘string’ }); const accepted: boolean = decoder.push(payload); if (decoder.err === pako.constants.Z_NEED_DICT) { return { nativeCode: decoder.err, state: ‘DICTIONARY_UNRESOLVED’, text: ‘’ }; } if (!accepted || decoder.err !== pako.constants.Z_OK || typeof decoder.result !== ‘string’) { return { nativeCode: decoder.err, state: ‘DECODE_FAILED’, text: ‘’ }; } return { nativeCode: 0, state: ‘READY’, text: decoder.result };}````to: ‘string’ 是该发行版的文本输出选项。Z_NEED_DICT的值为2。更容易忽略的是,2.1.0 的高层 Inflate会把设置错误字典时底层返回的Z_DATA_ERROR转回Z_NEED_DICT。缺字典和错字典,都可能落在这里;只看错误码无法分清二者。[对应发行包中的 lib/inflate.js](https://ohpm.openharmony.cn/ohpm/pako/-/pako-2.1.0.tgz)。本例的两种失败里,msg都是空字符串,页面不能只显示它。探测报告保留原始码,界面再结合数据声明的版本、用户所选版本解释原因。声明trace-v2却选了trace-v1,展示 DICTIONARY_MISMATCH,原始探测码仍显示 2,两者属于不同层次。排查顺序也随之清楚:先看压缩格式,再看声明版本是否存在,接着比较所选版本与实际资源内容,最后才追查其他解码失败。不要把所有非零错误都归到字典上;数据真的损坏时,继续轮流尝试字典只会增加噪声,让最初的故障信息更难保留。图中是开发界面演示:左侧为 DictBridge 工程,中间为 probeDictionary,右侧模拟器显示版本错配,底部记录 task=DB-026 expected=trace-v2 selected=trace-v1 nativeCode=2。该日志文本是 Demo 的展示约定,不冒充真实 IDE 截图。## 四、正式入口要先匹配,重试要换实例生产路径没有必要把已经知道不匹配的资源交给库试错。下面的入口先检查格式声明和字典版本,再把字典校验不匹配与其他解压失败分开,异常也不会被当成空日志返回。```typescriptfunction decodePacket(packet: TracePacket, selected: DictionaryEntry | undefined): ProbeReport { if (packet.codec !== 'zlib') { return { state: 'FORMAT_UNSUPPORTED', text: '' }; } if (!selected) { return { state: 'DICTIONARY_MISSING', text: '' }; } if (packet.dictionaryId !== selected.id) { return { state: 'DICTIONARY_MISMATCH', text: '' }; } try { const report = probeDictionary(packet.payload, selected.content); if (report.nativeCode === pako.constants.Z_NEED_DICT) { report.state = 'DICTIONARY_CONTENT_MISMATCH'; } return report; } catch { return { state: 'DECODE_EXCEPTION', text: '' }; }}```版本检查失败时没有调用解码器,因此报告不填写原始错误码。界面用短横线显示“未调用”,不能伪造一个库错误。版本名一致却仍然需要字典时,DICTIONARY_CONTENT_MISMATCH 只是排查分类:除资源被覆盖、清单标错或编码变化,也要检查压缩负载头中的 DICTID 是否损坏,不能据此认定字典文件出了错。这里检查的是清单中的格式声明,Inflate默认仍可自动识别 zlib 和 gzip。若实际协议要求只接受 zlib,还需显式设置windowBits: 15,并用异格式负载验证拒绝路径,不能把声明检查当成字节格式验证。页面开始一次新检查时,应先清除上一次还原文本,再提交本次报告。否则错误状态下面可能残留旧任务的正确内容,看起来像“虽然报错但已经恢复”。任务号、选中字典和负载也要在点击时形成同一份输入快照,结果区展示这次快照,避免用户切换选择后读到上一次的解释。页面中的“使用 trace-v2 重试”调用 decodePacket(packet, V2),它再次进入 probeDictionary,自然创建新的 Inflate。2.1.0 的实例在上述字典错误后已经结束,向原实例再次 push会返回false;补齐字典后不能接着用原来的对象。这几个函数没有定时器、文件句柄或网络订阅,局部解码器随调用结束失去引用。页面退出时不保留大型负载和结果即可。若以后加入异步字典下载,下载任务和页面状态更新需要另外管理,不能借“换了一个解码器”来代替取消旧请求。## 五、把恢复结果和排查证据放在一起对同一份完整负载,三组检查应当保留:不提供字典、提供 V1、提供 V2。前两组用于验证错误分支,最后一组还要比较还原全文,不能只看状态变绿。| 原始探测条件 | push返回值 | 原始码 | 文本结果 || --- | --- | --- | --- || 空字典字符串 | false | 2 | 无结果 ||V1.content| false | 2 | 无结果 ||V2.content| true | 0 | 与 SAMPLE 一致 |本次使用从 OHPM 下载的原始发行包,在 Node.js 环境检查了这些纯库行为:示例文本为 62 字节,压缩负载为 21 字节;V2 还原后逐字一致。这些数字只属于给定的静态样例,不能推成设备性能或真实日志压缩收益。接入后的回归还需要保留真正的历史压缩样本,不能每次用当前字典重新压缩再解压;后一种方法会绕开版本兼容问题。还应安排一次同名字典内容被改动的检查,确认它进入内容错配分支,并检查缺失字典后来补齐时,恢复操作使用的确实是原负载,而不是新生成的数据。手机演示页保留任务DB-026、数据声明与当前字典 trace-v2、状态 READY、原始码 0,同时展示此前 trace-v1 / code 2的探测记录。顶部时间固定为演示时间10:24`。这样读者能看到问题经过,而不只是一个成功提示。## 六、升级策略决定历史数据能留多久字典版本的寿命应跟数据寿命一起考虑。若旧诊断包还在有效期内,读取端就要保留对应字典,或提供明确的可恢复获取路径。删除旧字典之前,先确认仍依赖它的数据范围;客户端安装了新版本,不代表存量数据也自动迁移了。本文验证了发行包的纯计算语义,未在 DevEco Studio、HarmonyOS 模拟器或真机完成构建与运行验证;两张技术配图均为生成的演示图。接入实际工程后仍应验证包导入、目标 SDK 编译、页面退出和历史样本回放。字典名负责定位,内容必须固定,失败后创建新实例——这三项保持一致,历史数据才有清楚的读取路径。
更多推荐





所有评论(0)