【HarmonyOS开发小实践】HarmonyOS ArkGuard 源码混淆:保护代码与配置选项
HarmonyOS ArkGuard 源码混淆:保护代码与配置选项
release 构建时开启混淆,变量名变成 a、b、c,方法名面目全非,代码挤成一行——这就是 ArkGuard 干的事。混淆有两个直接目的:保护代码逻辑增加逆向难度、减小包体积。但 ArkGuard 不是开了就万事大吉,配错白名单运行时崩、混淆后堆栈看不懂、依赖包冲突这些问题都很常见。这篇把 ArkGuard 的工作原理、配置选项、常见坑讲清楚。
ArkGuard 能做什么不能做什么
先明确能力边界。ArkGuard 的能力范围:
能做的:
- 名称混淆(变量名、属性名、文件名等重命名)
- 代码压缩(删空格换行)
- 注释删除
- 删除 console.* 语句
不能做的:
- 控制流混淆(不改执行逻辑)
- 数据混淆(不改数据结构)
- C/C++、JSON、资源文件的混淆
ArkGuard 只处理 ArkTS/TS/JS 代码。和 Java 生态的 ProGuard 比,ArkGuard 是基础混淆工具,没有 ProGuard 那些高级混淆能力。对代码安全要求高的场景,除了 ArkGuard,还要考虑应用加密、安全加固这些措施。
工作原理
ArkGuard 在编译时读取模块的 build-profile.json5 配置,解析并合并当前模块与依赖模块的混淆规则,对中间文件做混淆处理,输出到 build 目录。
混淆发生在 UI 转换之后、字节码生成之前。混淆处理的是中间代码,不是源码也不是字节码。
语言的限制
ArkGuard 针对 JS/TS/ArkTS,这些语言和 Java 这种强类型语言不一样。JS 支持运行时动态修改对象和函数,而混淆是编译期的静态处理,这种差异可能导致混淆后的名称在运行时解析不到。
TS/ArkTS 虽然有静态类型系统,但用的是结构性类型机制——相同结构的不同命名类型视为等价。这导致无法精确追踪类型来源。
一个具体后果:ArkGuard 用全局生效的属性保留机制。配置保留属性 prop1,所有叫 prop1 的属性都会被保留,没法只保留类 A 的 prop1 而混淆类 B 的 prop1。这是语言特性决定的限制,不是工具的问题。
怎么开启
在模块的 build-profile.json5 里配置:
{
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true, // 开启混淆
"files": ["./obfuscation-rules.txt"] // 本模块混淆规则文件
},
"consumerFiles": ["./consumer-rules.txt"] // 被依赖时生效的规则
}
}
}
混淆只在 release 构建生效,debug 不混淆。开启后默认只混淆局部变量和参数名,要更强的混淆效果需要在 obfuscation-rules.txt 里配置选项。
DevEco Studio 5.0.3.600 及以后版本,新建工程默认关闭混淆。obfuscation-rules.txt 默认有这四项推荐配置:
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
混淆配置选项
在 obfuscation-rules.txt 里配置。用 # 加注释。
开关选项
| 选项 | 作用 | 起始 API |
|---|---|---|
-disable-obfuscation | 关闭所有混淆 | 10 |
-enable-property-obfuscation | 属性名称混淆 | 10 |
-enable-string-property-obfuscation | 字符串属性名混淆(需配合上一项) | 10 |
-enable-toplevel-obfuscation | 顶层作用域名称混淆 | 10 |
-enable-export-obfuscation | 导入导出名称混淆 | 10 |
-enable-filename-obfuscation | 文件/文件夹名混淆 | 10 |
-compact | 代码压缩到一行 | 10 |
-remove-comments | 删声明文件 JSDoc 注释 | 10 |
-remove-log | 删 console.* 调用 | 10 |
看几个具体效果。
-enable-property-obfuscation:
// 混淆前
class TestA {
static prop1: number = 0;
}
TestA.prop1;
// 混淆后
class TestA {
static i: number = 0;
}
TestA.i;
-enable-toplevel-obfuscation:
// 混淆前
let count = 0;
// 混淆后
let s = 0;
-compact:
// 混淆前
class TestA {
static prop1: number = 0;
}
TestA.prop1;
// 混淆后
class TestA { static prop1: number = 0; } TestA.prop1;
保留选项
光开混淆不开保留,几乎一定会出问题。保留选项指定哪些名称不能混淆。
| 选项 | 作用 |
|---|---|
-keep-property-name | 保留指定属性名 |
-keep-global-name | 保留顶层作用域/导入导出名称 |
-keep-file-name | 保留文件/文件夹名 |
-keep-comments | 保留指定元素的 JSDoc 注释 |
-keep-dts | 保留 .d.ts 文件中的所有名称 |
-keep | 保留指定路径下所有名称 |
配置示例:
-keep-property-name
firstName
lastName
addNum
-keep-file-name
DynamicImportFile
通配符支持:? 匹配单个字符,* 匹配任意数量字符。
-keep-property-name
a* # 保留所有 a 开头的属性名
? # 保留所有单字符属性名
* # 保留所有属性名(等于关了属性混淆)
哪些场景必须配白名单
这是实操中最关键的部分。以下场景的名称如果不配白名单,运行时大概率出问题。
1. so 库 API 名称
调用 native so 库的方法,方法名必须保留:
import testNapi from 'libentry.so';
testNapi.addNum(2, 3); // addNum 必须保留
配置:
-keep-property-name
addNum
2. JSON 文件字段
import jsonData from './data.json';
let val = jsonData.jsonProperty; // jsonProperty 必须保留
3. 数据库字段
const valueBucket: ValuesBucket = {
ID1: 'ID1', // ID1 必须保留
NAME1: 'jack' // NAME1 必须保留
}
4. 动态属性访问
字符串拼接或变量访问属性:
const obj = { staticName: 'value' };
const fieldName = 'static' + 'Name';
console.info(obj[fieldName]); // staticName 必须保留
5. 网络请求字段
httpRequest.request('https://example.com/Login', {
extraData: { usernameTest: 'test1', passwordTest: 'test2' }
// usernameTest 和 passwordTest 必须保留
});
6. 动态 import 路径
const moduleName = './DynamicImportFile';
const modules = await import(moduleName); // DynamicImportFile 必须保留
配置:
-keep-file-name
DynamicImportFile
混淆规则怎么合并
编译一个模块时,最终生效的规则是当前模块规则和依赖模块规则的合并结果。
合并逻辑:
- 混淆选项:或运算。任意一个规则文件里开了某选项,最终就包含该选项。
- 保留选项:并集。所有规则文件里的白名单合并。
API version 18 之后,默认只合并依赖模块的保留选项,不合并混淆选项。这避免了依赖包的混淆配置影响主模块。如果需要恢复旧行为,配置 -enable-lib-obfuscation-options。
三种配置文件的区别
| 文件 | 谁能改 | 影响本模块 | 影响依赖方 |
|---|---|---|---|
| obfuscation-rules.txt | 开发者 | 是 | 否 |
| consumer-rules.txt | 开发者 | 否 | 是 |
| obfuscation.txt | 自动生成 | 否 | 是 |
consumer-rules.txt 的规则在别的模块依赖本模块时生效。建议只在 consumer-rules.txt 里配保留选项,别配混淆选项,免得影响主模块的混淆效果。
混淆后的代码长什么样
源码:
export class UserService {
private userName: string = '';
private age: number = 0;
setName(name: string): void {
this.userName = name;
console.info('set name:', name);
}
getName(): string {
return this.userName;
}
}
开启全部混淆 + compact + remove-log 后,大致变成:
export class a {
private b: string = '';
private c: number = 0;
d(e: string): void { this.b = e; }
f(): string { return this.b; }
}
(export 的类名 a 是因为开了 -enable-export-obfuscation,方法体内的 console.info 被 -remove-log 删掉了,代码挤成一行是 -compact 的效果。)
混淆与调试的关系
混淆后代码名称都变了,crash 堆栈里也是混淆后的名称,直接看不懂。还原需要两个文件:
- nameCache.json:名称映射表,记录混淆前后的对应关系。构建后在
build/default/.../release/obfuscation/下。 - sourceMaps.map:源码映射信息,记录压缩/转换后代码到原始源码的映射。
用 DevEco Studio Command Line Tools 里的 hstack 插件可以自动还原堆栈:
hstack <crash日志文件> -s <sourceMaps.map路径> -n <nameCache.json路径>
nameCache.json 必须备份。每次全量构建都会覆盖这个文件。线上版本 crash 排查时,如果没有对应版本的 nameCache.json,堆栈就还原不了。发版时把这个文件和 sourceMaps.map 一起存档。
常见混淆问题排查
问题一:运行时找不到方法
现象:release 包运行崩溃,报错类似 xxx is not a function。
原因:某个方法名被混淆,但运行时通过原名访问了。
排查:看报错的方法名,在 obfuscation-rules.txt 里加 -keep-property-name 或 -keep-global-name。
问题二:so 库调用失败
现象:调用 native 方法报错。
原因:so 库的方法名被混淆。
排查:把所有 so 库导出的方法名加进 -keep-property-name。
问题三:JSON 解析后字段 undefined
现象:读 JSON 数据字段返回 undefined。
原因:JSON 文件的字段名被混淆了。
排查:把用到的 JSON 字段名加进白名单。
问题四:动态 import 失败
现象:import(path) 报找不到模块。
原因:文件名被混淆,动态 import 的路径对不上。
排查:把动态 import 的路径名加进 -keep-file-name。
问题五:路由跳转失败
现象:页面跳转报错。
原因:路由配置里的页面路径被混淆。
排查:路由表里的 pageSourceFile 路径加进 -keep-file-name。API version 20 之后系统自动处理,老版本要手动配。
问题六:HAR 包被依赖时出问题
现象:依赖某个 HAR 后,主模块运行异常。
原因:HAR 的 consumer-rules.txt 配了混淆选项,影响了主模块。
排查:检查 HAR 的 consumer-rules.txt,只保留保留选项,删掉混淆选项。
实践中要注意的
逐项开启混淆:别一上来就开所有混淆选项。先开 -enable-toplevel-obfuscation,跑通了再开 -enable-property-obfuscation,最后开 -enable-export-obfuscation 和 -enable-filename-obfuscation。每开一项都跑一遍功能,出问题好定位。
release 和 debug 行为差异:debug 不混淆,release 混淆。只在 release 出现的问题,优先怀疑混淆。排查时先关掉混淆确认是不是混淆导致的。
-compact 影响堆栈定位:release 堆栈只有行号没有列号,开了 -compact 后所有代码挤一行,行号定位失效。如果希望某些路径保留换行方便看堆栈,用 -keep-uncompact 指定不压缩的路径。
-enable-string-property-obfuscation 要谨慎:字符串属性名混淆后,如果代码里有特殊字符的字符串属性(比如 "\n"、""),可能没法通过白名单保留。这种场景别开这个选项。
@KeepSymbol 注解标记白名单:API version 19 开始,可以在源码里用 // @KeepSymbol 注释标记不混淆的名称,比在配置文件里写白名单更直观:
// @KeepSymbol
class MyClass02 {
prop01: string = "prop"; // MyClass02 和 prop01 都不会混淆
}
几条经验哦
- so 库 API 白名单第一时间配上:这是混淆最常见的坑,新项目开混淆前先把所有 so 库导出的方法名列进白名单。
- nameCache.json 每个版本都要存:发版时把 nameCache.json 和 sourceMaps.map 一起归档,线上 crash 还原全靠这两个文件。
- consumer-rules.txt 只放保留选项:别在 consumer-rules.txt 里配混淆选项,会影响依赖方的主模块。
- 用混淆助手排查:DevEco Studio 有混淆助手功能,能帮着识别需要配置的白名单,比手动排查快。
- 混淆不是安全银弹:ArkGuard 只做基础混淆,对有安全要求的场景,还要配合应用加密、加固这些措施。
更多推荐

所有评论(0)