本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

ArkGuard:源码混淆的边界与代价

 做移动应用的人大多遇到过这种情况:本地调试没问题,测试包也正常,一打发布包,页面白了,日志红了。

       "是不是开了混淆?"

       这句话听起来像甩锅,但很多时候确实就是。ArkGuard 是 HarmonyOS 生态的源码混淆工具集,目的是降低源码被直接阅读和复用的风险。它不做加密,也不保证绝对安全——在不改变程序语义的前提下,重命名部分标识符、属性和文件,让代码更难一眼看懂,同时尽量让应用照常跑。

       听起来只是改名字,但改名字从来不是孤立动作。一个函数改名了,所有调用方都得知道;一个属性改名了,JSON 里的字段、Native 导出的符号、动态拼接的字符串路径,都还认着旧名字。任何一处对不上,混淆后的应用就会出现白屏、崩溃,或者那种"按钮还在,点下去什么也不发生"的诡异状态。

一、ArkGuard 混淆的到底是什么

       ArkGuard 主要处理 ArkTS、TypeScript、JavaScript 代码。常见目标包括顶层作用域名称、属性名称、导入导出名称、文件名称。

       先划清一条边界:混淆不是加密。

       加密是"没有密钥就还原不了",混淆是"拿到代码也不容易快速理解"。混淆能提高逆向成本,减少业务结构、内部命名和实现细节的直接暴露,但它替代不了密钥保护、接口鉴权、传输加密和服务端安全。把它当作安全体系的一层,而不是全部,是比较稳妥的判断。

       另一个特点是 ArkGuard 参与构建流程。混淆不是应用启动后的魔法,而是编译打包阶段的一部分。开发者通过混淆配置文件和 build-profile.json5 里的相关配置,决定开启哪些能力、保留哪些名称。

二、四个开关,各自的脾气

       源码混淆配置里有几个核心选项经常一起出现。开得越多,保护面越大,但兼容性和白名单管理的压力也越高。

1. 顶层名称混淆:把函数和变量改得不像名字

       -enable-toplevel-obfuscation 处理顶层作用域中的名称。原本叫 calculateUserPermission 的函数,混淆后可能只剩一个短名。读代码的人不再能从名字直接看出业务意图。

       但顶层名称一旦被外部模块、动态调用或反射式逻辑依赖,就不能只顾着改得彻底。跨模块接口必须明确保留,否则调用方和被调用方各说各话。

2. 属性混淆:最容易出"数据明明有,取出来是 undefined"

       -enable-property-obfuscation 处理对象属性名。收益明显,踩坑也最集中。

       典型场景:JSON 文件里有字段 jsonProperty,开了属性混淆后代码用混淆后的名字访问,源码变成了 data.i,JSON 里还是 jsonProperty,两边对不上,经典报错:

Cannot read property xxx of undefined

       网络数据、JSON 字段、数据库字段、Want 参数、动态属性、由字符串决定的字段——这些都是外部约定,不是普通内部变量,工具猜不到。要用 -keep-property-name 把关键属性加进保留名单。

3. 导入导出混淆:模块之间不能各改各的

       -enable-export-obfuscation 处理导入导出相关名称,通常和顶层名称、属性混淆配合使用。

       最典型的事故是 HAP 和 HSP。两个模块独立编译,导出方把 addNum 改成了 b,导入方按另一套规则理解成 n,运行时找不到对应导出:

'module1/file1' does not provide an export name 'x'

       HAP/HSP、HAR、本地模块、三方库的编译关系不一样,不能用一条规则通吃。跨模块公共接口要有稳定命名,在发布规则和消费规则中保持一致。对外暴露的函数、类、命名空间成员,根据实际边界配置 -keep-global-name。

4. 文件名混淆:路径也是程序的一部分

       -enable-filename-obfuscation 处理文件名。普通静态引用,工具能同步调整引用关系;但动态 import()、运行时拼接路径、按文件名加载模块的代码就不行。

       业务代码如果把文件路径当字符串保存,文件名改了字符串没改,运行时就迷路了。用 -keep-file-name 保留特定文件名,或者改代码,减少对隐式路径的依赖。

