一个很耗人的现象:Debug 包怎么点都正常,CI 出的 Release 包一到线上就出事。数据库 insert 成功,query 抛 no such column: a1;或者更安静一点,查询不报错,但字段读出来全是 null。你以为是混淆规则漏了,往 obfuscation-rules.txt 里加 -keep,加完 UI 好了数据库还坏;再加一批 -keep-property-name,终于全绿,但混淆收益几乎归零。下一轮迭代加了个新实体,同样的问题换个地方再来一次。

根因不在 keep 规则写得够不够多,在于你把不稳定的东西写进了持久化契约。数据库列名、状态观测点这些必须长期稳定的“名字”,被绑在了会被混淆改写的 ArkTS 属性名上。只要这条隐式依赖还在,keep 名单就永远补不完。

混淆改的是属性名,不是“代码里看起来的那个字符串”

先看一段看起来毫无问题的代码。

typescript

@ObservedV2
export class AccountEntity {
  userId: string = '';
  @Trace displayName: string = '';
  @Trace balance: number = 0;
}

// 写入:把实体直接当 ValuesBucket 用
await store.insert('account', entity as object as relationalStore.ValuesBucket);

// 查询:用属性名取列下标
const idx = rs.getColumnIndex('displayName');
const name = rs.getString(idx);

这段代码假设了两件事:属性名在运行时不会被改写,属性名恰好等于数据库列名。开启 -enable-property-obfuscation 之后,displayName 在产物里可能变成 a1,getColumnIndex('displayName') 去找一个不存在的列,query 就抛 no such column;或者建表时列名已经被写成了短名,老用户升级后旧表和新代码对不上,触发一次意料之外的迁移。

@ObservedV2 / @Trace 的失效是同一根因的另一面。状态观测点依赖属性名来建立依赖关系,属性名被混淆改写之后,观测链在运行时找不到原来的锚点。它不报错,只是不刷新。你把同样的代码放回 Debug 包,又完全正常,因为 Debug 编译默认不开启混淆。

Debug 和 Release 的差异不止混淆一项,排查时别只切构建模式

官方在提示里专门纠正过一个排查动作:若要确认行为差异是否由混淆引起,应该通过开启或关闭混淆开关来对比,而不是只切换 Debug/Release 构建模式。因为 Debug 和 Release 之间还有压缩、-remove-log 等差异,只切构建模式会把混淆问题和其它构建差异搅在一起,排查方向反而被带偏。

最直接的验证方式是在 CI 上打一个关了混淆的 Release 包,装到同一台设备上跑一遍。如果关了混淆就正常,开了就崩,问题锁定在混淆;如果关了混淆照样崩,那就要往压缩或构建配置的其它环节上查。

列名要显式,不要让属性名既当代码标识又当持久化契约

解法不是往 keep 名单里堆更多规则,而是把“持久化用的名字”从属性名上摘下来,让两者解耦。

最直接的做法是在实体定义里显式写出列名,写入和查询都走这个显式映射,不依赖属性名在运行时长什么样。

typescript

const COLUMN_MAP: Record<string, string> = {
  userId: 'user_id',
  displayName: 'display_name',
  balance: 'balance'
};

function toBucket(entity: AccountEntity): relationalStore.ValuesBucket {
  return {
    [COLUMN_MAP.userId]: entity.userId,
    [COLUMN_MAP.displayName]: entity.displayName,
    [COLUMN_MAP.balance]: entity.balance
  };
}

const COLUMN_DISPLAY_NAME = COLUMN_MAP.displayName; // 'display_name'
const idx = rs.getColumnIndex(COLUMN_DISPLAY_NAME);

toBucket 把实体翻译成显式的列名映射,getColumnIndex 用的是常量 COLUMN_DISPLAY_NAME,不是字符串字面量 'displayName'。混淆可以改属性名,但改不了 'display_name' 这个值,也改不了你显式写在映射表里的键。持久化契约从此和 ArkTS 属性名脱钩。

建表语句同样要用显式列名,不要在 CREATE TABLE 里直接拼属性名。

typescript

