返回资讯中心
上架策略

2026 马甲包代码混淆技术方案:ProGuard + 资源替换实战

2026-06-12 22分钟阅读星辰出海

为什么代码混淆对马甲包至关重要

Google Play 的重复内容检测远比大多数开发者想象的要严格。它不仅仅对比包名和应用名称——它会扫描 APK 内部的代码结构、类名、方法签名、资源 ID 甚至是布局文件的层级关系。如果你的马甲包和主包在代码层面几乎一模一样,只换了个图标和名字,那被检测下架只是时间问题。

我们在马甲包是什么这篇文章里详细解释过,马甲包的核心逻辑是"让同一个应用在不同账号下看起来像完全不同的产品"。但"看起来不同"只是第一步,更关键的是让 Google 的自动化审查系统"查不出相同"。

代码混淆解决的就是这个问题——从底层代码到上层资源,全方位打乱原有特征,让每个马甲包都有独立的技术指纹。

Google Play 重复应用检测机制拆解

在动手做混淆之前,你需要先了解 Google 是怎么查的:

1. 代码结构比对

Google Play 会反编译 APK,提取 DEX 文件中的类名、方法名、字段名,生成结构指纹。两个 APK 如果有超过 60% 的代码结构重合,就会触发人工审查。

2. 资源 ID 匹配

Android 编译时会给每个资源分配一个 ID(如 0x7f0a0012)。如果两个 APK 的资源 ID 映射表高度一致,说明它们来自同一个代码库。

3. 布局文件相似度

XML 布局的结构、控件 ID、层级深度都会被纳入比对。即使你改了控件名字,只要层级结构没变,相似度评分依然很高。

4. 签名和元数据

虽然不同账号可以用不同的签名文件,但 Google 还会检查 AndroidManifest.xml 中的 versionCode、权限声明、Activity 声明等元数据的相似度。

ProGuard/R8 混淆配置实战

基础混淆规则

ProGuard(或 R8)是 Android 构建工具自带的代码混淆器。它的工作原理是把类名、方法名、字段名替换成无意义的短名(如 abc),同时移除未使用的代码。

proguard-rules.pro 中添加以下规则:

# 基础混淆 - 所有类名方法名替换为短名
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose

# 混淆时不使用字典,让每次构建生成不同的映射
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*

# 保留 Application 类(必须)
-keep public class * extends android.app.Application

# 保留四大组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider

# 保留 JNI 方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留自定义 View 的构造方法
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}

# 保留 Serializable
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

自定义混淆字典——让每个马甲包不同

这是最关键的一步。默认的 ProGuard 混淆结果在同一个代码库上是确定性的——同样的代码,同样的混淆规则,每次生成的 a.classb.class 内容完全一样。这意味着 Google 比对你的两个马甲包时,混淆后的类名居然还是一一对应的。

解决方案是使用自定义混淆字典,并且每个马甲包使用不同的字典

# 混淆字典 - 每个马甲包使用不同的字典文件
-classobfuscationdictionary custom-dict-1.txt
-packageobfuscationdictionary custom-dict-1.txt
-obfuscationdictionary custom-dict-1.txt

字典文件格式很简单,每行一个词:

# custom-dict-1.txt
AlphaWave
BetaStorm
GammaFlux
DeltaPulse
EpsilonCore
ZetaMind
EtaSwift
ThetaBase
...

为第二个马甲包准备 custom-dict-2.txt,使用完全不同的词汇。这样混淆后,同样的类在两个包里会被映射成完全不同的名字——Google 的结构指纹比对就会失效。

实用建议:准备 5-10 套字典轮换使用。字典词汇尽量选用自然语言(如动物名、星座名、元素名),避免纯字母序列,因为 Google 也能识别 a.b.c 这种典型的混淆模式。

R8 完整模式 vs 兼容模式

从 Android Gradle Plugin 4.0 开始,R8 已经完全取代了 ProGuard。在 build.gradle 中:

