sdk 打包必备,proguard 混淆规则如何配置

前言

作为一名 Android 开发者,如果你不想你的 app 或者 sdk 裸奔,那么在发布之前对代码混淆是必须的,它可以把类名、属性名和方法名变成毫无意义的 a,b,c 等。总得来说混淆带来两个好处:

  1. 一定程度上减小包的体积
  2. 使代码反编译之后难以阅读

混淆概念相对比较容易,很多 Android 开发者或多或少的都了解一些。那么对于混淆规则的配置有没有深入的研究呢?到底哪些代码应该混淆,哪些代码应该保留呢?很多时候是靠启动 CV 大法东拷贝一句西拷贝一句,搞到最后混淆规则配置文件杂乱无章。下面我就分享下 Android 中 ProGuard 那些事

ProGuard 简介

ProGuard 官网地址:https://www.guardsquare.com/proguard

ProGuard 的功能有四个:压缩、优化、混淆以及预校验;压缩环节会检测和移除无用的类、字段、方法和属性;优化环节会优化字节码和删除未使用的指令;混淆环节会用无意义的短变量去重命名类、字段以及方法;最后的预验证步骤向类添加预验证信息,这是 Java Micro Edition 和 Java 6 及更高版本所必需的,在 Android 开发中不需要。整个过程如下图所示:

Android Studio 中启用 ProGuard

Android Studio 集成 Java 语言的 ProGuard 作为压缩,优化和混淆工具,配合 Gradle 构建工具使用很简单,只需要在工程应用目录的 gradle 文件中设置 minifyEnabled 为 true 即可

1
2
3
4
5
6
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

上面的配置表示对 release 版本进行混淆处理。proguardFiles 指定了配置混淆规则的文件,可以是多个文件,最后合并多个配置文件为一个

  • getDefaultProguardFile(‘proguard-android-optimize.txt’) 方法从 Android SDK 安装目录的 tools/proguard/ 文件夹中获取默认的规则。默认只有压缩和混淆两个功能,想要进一步使用字节码优化功能请使用同一目录下的 proguard-android-optimize.txt 文件。

注意:从 Gradle 插件 2.2 版本开始,使用的是 Gradle 插件中内置的配置文件,可以在/build/intermediates/proguard-files 下看到,Android SDK 目录下的配置文件会被插件忽略。

  • proguard-rules.pro 用于添加自定义的混淆规则,例如保留一些不想被删除或混淆的代码。该文件默认位于 build.gradle 同级目录下。
  • 一般还会配合 shrinkResources 来进行资源压缩,Android Studio 3.0 之后在 library 中不能配置 shrinkResources 否则报如下错误

个人理解这个问题原因是 library 中的资源可能被外部引用,单独构建 library 的时无法确定资源是否应该删除

ProGuard 语法

ProGuard 语法的基本符号

- 表示一条规则的开始

Keep 选项的语法

keep选项 描述 压缩 混淆
-keep 保留指定的类和类成员,防止被移除或混淆 不压缩 不混淆
-keepnames 不混淆指定的类和类成员 压缩 不混淆
-keepclassmembers 保留指定类中的类成员,防止被移除或混淆。假如类未被保留则失效 不压缩 不混淆
-keepclassmembernames 不混淆指定类中的类成员 压缩 不混淆
-keepclasseswithmembers 保留类中成员及包含它的类,防止被移除或混淆 不压缩 不混淆
-keepclasseswithmembernames 不混淆类中成员及包含它的类 压缩 不混淆

这三组看起来很容易混淆,其实掌握其中的关键就很好区分:

  1. 带 name 后缀的表示只是防止混淆,若是没有被使用到还是可被移除;而没有带 name 后缀的表示防止被混淆和移除
  2. -keep 表示保留指定的类和类成员,而 -keepclasseswithmembers 表示保留类中成员及包含它的类。例如:-keep class * {native ;} 表示保留所有的类和其中的 native 方法;-keepclasseswithmembers class * {native ;} 表示保留包含 native 方法的类和类中的 native 方法
keep选项 描述 常用值 常用值说明
-keeppackagenames 指定不混淆给定的包名称,接受逗号分隔的包名称列表 -keeppackagenames packagename1
-keepattributes 指定要保留的任何可选属性,接收逗号分隔的属性列表 Annotation、SourceFile、InnerClasses、EnclosingMethod、Signature、LineNumberTable 注解、源文件的名称、内部类、定义类的方法、类、字段或方法的通用签名、方法的行号
-keepparameternames 指定保留参数名称和方法

