标签:Harmony os、ArkTS、资源编码、数据校验、像素图纸
一张 70×70 拼豆图有 4900 个位置。如果把每个位置直接写成包含 row、col、colorId、colorCode、hex 和 isEmpty 的对象,资源文件会迅速膨胀,人工审查也几乎不可能。工程采用了更紧凑的形式:每行是一段字符串,每个字符代表空格或色板索引,加载时再展开为完整 BeadCell[]。
当前资源层包含 50 张图,每张都有 70 行施工图、16 行预览和最多 16 个颜色。仅施工图就有 245,000 个字符位置。单字符编码解决了"源码如何保存"的问题,却没有自动解决运行时内存、非法字符、行宽不一致和色板越界。本篇将编码表、解码过程与资源校验放在同一条链路中分析。

一、资源对象只保存三类信息
每张资源图由颜色数组、完整图纸行和预览行组成:
typescript
export interface AssetChart {
colors: string[];
chartRows: string[];
previewRows: string[];
}
当前每个 chartRows 含 70 个长度为 70 的字符串,previewRows 含 16 个长度为 16 的字符串。以 50 张图计算:
text
完整图纸字符数 = 50 × 70 × 70 = 245,000
预览字符数 = 50 × 16 × 16 = 12,800
合计位置数 = 257,800
这些数字还没有计入引号、逗号和换行,但已经能说明编码规模。若每个位置都写成对象,同一个 colorId、行列坐标和字段名会重复数十万次;字符串行则把重复信息推迟到加载阶段生成。
二、一个字符刚好覆盖 16 色与空格
解码表采用三段规则:
| 字符 | 含义 | 返回索引 |
|---|---|---|
. |
空格 | -1 |
0---9 |
色板前 10 色 | 0---9 |
A---F |
色板后 6 色 | 10---15 |
对应实现保持为纯整数映射:
typescript
private static assetColorIndex(token: string): number {
if (token === '.') {
return -1;
}
const digitIndex = '0123456789'.indexOf(token);
if (digitIndex >= 0) {
return digitIndex;
}
const letterIndex = 'ABCDEF'.indexOf(token);
if (letterIndex >= 0) {
return 10 + letterIndex;
}
return -1;
}
因此 0 不是"无颜色",而是第 0 个颜色;只有 . 表示空格。小写 a、字母 G、空格字符和其他符号都会返回 -1。这个约定让一个 UTF-16 代码单元就能表达一个格子,也把色板上限明确固定为 16。

三、色板顺序就是文件协议的一部分
资源文件只保存十六进制颜色,没有保存色号。仓库解码时按固定顺序补上业务色号:
typescript
const codes: string[] = [
'H7', 'G1', 'D9', 'A12', 'F1', 'D11', 'H6', 'H2',
'H5', 'E1', 'M12', 'H14', 'F16', 'F17', 'M5', 'G18'
];
于是字符与色号的关系由数组位置决定:0 -> H7,9 -> E1,A -> M12,F -> G18。如果只调整 colors 顺序,却没有同步重新编码所有行,图形轮廓仍然完整,颜色却会整体错位。这类错误比解析失败更隐蔽,因为页面仍然能正常渲染。
可以把三者看作一个不可拆分的版本:
text
字符 token -> palette index -> color code / hex
任何一段改变,都应重新核对固定像素样本,而不是只验证数组长度。
四、解码时才创建完整 BeadCell
createCellsFromRows() 按行和列遍历字符串,取出一个字符,解析色板索引,再生成业务格子:
typescript
private static createCellsFromRows(
id: string,
palette: PaletteColor[],
rows: string[]
): BeadCell[] {
const cells: BeadCell[] = [];
for (let row = 0; row < rows.length; row++) {
const sourceRow = rows[row];
for (let col = 0; col < sourceRow.length; col++) {
const token = sourceRow.substring(col, col + 1);
const colorIndex = PatternRepository.assetColorIndex(token);
const color = colorIndex < 0 || colorIndex >= palette.length
? null
: palette[colorIndex];
cells.push(PatternRepository.createCell(id, row, col, color));
}
}
return cells;
}
展开后的每个格子拥有稳定坐标和完整颜色信息,页面、统计服务与 PNG 导出服务都不再关心资源字符。这是很有价值的边界:紧凑格式只存在于资源层,业务层始终处理统一模型。
五、非法字符被当作空格,容错也可能掩盖错误
当前实现把两种情况都转换为 null 颜色:
- 字符无法识别,例如
G或小写a。 - 字符能得到索引,但索引超出当前色板长度。
最终生成的格子会是 isEmpty=true。运行时因此不会崩溃,但资源录入错误会表现为图案缺一颗豆,而不是明确的异常。若缺口位于大面积背景中,人工预览很难发现。
更适合资源构建阶段的策略是"严格校验,运行时保底":
typescript
interface AssetIssue {
row: number;
col: number;
token: string;
reason: string;
}
开发时扫描全部行并报告精确坐标;正式运行时仍可保留空格回退,避免单个坏字符让整个图库无法打开。两层策略各自解决不同问题。
六、行宽不一致会让二维坐标悄悄错位
图纸宽度当前取第一行长度:
typescript
private static rowWidth(rows: string[]): number {
if (rows.length === 0) {
return 0;
}
return rows[0].length;
}
解码循环却按每一行自己的 sourceRow.length 追加格子。假设宽度记录为 70,而第 20 行只有 69 个字符,从该行之后,扁平数组按 width=70 重新分组时就会整体左移;若某行多了一个字符,后续又会整体右移。
这不是单行少一格,而是从错误点开始污染余下所有行。因此资源校验至少要满足:
text
chartRows.length == 70
every chartRows[row].length == 70
previewRows.length == 16
every previewRows[row].length == 16
colors.length <= 16
宽高如果未来不固定,也应先从元数据得到期望值,再验证每一行,而不是默认相信第一行。

