Administrator
发布于 2026-08-25 / 29 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 62 篇:ProGuard/R8 深度优化:混淆、加密与去重

系列文章《Android 工程师的 100 篇进阶笔记》第 62 篇

上一篇讲了 APK 体积整体优化,这篇专攻 R8/ProGuard 的进阶能力。除了默认压缩,还有混淆字符串、删除调试日志、Keep 规则调优这些"重武器"。

R8 vs ProGuard

Android 长期用 ProGuard(来自 GuardSquare 的商业产品),2018 年后 Google 用自家 R8 替代:

工具

速度

优化能力

开源

ProGuard

慢

基础

闭源

R8

快 2-4x

更激进

开源

R8 默认 mode 是 compat,AGP 8.0+ 默认 fullMode:

// build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"), // R8 aggressive
                "proguard-rules.pro"
            )
        }
    }
}

proguard-android-optimize.txt 比 proguard-android.txt 多一些激进优化(多次优化、合并重复代码)。

Keep 规则的本质

R8 不是简单删代码。它的规则分三层:

[1] 保留入口:哪些代码不能删
  - Application 类
  - Activity / Service / BroadcastReceiver / ContentProvider 子类
  - JNI 调用方法
  - 反射 / annotation 用到的类

[2] 优化规则:能合并/内联的尽量做
  - 移除未使用代码
  - 方法内联
  - 字符串字段优化

[3] 混淆:改名字让代码难看懂
  - 重命名类、方法、字段
  - 重命名 - 不影响可读性的命名空间

实战规则集

下面是生产环境可用的完整规则集,按问题归类:

1. 模型类(保持字段名)

数据类、序列化框架用的模型类必须 keep,否则序列化失败:

# Gson
-keep class com.example.model.** { *; }
-keepattributes Signature
-keepattributes *Annotation*

# Moshi
-keep class com.example.model.** { *; }
-keepclassmembers class * {
    @com.squareup.moshi.JsonClass *;
}

2. Kotlin 反射

Kotlin 的 kotlin.Metadata 注解保留:

-keepattributes RuntimeVisibleAnnotations
-keepattributes RuntimeVisibleParameterAnnotations
-keep class kotlin.Metadata { *; }
-keepattributes Signature, InnerClasses, EnclosingMethod

3. 协程

kotlinx.coroutines 用到反射调试栈:

-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler { *; }
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory { *; }
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory { *; }
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler { *; }

4. Enum

Enum 经常被反序列化或反射用:

-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

5. 第三方库的官方规则

很多库自带规则:

# Retrofit
-dontwarn retrofit2.**
-keepattributes Signature, Exceptions

# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**

# Glide
-keep class com.bumptech.glide.** { *; }
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class * extends com.bumptech.glide.GeneratedAppGlideModuleImpl

# Hilt
-keep,allowobfuscation,allowshrinking class dagger.hilt.android.internal.managers.* { *; }
-keep,allowobfuscation,allowshrinking @dagger.hilt.android.HiltAndroidApp class *

# Room
-keep class androidx.room.RoomDatabase { *; }
-keep @androidx.room.Entity class * { *; }

6. 自定义 View

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

7. 序列化/反序列化

Parcelable 字段:

-keepclassmembers class * implements android.os.Parcelable {
    public static final ** CREATOR;
}

R8 FullMode 注意点

getDefaultProguardFile("proguard-android-optimize.txt") 已经默认启用 fullMode 的大部分。fullMode 的关键区别:

维度

normal

fullMode

删除代码

未引用代码

未引用 + 隐式引用

内联

简单

激进,跨类

字段名

保留 getter/setter

全部混淆

老的程序经过 fullMode 优化经常出问题,常见:

1. 反射调用的类被改名

Class.forName("com.example.MyClass").newInstance()

R8 改名后类名找不到。解决方案:构造器加 @Keep 或写 keep 规则。

2. JNI 方法匹配失败

companion object {
    init {
        System.loadLibrary("native")
    }
    
    external fun nativeLogin(token: String): Int
}