其他可选属性请看 https://www.guardsquare.com/manual/configuration/attributes

通配符

类名
通配符 含义
? 匹配任意单个字符,包名分隔符(.)除外
* 匹配除(.)外的任意字符(不匹配子包)
** 匹配任意字符(匹配子包)
字段和方法
通配符 含义
<fields> 匹配所有字段
<methods> 匹配所有方法,不包含构造方法
(…) 匹配所有构造方法
? 匹配任意单个字符
* 匹配除(.)外的任意字符,匹配任意字段和方法
类型
通配符 含义
% 匹配任意原始数据类型,例如 boolean、int,不包括 void
** 匹配任意字符,不匹配基础数据类型、数组、void
*** 匹配任意类型
匹配任意参数个数,任意参数类型

修饰符

修饰符 描述
public 公共,通常和 class、 组合使用。例:public
private 私有,通常和 class、 组合使用。例:private
protected 包内公共,通常和 class、 组合使用。例:protected
native 本地,通常和 组合使用保留本地方法。例:native
extends 继承,通常和 class 配合使用。例: -keep class * extends android.app.Activity
implements 实现,通常和 class 配合使用。例:-keep class * implements android.view.OnClickListener

其他

符号 描述 例子
includedescriptorclasses 不混淆方法和字段的类型描述符中的任何类,默认只保留基础数据类型 -keep, includedescriptorclasses
-dontwarn 不对未解析的引用和其他重要问题发出警告 -dontwarn twitter4j.**
-dontnote 不打印潜在的错误或疏漏的注释 -dontnote
-dontoptimize 关闭优化 -dontoptimize
-verbose 混淆过程中记录日志 -verbose

更多配置请看proguard官网 https://www.guardsquare.com/manual/configuration/usage

哪些类和类成员不应该被混淆

  1. 需要暴露给外部调用的类和类成员(基本上是 public 方法和字段)
  2. 需要暴露给外部使用的内部类或者接口
    保留内部类或者接口写下如下,内部类和外部类用 $ 衔接
1
2
3
-keep class a$b{
*;
}
  1. 包名不混淆,否则很容易和其他库冲突
    默认规则文件 proguard-android-optimize.txt 中的 -allowaccessmodification 配置会把混淆的类归到随机的包名下,如下图:

  1. 依赖的三方库保留原始代码不动
    不公开代码的三方库代码已经混淆过了,无需重复混淆。公开代码的三方库如果提供了混淆规则就复制过来,若是没有提供混淆规则不必大费周章的去找哪些代码不应该被压缩混淆,除非对包体积有很高的要求。

无需配置一堆规则的简介写法

1
2
3
4
# as3.0 之前的写法
-keep class !packageName.** {*;}
# as3.0 之后的写法
-keep class !packageName.**,** {*;}
  1. 在 AndroidManifest.xml 配置的四大组件
    四大组件被混淆之后无法与 manifest 注册的类匹配
  2. Java 的 native 方法
    JNI 方法与定义的 native 方法名对应,混淆之后会找不到方法
  3. 反射使用的类、方法、属性
    Class.forName(“”) 如类名被混淆将找不到类
  4. Json 对应的实体类
    Gson 或者 Fastjson 的原理中涉及反射创建该类型的对象,反射需要用到完整类路径,混淆了会找不到
  5. JavaScript 调用 Java 的方法
  6. Layout 文件中使用到的自定义view 以及 set get 属性方法
    Xml 配置的完整自定义 view 类路径,若自定义 view 混淆则找不到
  7. 在 Activity 中的方法参数是 view 的方法,即 layout 中定义的点击事件
  8. Parcelable、Serializable 序列化类

