Android 优化 - R8 混淆

一、概念

1.1 代码混淆

混淆(Obfuscation)是应用发布前必不可少的环节。它不仅能缩小 APK 体积,还能增加反编译的难度,保护核心代码逻辑。

1.2 Android ProGuard / R8 流程

|--------------------|------------------------------------------|
| 压缩 Shrinking | 检测并删除未使用的类、字段、方法和属性。 |
| 优化 Optimization | 分析并优化字节码,甚至内联方法。 |
| 混淆 Obfuscation | 将类名、方法名、字段名重命名为无意义的短字符(如 a, b, c)。 |
| 预检 Preverification | 在 Java 平台上对类进行预验证。 |

1.3 不能混淆的内容

|-------------------|--------------------------------------------------------------------------------------------------------------|
| 反射使用的代码 | 反射通过字符串寻找类/方法,混淆后名称变了,反射会直接报错。 |
| 与 JS 交互的接口 | 在 WebView 中通过 @JavascriptInterface 暴露给 JS 调用的方法。 |
| 与 JS 交互的接口 | -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } |
| JNI 调用(Native 方法) | Java 层声明的 native 方法,需要与 C/C++ 层的函数名对应。 |
| JNI 调用(Native 方法) | -keepclasseswithmembernames class * { native <methods>; } |
| 序列化对象 | 实现 Parcelable 或 Serializable 接口的类,其成员名会被反射读写,不能混淆。 |
| 序列化对象 | -keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; } |
| View及子类 | 在布局 XML 中引用的自定义 View,可能通过反射调用其构造方法。 |
| View及子类 | -keep public class * extends android.view.View |
| 四大组件 | 在 AndroidManifest.xml 中注册的类(系统默认已处理,通常无需手动配置)。 |
| 四大组件 | -keep public class * extends android.app.Activity -keep public class * extends android.app.Service |
| JSON 映射类(Bean) | Gson、FastJson 等库通过反射将 JSON 字符串映射为对象,类名和字段名都不能混淆。 |
| JSON 映射类(Bean) | -keep class com.yourpackage.model.** { *; } (将包名替换为您自己的) |
| 注解 | 许多库(如 EventBus、Room)使用注解,这些注解本身需要保留。 |
| 注解 | -keepattributes *Annotation* |
| 枚举 | 枚举的 values() 和 valueOf() 方法是编译器生成的静态方法,通过反射调用。 |
| 枚举 | -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } |

1.4 最佳实践建议

|---------|-------------------------------------------------------------------------------------|
| 模块化混淆 | 如果是开发 Library(SDK),请使用 consumerProguardFiles。这样集成你 SDK 的 App 会自动应用这些混淆规则,无需开发者手动复制。 |
| 尽早测试 | 不要等发布正式版前才开启混淆。建议在 debug 模式下也偶尔开启混淆进行回归测试。 |
| 善用三方库规则 | 现在的流行库(如 Retrofit, OkHttp, Glide)通常在官网或 README 中提供了成熟的混淆规则,直接复制即可。 |

二、开启混淆

在项目的 build.gradle 中针对 release 构建开启。完成配置后执行 release 构建(例如通过 ./gradlew assembleRelease),R8 就会自动运行。