七、紧凑资源不等于低运行时内存
一次 PatternRepository.getPatterns() 会创建全部 50 张完整图和预览图:
text
每张格子对象数 = 70 × 70 + 16 × 16 = 5,156
50 张对象数 = 5,156 × 50 = 257,800
资源文件确实紧凑,但调用后仍会展开为 257,800 个 BeadCell 对象。每个对象还携带字符串 ID、色号和颜色值,真实堆占用远高于字符数量。
这说明"存储格式"和"加载策略"是两个独立维度:
- 单字符行减少源码体积与资源维护成本。
- 按需解码或缓存最近访问项,才减少首屏对象分配。
若首页只展示缩略图,可以先解码 16×16 预览;进入编号页时再解码对应 70×70 图纸。这样仍然复用同一套资源协议,却不必在应用初始化时展开所有完整矩阵。
八、50 个 if 分支可以继续收敛,但先保留静态类型
当前 PatternAssetCharts.get(id) 通过 50 个明确的 ID 分支返回资源。优点是结构直观、构建时可获得类型约束;缺点是文件超过五千行,查找、合并和人工校对成本高。
可演进为按分类拆分,再用显式映射汇总:
typescript
const animeCharts: Record<string, AssetChart> = { /* ... */ };
const gameCharts: Record<string, AssetChart> = { /* ... */ };
static get(id: string): AssetChart | null {
return animeCharts[id] ?? gameCharts[id] ?? null;
}
重点不是换一种语法,而是让每个资源仍然经过同一校验入口。若直接读取外部 JSON,还需要处理字段缺失、字符编码和类型收窄,不能因为文件更短就省略边界验证。
九、资源扫描应输出可复算摘要
对 50 张图执行资源扫描时,建议每张至少输出以下摘要:
text
id=anime-1
palette=16
chart=70x70
preview=16x16
invalidTokens=0
raggedRows=0
usedColorIndexes=0..15
总览再核对:
- 图案 ID 是否与仓库中的 50 个种子一一对应。
- 是否存在资源但没有种子,或种子没有资源。
- 字符索引是否超出色板。
- 空格比例是否异常突变。
- 预览与完整图的主色分布是否相近。
这些摘要比逐行阅读 70 个长字符串更有效,也能在资源更新后快速发现结构变化。
十、编码上限变化时必须显式升级协议
0-9A-F 的优势是一个字符覆盖 16 色。如果未来需要 20 色,不能随意把 G-J 接在后面而不记录版本,因为旧解码器会把新字符当作空格。可以选择:
- 明确扩展字母表并增加资源版本。
- 改用两字符定长索引。
- 使用调色板压缩后的二进制资源。
对当前 16 色业务,单字符方案足够简单且可读。真正需要提前设计的是版本识别和失败方式,而不是为了假设中的规模立即引入复杂格式。
小结
Harmony os 应用中的图纸资源可以很紧凑:用 . 表示空格、0-9A-F 表示 16 个色板索引,50 张 70×70 图与 16×16 预览共承载 257,800 个字符位置;加载后再统一展开为 BeadCell[],让页面、统计和导出服务不感知底层编码。
这套方案真正的工程边界有三条:色板顺序属于协议,不能单独调整;每行宽度必须在解码前验证;紧凑存储不能替代按需加载。把资源扫描、运行时回退和对象展开分开设计,单字符编码才能同时获得可维护性、可定位性与稳定渲染结果。