默认规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 不进行优化
-dontoptimize
# 不使用大小写混合类名
-dontusemixedcaseclassnames
# 指定不忽略非公共库类
-dontskipnonpubliclibraryclasses
# 混淆过程中记录日志
-verbose
# 保留注解属性、泛型、内部类、封闭方法,后面都是三个属性都是为了反射正常运行
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
# 保留 google license 服务接口
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.android.vending.licensing.ILicensingService
-dontnote com.android.vending.licensing.ILicensingService
-dontnote com.google.vending.licensing.ILicensingService
-dontnote com.google.android.vending.licensing.ILicensingService
# 不混淆包含 native 方法的类和类中的 native 方法以及方法的参数类型
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# 保留自定义 View 的 setXx() 和 getXx() 方法
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# 保留 Activity 中参数是 View 的方法
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# 保留 enum 中的静态 values() 和 valueOf 方法
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留 Parcelable 子类中的 CREATOR 字段
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# 保留 JavascriptInterface 注解标记的方法
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# 不对 android.support 包下的代码警告
-dontnote android.support.**
-dontnote androidx.**
-dontwarn android.support.**
-dontwarn androidx.**

# 不对 FloatMath 类代码警告
-dontwarn android.util.FloatMath

# 保留 Keep 注解标记的类型
-keep class android.support.annotation.Keep
-keep class androidx.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <methods>;
}
# 标记字段时,保留标记的字段和包含它的类名
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <fields>;
}
# 标记构造函数时,保留标记的构造函数和包含它的类名
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
-keepclasseswithmembers class * {
@androidx.annotation.Keep <init>(...);
}
# 不打印有关配置中潜在错误或遗漏的注释
-dontnote org.apache.http.**
-dontnote android.net.http.**

# 不打印有关配置中潜在错误或遗漏的注释
-dontnote java.lang.invoke.**

自定义规则配置模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#############################################
# 写死部分
#############################################
# 关闭预检
-dontpreverify
# 保留参数名称和方法 IDE会提示形参
-keepparameternames
# 抛出异常时保留行号,便于堆栈还原
-keepattributes SourceFile,LineNumberTable
# 四大组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
# layout 中使用的 Fragment
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment

# 保留 R 下面的资源
-keep class **.R$* {*;}
-keepclassmembers class **.R$* {
public static <fields>;
}
#############################################
# 自定义部分 需要修改
#############################################

# 保留三方库
-keep class !packageName.**,** {*;}
# 保留包名
-keeppackagenames packageName
# 保留与js交互的类和相应方法
# 反射使用的类、方法、属性
# Json 对应的实体类,建议放在统一包名下或者类加@Keep注解
-keep class package.entity.*{*;}
# layout 文件中使用到的自定义 View
# 给外部调用的类和类成员
# 给外部调用的内部类和接口

混淆后的堆栈还原

代码经过 Proguard 优化混淆之后增加了反编译的难度,同时也带来线上堆栈信息定位困难。好在 Proguard 为我们提供了还原工具,先来看下 Proguard 每次构建后生成的内容

输出结果

混淆构建完成之后,会在 /build/outputs/mapping/release 目录下生成如下文件:

  • configuration.txt
    总的混淆规则配置文件,包含默认的和自定义的
  • mapping.txt
    提供混淆前后的内容对照表
  • seeds.txt
    罗列出未进行混淆处理的类和成员
  • usage.txt
    罗列出被移除的代码

混淆还原

体统为我们提供了 retrace 工具来还原混淆,retrace 工具结合 mapping.txt 就可以将混淆后的堆栈信息还原为正常情况下的堆栈信息。主要有两种方式来还原:

  1. 利用 retrace 脚本工具
    脚本工具位于 Android Sdk 路径的 /tools/proguard/bin 目录中

Proguardgui.sh 是我们所需的脚本,将脚本拖到终端按回车就能看到如下 gui 界面

选择 ReTrace 一栏。导入 mapping.txt 文件,然后在 stack trace 中填写混淆后的堆栈信息,最后点击 ReTrace 按钮就能还原我们的堆栈信息,从而快速定位线上问题

  1. 利用 retrace 命令行
    我们要先将崩溃信息保存到 txt 格式的文件中(如proguard.txt)保存,然后执行如下命令:
1
retrace.sh -verbose mapping.txt proguard.txt

通过上述堆栈还原的分析可以发现最重要的是 mapping.txt 文件,里面记录了混淆前后的内容对照。如果 mapping.txt 文件丢失,堆栈将无法还原。所以每个版本的 mapping.txt 文件一定要保存好,最好有一个版本追溯(可以存在打包机上)

小知识

在 module 库中配置 consumerProguardFiles 可以将混淆规则一并打入库产物 aar 中,这样使用方使用我们的库将无需额外配置库对应的混淆规则

1
2
3
4
5
6
7
8
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"

consumerProguardFiles "consumer-rules.pro"
}