R8 改 native 方法名,但 C++ 还在找旧名字。规则:

-keepclasseswithmembernames class * {
    native <methods>;
}

3. 注解被剥离

R8 默认不剥离 RuntimeVisibleAnnotations,但剥离 RuntimeInvisibleAnnotations。如果用注解做权限检查或路由:

-keepattributes *Annotation*

字符串混淆

R8 默认不混淆字符串内容(只是改类名)。商业敏感(API key、URL)的字符串需要加密:

# 自己实现字符串加密
-keep,allowobfuscation class com.example.SecurityUtil {
    public static java.lang.String decrypt(java.lang.String);
}

# 把字符串预先加密:
// 编译前宏:SOURCE_TOOLS_ENCRYPTION=true
// 源码中的:"https://api.example.com"
// 替换为:DecryptUtil.decrypt("EncryptedString")

开源方案:

  • StringFog(美团):字符串加密 Gradle 插件

  • Paranoid(Yandex):Java/Kotlin 类和字符串混淆

调试日志删除

Log 类调用虽然不占空间,但是泄漏业务逻辑和调试代码。用日志优化插件:

plugins {
    id("id.zelory.compressor")
}

// 删除全部 Log.v Log.d Log.i 调用
// 保留 Log.w Log.e

或者自己用 ProGuard 规则:

# 删除 RLog.d 调用
-assumenosideeffects class com.example.RLog {
    public static int d(...);
    public static int v(...);
}

R8 识别出 d(...) 是空方法后会连调用一起删除。

自定义 R8 规则

R8 支持自定义 Java agent 改字节码,但更实用的方式是写自定义 Gradle 任务:

// buildSrc/src/main/kotlin/RuleGenerator.kt
class RuleGenerator {
    @TaskAction
    fun generate() {
        val sb = StringBuilder()
        // 扫描所有被 @RouterApi 注解的类
        // 自动生成 keep 规则
        outputFile.writeText(sb.toString())
    }
}

不推荐自己造这种轮子,但简单项目可以用。

调试反混淆后的 Crash

Crashlytics / Bugsnag 收到的崩溃堆栈是混淆后的字段:

java.lang.NullPointerException: 
  at com.example.b.a(Unknown Source:42)
  at com.example.c.b(Unknown Source:18)

需要 mapping.txt 反混淆:

# R8 在 build/outputs/mapping/release/mapping.txt
# 上传给 Crashlytics

Android Studio 上也能反混淆:

Run > Edit Configurations > + 
  > ProGuard/R8 > Mapping File Path

验证混淆成功的步骤

# 1. R8 是否报错
./gradlew :app:assembleRelease --info 2>&1 | grep -i 'r8\|keep'

# 2. 检查 mapping 文件
cat app/build/outputs/mapping/release/mapping.txt | head -50

# 3. 用 APK Analyzer 看到底多少类被删了
# -> Dex packages 标签页,对比 release vs debug 的类数

# 4. 跑 monkey 测试
adb shell monkey -p com.example --pct-syskeys 0 \
    --throttle 100 1000

# 5. 跑核心流程的 Espresso 测试
./gradlew :app:connectedAndroidTest

性能 trade-off

混淆和优化是有成本的:

优化

编译时间

runtime 影响

minify(混淆)

+50%

-3%(类加载更快)

fullMode

+100%

-5%

多 pass optimize

+200%

-10%

大型项目 release 构建要从 30s 涨到 1-2 分钟。但打包时间长换来包体积小、运行快,怎么选看场景。

小结

R8 配置表面简单实则深水区。做好 R8 既需要懂 ProGuard 语法,也要理解 Android 运行时机制。从模型类 keep、JNI 字段命名、字符串加密,到日志剥离、性能取舍,都值得花时间研究。建议每个项目都做一次 R8 配置审计,用工具扫描不合理的规则、能合并的 keep。

下一篇聊聊离线优先架构——移动端的数据存储设计。


评论