Kotlin 复制代码
android {
    buildTypes {
        release {
            // 开启代码压缩、混淆和优化
            minifyEnabled = true
            // 开启资源压缩(删除无用图片、布局等),需配合混淆使用
            shrinkResources = true
            // 指定混淆规则文件
            // 参数一:引用 Android SDK 提供的默认优化规则文件
            // 参数二:自定义规则文件,用于添加特定于自己应用的保留规则
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

三、配置语法

R8 完全兼容 ProGuard 的配置语法,规则都写在 proguard-rules.pro 文件中 。混淆的核心是 保持(keep)那些 R8 无法通过静态代码分析自动识别为入口点的代码。错误的规则会导致应用运行时崩溃。

大多数成熟的第三方库(如 Retrofit, OkHttp, Glide 等)会在其 aar 或 jar 包中包含自己的 Proguard/R8 规则,通常不需要手动添加 。但如果遇到因混淆导致的库功能异常,可以查阅该库的官方文档,通常会提供需要手动添加的规则。

3.1 常用 keep 指令

|-------------------------|-------------------------------------|
| -keep | 保持类和类成员不被混淆或移除。 |
| -keepclassmembers | 保持成员不被混淆,但类名可以被混淆。 |
| -keepclasseswithmembers | 如果类包含指定成员,则保持类和成员都不被混淆。 |
| -dontwarn | 让 R8 忽略对某个类的找不到等警告,常用于引入的第三方库不完整时 。 |

3.2 通配符

|-------------|-------------------------|
| * | 匹配任意字符,但不包括包分隔符。 |
| ** | 匹配任意字符,包括包分隔符(匹配包及其子包)。 |
| <methods> | 匹配所有方法。 |
| <fields> | 匹配所有字段。 |

四、构建后的问题处理排查

混淆后,R8 会在 app/build/outputs/mapping/release/ 目录下生成几个关键文件:

|-------------|----------------------------------------------------------------------|
| mapping.txt | 最重要的文件。它记录了原始类、方法、字段名与混淆后名称的映射关系。请务必为每个发布的版本妥善保存此文件,它是解读混淆后崩溃日志的钥匙 。 |
| usage.txt | 列出了 R8 从 APK 中移除的、未被使用的代码。 |
| seeds.txt | 列出了所有通过 -keep 规则保留下来的代码"根"。 |

4.1 混淆后的崩溃日志难以定位

当您的应用在线上崩溃,收集到的堆栈日志是混淆过的,如 a.b.c(SourceFile:1)。

  • 保留行号信息:在规则中加入 -keepattributes SourceFile,LineNumberTable。
  • Android Studio 自带的 GUI 工具(点击菜单栏 Tools → Android → Analyze Stack Trace...)来直接粘贴日志和 mapping 文件进行还原 。
  • 使用 Android SDK 提供的 retrace 工具(位于 sdk/tools/proguard/bin/ 下,Windows 是 retrace.bat,Mac/Linux 是 retrace.sh)和对应的 mapping.txt 文件,将混淆日志还原为可读的堆栈信息 。
XML 复制代码
retrace.sh -verbose mapping.txt obfuscated_stacktrace.txt

4.2 Gson 解析结果全为 null

Debug 模式正常,Release 模式下解析后的对象字段全为 null。原因是 Bean 类的字段名被混淆,导致与 JSON 键名匹配失败。

  • 给 Bean 类字段添加 @SerializedName("key") 注解。
  • 或者将 Bean 类整体 keep 掉。

4.3 枚举类型报错

混淆后,Enum.valueOf() 抛出异常。

Groovy 复制代码
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

4.4 依赖库重复定义

多个 jar 包包含相同的类,混淆时报错。

  • 使用 -dontnote-dontwarn 压制特定包的警告,或者在 configurations 中排除重复依赖。

4.5 资源压缩删除了动态获取的资源

使用 getIdentifier() 动态获取资源 ID 时(如 getResources().getIdentifier("icon_" + index, "drawable", getPackageName())),资源被 shrinkResources 删除了。

  • 在 res/raw 文件夹下创建 keep.xml。
XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@drawable/icon_*, @layout/unused_but_needed" />

4.6 桥接的某些js函数在release模式下报错

代码在 debug 模式下正常, 打包 release 包无法执行,报错"TypeError: window.JavaScriptInterface.close is not a function"。

  • 在 Debug 模式下,代码不进行混淆,方法名保持为 close。 在 Release 模式下,为了减小包体积和安全性,混淆器会将 close(String s) 重命名为类似 a(String b)。由于 JavaScript 端仍然在尝试调用 window.android.close(),自然会提示找不到方法。在混淆文件中,添加如下规则:
XML 复制代码
//在混淆文件中,添加如下规则
# 保持带有 JavascriptInterface 注解的方法不被混淆
-keepattributes JavascriptInterface
-keepattributes *Annotation*

-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
相关推荐
summerkissyou19872 小时前
Android-Audio-MediaPlayer和AudioTrack播放区别
android·audio
独自破碎E2 小时前
BISHI69 [HNOI2008]越狱
android·java·开发语言
Rhystt3 小时前
furryCTF题解|Web方向|ezmd5、猫猫最后的复仇
android·前端·web安全·web
测试工坊17 小时前
Android CPU 使用率采集入门:从原理到公式
android
恋猫de小郭17 小时前
iOS + AI ,国外一个叫 Rork Max 的项目打算替换掉 Xcode
android·前端·flutter
systeminof20 小时前
从静态到实时对抗:首例安卓Runtime AI病毒解析
android·人工智能
福大大架构师每日一题21 小时前
ComfyUI v0.14.2 发布:修复 Gemini/Nano banana 节点空白图像问题,全新 MIME 匹配机制登场
android·comfyui
fengci.21 小时前
ctfshow大牛杯
android