const SQL_CREATE = `
  CREATE TABLE IF NOT EXISTS account (
    user_id TEXT PRIMARY KEY,
    display_name TEXT NOT NULL,
    balance REAL DEFAULT 0
  )
`;

@Trace 的观测点也要显式化,或者用稳定的 key 锚定

状态观测这一侧,问题稍微绕一点。@Trace 依赖属性名建立依赖,属性名被混淆之后依赖就断了。一个务实的做法是不把需要跨混淆边界稳定存在的状态字段交给会被改写的名字。

如果某个状态字段需要在 Release + 混淆下仍然可靠地驱动 UI,把它放在一个显式命名的容器里,或者用 AppStorage 的显式 key 作为锚点,而不是依赖属性名本身。

typescript

// 需要跨混淆稳定驱动 UI 的字段,用显式 key 锚定
AppStorage.setOrCreate('account_display_name', entity.displayName);

// UI 侧消费显式 key,不依赖实体属性名
@StorageProp('account_display_name') displayName: string = '';

'account_display_name' 是一个字符串值,混淆不会改写它。@StorageProp 消费的是这个稳定 key,而不是实体上那个可能变成 a1 的属性。代价是状态源从实体属性挪到了 AppStorage,但换来的是观测链在混淆下不断。

如果业务上必须保留实体上的 @Trace,那就需要给这些属性名配 -keep-property-name,但只 keep 观测锚点属性,不要无差别 keep 整个实体。keep 的范围越小,混淆的收益保留得越多。

HAR 和 HSP 的混淆是分开的,规则不跨模块继承

还有一个在模块化工程里很容易踩的边界:每个模块的混淆是独立进行的,HAR 的混淆规则不会自动应用到依赖它的 HAP 或 HSP 上。HAR 里的实体属性被混淆成短名,使用方 HAP 里的查询代码如果还按原始属性名去找列,两边对不上,no column 照样出现。

使用方需要在自己的 obfuscation-rules.txt 里声明对 HAR 中持久化相关属性名的 keep,或者更稳妥的做法是让 HAR 对外暴露的持久化契约本身就是显式列名映射,使用方只消费映射,不感知内部属性名。

text

# HAP 侧,只 keep 持久化/观测锚点,不要整个包 keep
-keep-property-name
display_name
user_id
account_display_name

-keep-property-name 后面列的是必须保持原名的属性,不是整个类。把范围收窄到列名映射和观测 key 涉及的那几个名字,混淆对其它业务代码的收益还在。

建表 SQL 里的列名别从属性名拼出来,否则迁移会替你翻车

一个只在老用户升级路径上出现的变体:新版本代码用 entity as ValuesBucket 建表,列名被混淆成了短名;老版本数据库里表已经存在,CREATE TABLE IF NOT EXISTS 不会重建,后续查询按新的短名去老表里找列,找不到。更隐蔽的是,如果建表逻辑里用了 Object.keys(entity) 拼列名,混淆之后列名集合整体变了,老表和新代码的列名空间对不上,触发的是一次意料之外的迁移。

建表 SQL 里的列名必须是写死的显式字符串,不能从任何运行时属性名推导出来。列名映射表和 CREATE TABLE 里的列名保持同一份来源,改一处两边同步,不要在两个地方各自维护。

排查顺序比 keep 规则更重要

下次 Release 包出现“数据库读不出、UI 不刷新、日志类名短”这组症状同时出现的时候,排查顺序可以压缩成三步。第一步,确认关了混淆的 Release 包是否正常,把混淆从嫌疑名单里确认或排除。第二步,如果锁定混淆,先看数据库列名和 @Trace 观测点有没有依赖运行时属性名,而不是先翻 keep 规则。第三步,如果确实需要 keep,只 keep 持久化契约和观测锚点涉及的属性名,不要整个模块 keep,keep 完再验一遍包体积的收益还在不在。

混淆规则是补丁,持久化契约才是根。把“必须稳定的名字”显式写在字符串常量或映射表里,让属性名回归“只在代码里标识一个字段”的本职,keep 名单会从几百行缩到几十行,下一轮加实体的时候也不会再把同样的问题带回来。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