三、白名单是接口契约,不是投降

       很多团队第一次踩混淆坑,会把所有名称都加进白名单。功能是恢复了,但保护也基本没了。

       更合理的做法是把白名单当接口契约管理:只有跨边界、动态访问、外部数据约定、Native 接口这些真正需要稳定名称的部分才保留。

       常见保留思路:

   • -keep-property-name 保护 JSON、数据库、Want、网络协议字段;

   • -keep-global-name 保护模块对外导出的函数、类、命名空间成员;

   • -keep-file-name 保护动态 import 或运行时直接加载的文件;

   • -keep 保留特定文件、类或代码范围。

       白名单越精确,混淆收益越完整,后续维护也越可控。为了消灭一个报错把整个项目配置成"所有名字都不许动",工具还在跑,保护目标先下班了。

四、Native so 是一条特殊边界

       ArkGuard 处理的是 JS、TS、ArkTS 源码。Native so 里 C/C++ 定义的接口,不会因为开了属性混淆就跟着改名;但 ArkTS 侧调用 so 接口的属性名会变。

       比如 C++ N-API 导出 addNum,ArkTS 代码 testNapi.addNum(1, 2) 调用。混淆后调用处变成 testNapi.m(1, 2),Native 侧只认 addNum,两边失联,严重时直接崩溃。

       所以 Native 接口名、N-API 导出属性、跨语言桥接字段,必须明确加进属性白名单。跨语言边界最忌讳"工具应该能猜到"——工具看到的是语法,真正的契约藏在另一种语言和另一套构建产物里。

五、排查顺序:先关掉,再定位,再精确保留

       混淆问题最怕一上来盯着业务逻辑凭感觉改。比较靠谱的路径:

       第一步,在 obfuscation-rules.txt 里临时加 -disable-obfuscation,重新构建验证。问题消失,基本确认是混淆引起。

       第二步,看运行日志和崩溃栈。混淆后方法名变了,异常栈行号对应的是编译产物,不是原始源码,要结合编译输出定位。

       第三步,判断故障属于哪类边界:属性、导入导出、动态文件名、Native 接口,还是跨模块编译不一致。

       第四步,只加必要的保留规则,重新构建、安装、回归。每加一条白名单,要知道它保护的是哪个运行时契约。

       那种"没崩溃但页面白屏或功能异常"的问题,要打开 HiLog,定位异常代码段,补关键字段日志,确认到底是数据名称变了、路径失效,还是模块接口不一致。混淆问题往往不是工具报错,而是程序悄悄拿到一个 undefined,在更远的地方爆炸。

六、三方库的规则也会影响主模块

       遇到"我没开某选项,为什么产物被压成一行"这种问题,别只看当前模块。还要看 oh-package.json5 里的依赖,以及本地库、oh_modules 下的混淆配置。

       尤其留意三方库的 consumer-rules.txt、obfuscation.txt。某些开关在主模块开启混淆后会产生连锁影响,导致预期外的压缩、属性重命名或日志移除。

       从工程治理角度,三方库发布者应该把"消费方必须保留的接口"写进消费规则,而不是把全局混淆开关一股脑塞给主工程。应用开发者则要把依赖升级纳入回归测试,不能只测自己的源码。

七、用好 ArkGuard,关键在边界清楚

       ArkGuard 的价值不是让代码看起来越陌生越好,而是让内部实现更难直接理解,同时让公开契约稳定、可验证。

       一套成熟的混淆策略通常有几个特点:内部实现尽量混淆;外部接口明确保留;动态访问有清晰约定;Native 和 ArkTS 跨语言边界有专门测试;HAP、HSP、HAR、三方库分别验证;Debug 和 Release 构建都纳入自动化回归。

       混淆不是"按下按钮代码立刻隐身",而是对程序边界的一次体检。凡是依赖名字、路径、字段、模块契约的地方,都应该明确写出来、测出来、守住。

       发布包再出白屏,别急着把锅扣给 ArkGuard。先问三个问题:这个名字是不是跨边界使用?这个属性是不是来自外部数据?这个模块是不是和调用方独立编译?

       答案找到,修复通常就不远了。


      🔗 官网开发者学堂视频:华为开发者学堂

     🔗 社区DFX专题文章:  华为开发者问答 | 华为开发者联盟

【扫码加入 HarmonyOS DFX 技术交流群】

Logo

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

更多推荐