android {
    buildTypes {
        release {
            // 启用 R8 完整模式(默认开启)
            minifyEnabled true
            shrinkResources true

            // 使用自定义混淆配置
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

R8 完整模式比兼容模式做了更激进的优化——不仅混淆名字,还会内联方法、合并类、移除无用代码。这些额外的变换让每个马甲包的差异更大。

资源文件替换策略

代码混淆只解决了 DEX 层面的问题,资源层同样需要处理。以下是需要替换的关键资源:

1. 图标和启动页

最基本也是最重要的替换。每个马甲包必须有完全不同的:

  • 应用图标:不是简单换色,需要重新设计。图标风格、构图、色彩方案都应不同
  • 启动页:布局、配色、动画效果全部重做
  • 应用内占位图:空状态、错误页、加载动画等

2. 布局文件处理

布局文件是资源指纹的重要来源。以下是有效的混淆手段:

重命名资源 ID:在 res/values/ids.xml 中定义的 ID 名称会被编译进 R 类,成为指纹的一部分。每个马甲包使用不同的 ID 命名方案:

<!-- 马甲包 A -->
<item type="id" name="main_container_a"/>
<item type="id" name="nav_header_a"/>

<!-- 马甲包 B -->
<item type="id" name="root_layout_b"/>
<item type="id" name="top_section_b"/>

调整布局层级:不改变视觉效果的前提下,增加或减少一层嵌套,就能显著改变布局文件的结构指纹。比如把 LinearLayout > TextView 改成 FrameLayout > LinearLayout > TextView,多加一层不影响显示但改变了结构。

重命名 XML 中的控件 ID:在布局 XML 中,把 android:id="@+id/loginButton" 改成 android:id="@+id/enterAccount",对功能零影响但指纹完全不同。

3. 字符串资源

strings.xml 中的字符串虽然不会被直接比对,但它们会被编译进资源表影响资源 ID 的分配顺序。推荐做法:

  • 每个马甲包的 strings.xml 条目顺序打乱
  • 添加一些无用的字符串条目来改变资源 ID 分配
  • 应用名称和描述必须完全不同

4. 颜色和样式

<!-- 不要只换颜色值,连颜色名称也换掉 -->
<!-- 马甲包 A -->
<color name="primary_blue">#1565C0</color>

<!-- 马甲包 B -->
<color name="brand_main">#0D47A1</color>

虽然颜色名称不会进入最终 APK,但它们会影响 R 类中资源 ID 的分配顺序——而资源 ID 顺序正是 Google 比对的关键指标之一。

包名和签名修改

包名修改

包名(Package Name)是最基础的差异化手段,但只改包名远远不够。正确的做法:

  1. applicationId:在 build.gradle 中修改,这是应用在设备上和商店中的唯一标识
  2. 源码包路径:Java/Kotlin 源码的目录结构也要跟着改。如果 applicationIdcom.example.appA,但代码还在 com.example.app 包下,Google 仍然能关联
android {
    defaultConfig {
        applicationId "com.brandone.studio"  // 每个马甲包不同
    }

    // 为不同马甲包配置不同的源码路径
    sourceSets {
        main {
            java.srcDirs = ['src/main/java']
        }
    }
}

签名文件管理

每个马甲包必须使用不同的签名密钥。在马甲包 Google Play 上架中我们讲过,不同开发者账号的签名文件必须独立,不能复用。

android {
    signingConfigs {
        releaseA {
            storeFile file("keystore/brand-a.jks")
            storePassword "xxx"
            keyAlias "brand-a"
            keyPassword "xxx"
        }
        releaseB {
            storeFile file("keystore/brand-b.jks")
            storePassword "yyy"
            keyAlias "brand-b"
            keyPassword "yyy"
        }
    }
}

关于签名文件的更详细管理方案,可以参考 Android 官方的应用签名文档

进阶混淆技巧

1. 代码结构注入

在关键类中注入无意义的代码块,改变方法体和控制流:

// 原始代码
public void processPayment(Order order) {
    validateOrder(order);
    chargeCustomer(order);
    sendConfirmation(order);
}

// 注入干扰代码后
public void processPayment(Order order) {
    String _ref = String.valueOf(System.currentTimeMillis());
    if (_ref.length() > 100) {  // 永远不会执行,但改变了字节码
        Log.d("DEBUG", _ref);
    }
    validateOrder(order);
    chargeCustomer(order);
    sendConfirmation(order);
}

这种"死代码注入"不改变逻辑,但改变了编译后的字节码指纹。每个马甲包注入不同的代码块,指纹差异就产生了。

2. 第三方库版本差异化

如果应用集成了多个第三方 SDK(广告、分析、推送等),不同的马甲包可以使用不同版本的同一 SDK。比如马甲包 A 用 Retrofit 2.9.0,马甲包 B 用 2.10.0。不同版本的字节码差异很大,能有效降低代码相似度评分。

但要注意:不要为了差异化而使用过老的版本,安全漏洞和兼容性问题得不偿失。

3. Kotlin Metadata 清除

Kotlin 编译后会生成 @Metadata 注解,其中包含了完整的类结构信息——包括原始类名、方法名、属性名。即使 ProGuard 混淆了字节码中的名字,@Metadata 注解里还保留着原名,等于白混。

# 移除 Kotlin Metadata 注解
-assumenosideeffects class kotlin.Metadata {
    *;
}

或者更激进地:

# 完全删除 Kotlin Metadata
-dontwarn kotlin.Metadata
-dontnote kotlin.Metadata

4. Native 库处理

如果应用包含 .so 文件,每个马甲包需要重新编译 native 代码。只换 Java 层的混淆不够——Google 会对比 native 库的 ELF 结构和符号表。

处理方案:

  • 在 C/C++ 代码中使用 #define 宏替换函数名
  • 使用 __attribute__((visibility("hidden"))) 隐藏不必要的导出符号
  • 编译时添加不同的 -fvisibility=hidden 选项
  • 使用 NDK 的不同编译参数生成差异化的二进制

自动化构建脚本

手动做这些替换很容易出错,建议使用脚本自动化:

#!/bin/bash
# build-variant.sh - 马甲包自动化构建脚本

VARIANT=$1  # A, B, C...
DICT_NUM=$2 # 字典编号

# 1. 替换 applicationId
sed -i '' "s/applicationId \".*/applicationId \"com.brand${VARIANT,,}.app\"/" app/build.gradle

# 2. 切换混淆字典
cp "dicts/custom-dict-${DICT_NUM}.txt" app/custom-dict.txt

# 3. 替换图标资源
cp -r "variants/${VARIANT}/res/" app/src/main/res/

# 4. 替换字符串资源
cp "variants/${VARIANT}/strings.xml" app/src/main/res/values/strings.xml

# 5. 使用对应签名文件
sed -i '' "s/keystore\/.*\.jks/keystore\/brand-${VARIANT,,}.jks/" app/build.gradle

# 6. 构建
./gradlew assembleRelease

echo "✅ 马甲包 ${VARIANT} 构建完成"
echo "输出路径: app/build/outputs/apk/release/"

通过这种脚本,你只需要准备好多套资源文件和字典,一条命令就能生成差异化的马甲包。

混淆验证:如何确认你的马甲包真的"不同"

做完混淆不代表万事大吉,你还需要验证。以下是实用的验证方法:

APK 结构对比

# 解包两个 APK
apktool d app-a-release.apk -o apk-a
apktool d app-b-release.apk -o apk-b

# 对比 smali 代码
diff -rq apk-a/smali apk-b/smali | head -50

# 对比资源表
diff apk-a/res/values/public.xml apk-b/res/values/public.xml

如果对比结果显示大量相同文件,说明混淆不够彻底。

Android Studio APK Analyzer

直接用 Android Studio 自带的 APK Analyzer 打开两个 APK,对比:

  • DEX 文件中的类列表
  • 资源表中的 ID 分配
  • Manifest 中的组件声明

在线工具

使用 VirusTotalAPKCombo 上传两个 APK,查看它们是否被识别为同一应用的不同版本。如果被识别为不同应用,说明混淆效果良好。

常见失败案例

案例一:只改了包名和图标

这是最常见的错误。开发者以为改了 applicationId 和图标就万事大吉,结果上架三天就被下架。Google 的检测不依赖包名和图标——它看的是代码和资源指纹。

案例二:使用了相同的混淆字典

有些团队做了 ProGuard 混淆,但所有马甲包用同一套规则和字典。结果混淆后的类名完全一致(都叫 abc),Google 一比对,结构指纹 100% 匹配。

案例三:忘记处理 Kotlin Metadata

混淆做得很好,但没清除 @Metadata 注解。Google 反编译后发现,虽然类名变了,但 Metadata 里还存着原始类名 PaymentProcessorLoginManager,直接暴露。

FAQ

马甲包代码混淆后功能会出问题吗?

只要混淆规则配置得当,不会影响功能。关键是要正确使用 --keep 规则保留反射调用的类、JNI 方法和序列化类。建议混淆后做一轮完整的回归测试,特别关注序列化/反序列化、JNI 调用和动态加载模块。在Google Play 上架全流程中我们强调过,上架前的测试一定不能省。

一个代码库最多能做多少个马甲包?

理论上没有上限——只要每个马甲包的混淆字典、资源文件、布局结构都不同。但实际操作中,我们建议一个代码库不超过 5 个马甲包。超过这个数量,维护成本和出错概率会显著上升。如果需要更多,建议拆分代码库。

ProGuard 和 R8 应该用哪个?

优先使用 R8。R8 是 ProGuard 的升级替代品,混淆效果更强,生成的 APK 体积更小。从 Android Gradle Plugin 4.0 起 R8 已是默认选项。如果你的项目还在用旧的 ProGuard,建议尽快迁移。

资源替换要做到什么程度才算合格?

最低标准:图标、启动页、strings.xml 全部替换,布局文件至少 30% 的控件 ID 重命名,颜色资源名称全部更换。更高标准:布局层级做调整、添加无用的资源条目、使用不同版本的第三方库。可以用上面提到的 APK 对比方法验证差异度。

被 Google 检测到重复应用会怎样?

第一次通常只是下架该应用,不会对开发者账号做处罚。但如果同一账号反复上传重复应用,可能会收到政策警告,严重者会被封号。关于被拒后的处理流程,可以参考我们写的Google Play 审核被拒解决方案

写在最后

代码混淆是马甲包上架的技术核心,但不是全部。一个成功的马甲包策略需要从代码层、资源层、账号层三个维度同时发力。代码混淆解决的是"查不出相同"的问题,而合理的账号运营和上架节奏解决的是"不被关联"的问题。

如果你正在做马甲包上架,可以先从自定义混淆字典和资源替换这两个最有效的手段入手,用 APK 对比工具验证效果。遇到 Google Play 审核问题,也欢迎通过星辰出海获取专业支持——我们在马甲包上架领域有丰富的实战经验,能帮你少走弯路。

相关阅读