什么是混淆
代码压缩通过 ProGuard 提供,ProGuard 会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决 64k 引用限制的有用工具)。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的 APK 难以被逆向工程,这在应用使用许可验证等安全敏感性功能时特别有用。
资源压缩通过适用于 Gradle 的 Android 插件提供,该插件会移除封装应用中未使用的资源,包括代码库中未使用的资源。它可与代码压缩发挥协同效应,使得在移除未使用的代码后,任何不再被引用的资源也能安全地移除。
压缩代码
要通过 ProGuard 启用代码压缩,请在 build.gradle
文件内相应的构建类型中添加 minifyEnabled true
。
请注意,代码压缩会拖慢构建速度,因此您应该尽可能避免在调试构建中使用。不过,重要的是您一定要为用于测试的最终 APK 启用代码压缩,因为如果您不能充分地自定义要保留的代码,可能会引入错误。
例如,下面这段来自 build.gradle
文件的代码用于为发布构建启用代码压缩:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
注:Android Studio 会在使用 Instant Run 时停用 ProGuard。如果您需要为增量式构建压缩代码,请尝试试用 Gradle 压缩器。
除了 minifyEnabled
属性外,还有用于定义 ProGuard 规则的 proguardFiles
属性:
-
getDefaultProguardFile('proguard-android.txt')
方法可从 Android SDKtools/proguard/
文件夹获取默认的 ProGuard 设置。提示:要想做进一步的代码压缩,请尝试使用位于同一位置的
proguard-android-optimize.txt
文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。 proguard-rules.pro
文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle
文件旁)。
要添加更多各构建变体专用的 ProGuard 规则,请在相应的 productFlavor
代码块中再添加一个 proguardFiles
属性。例如,以下 Gradle 文件会向 flavor2
产品定制添加 flavor2-rules.pro
。现在 flavor2
使用所有三个 ProGuard 规则,因为还应用了来自 release
代码块的规则。
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
productFlavors {
flavor1 {
}
flavor2 {
proguardFile 'flavor2-rules.pro'
}
}
}
每次构建时 ProGuard 都会输出下列文件:
说明 APK 中所有类文件的内部结构。
提供原始与混淆过的类、方法和字段名称之间的转换。
列出未进行混淆的类和成员。
usage.txt
列出从 APK 移除的代码。
这些文件保存在
中。
自定义要保留的代码
对于某些情况,默认 ProGuard 配置文件 (proguard-android.txt
) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。不过,ProGuard 难以对许多情况进行正确分析,可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:
- 当应用引用的类只来自
AndroidManifest.xml
文件时 - 当应用调用的方法来自 Java 原生接口 (JNI) 时
- 当应用在运行时(例如使用反射或自检)操作代码时
测试应用应该能够发现因不当移除的代码而导致的错误,但您也可以通过查看
中保存的 usage.txt
输出文件来检查移除了哪些代码。
要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行 [-keep](https://stuff.mit.edu/afs/sipb/project/android/sdk/android-sdk-linux/tools/proguard/docs/index.html#manual/usage.html)
代码。例如:
或者,您可以向您想保留的代码添加 [@Keep](https://developer.android.com/reference/android/support/annotation/Keep.html)
注解。在类上添加 @Keep
可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。
在使用 -keep
选项时,有许多事项需要考虑;如需了解有关自定义配置文件的详细信息,请阅读 ProGuard 手册。问题排查一章概述了您可能会在混淆代码时遇到的其他常见问题。
解码混淆过的堆叠追踪
在 ProGuard 压缩代码后,读取堆叠追踪变得困难(即使并非不可行),因为方法名称经过了混淆处理。幸运的是,ProGuard 每次运行时都会创建一个 mapping.txt
文件,其中显示了与混淆过的名称对应的原始类名称、方法名称和字段名称。ProGuard 将该文件保存在应用的
目录中。
请注意,您每次使用 ProGuard 创建发布构建时都会覆盖 mapping.txt
文件,因此您每次发布新版本时都必须小心地保存一个副本。通过为每个发布构建保留一个 mapping.txt
文件副本,您就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试。
在 Google Play 上发布应用时,您可以上传每个 APK 版本的 mapping.txt
文件。Google Play 将根据用户报告的问题对收到的堆叠追踪进行去混淆处理,以便您在 Google Play Developer Console 中进行检查。如需了解详细信息,请参阅帮助中心有关如何对崩溃堆叠追踪进行去混淆处理的文章。
要自行将混淆过的堆叠追踪转换成可读的堆叠追踪,请使用 retrace
脚本(在 Windows 上为 retrace.bat
;在 Mac/Linux 上为 retrace.sh
)。它位于
目录中。该脚本利用 mapping.txt
文件和您的堆叠追踪生成新的可读堆叠追踪。使用 retrace 工具的语法如下:
例如:
如果您不指定堆叠追踪文件,retrace 工具会从标准输入读取。
通过 Instant Run 启用代码压缩
如果代码压缩在您增量构建应用时非常重要,请尝试适用于 Gradle 的 Android 插件内置的试用代码压缩器。与 ProGuard 不同,此压缩器支持 Instant Run。
您也可以使用与 ProGuard 相同的配置文件来配置 Android 插件压缩器。但是,Android 插件压缩器不会对您的代码进行混淆处理或优化,它只会删除未使用的代码。因此,您应该仅将其用于调试构建,并为发布构建启用 ProGuard,以便对发布 APK 的代码进行混淆处理和优化。
要启用 Android 插件压缩器,只需在 "debug" 构建类型中将 useProguard
设置为 false
(并保留 minifyEnabled
设置 true
):
android {
buildTypes {
debug {
minifyEnabled true
**useProguard false**
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
注:如果 Android 插件压缩器最初删除了某个方法,但您之后更改了代码,使该方法可访问,Instant Run 会将其视为结构代码更改并执行冷交换。
压缩资源
资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在您添加包含资源的代码库时体现得尤为明显 - 您必须移除未使用的库代码,使库资源变为未引用资源,才能通过资源压缩器将它们移除。
要启用资源压缩,请在 build.gradle
文件中将 shrinkResources
属性设置为 true
(在用于代码压缩的 minifyEnabled
旁边)。例如:
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
如果您尚未使用代码压缩用途的 minifyEnabled
构建应用,请先尝试使用它,然后再启用 shrinkResources
,因为您可能需要编辑 proguard-rules.pro
文件以保留动态创建或调用的类或方法,然后再开始移除资源。
注:资源压缩器目前不会移除 values/
文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本。有关详情,请参阅问题 70869。
自定义要保留的资源
如果您有想要保留或舍弃的特定资源,请在您的项目中创建一个包含
标记的 XML 文件,并在 tools:keep
属性中指定每个要保留的资源,在 tools:discard
属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。您可以使用星号字符作为通配符。
例如:
将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml
。构建不会将该文件打包到 APK 之中。
指定要舍弃的资源可能看似愚蠢,因为您本可将它们删除,但在使用构建变体时,这样做可能很有用。例如,如果您明知给定资源表面上会在代码中使用(并因此不会被压缩器移除),但实际不会用于给定构建变体,就可以将所有资源放入公用项目目录,然后为每个构建变体创建一个不同的 keep.xml
文件。构建工具也可能无法根据需要正确识别资源,这是因为编译器会添加内联资源 ID,而资源分析器可能不知道真正引用的资源和恰巧具有相同值的代码中的整数值之间的差别。
启用严格引用检查
正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码调用 [Resources.getIdentifier()](https://developer.android.com/reference/android/content/res/Resources.html#getIdentifier(java.lang.String,%20java.lang.String,%20java.lang.String))
(或您的任何库进行了这一调用 - AppCompat 库会执行该调用),这就表示您的代码将根据动态生成的字符串查询资源名称。当您执行这一调用时,默认情况下资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。
例如,以下代码会使所有带 img_
前缀的资源标记为已使用。
资源压缩器还会浏览代码以及各种 res/raw/
资源中的所有字符串常量,寻找格式类似于 file:///android_res/drawable//ic_plus_anim_016.png
的资源网址。如果它找到与其类似的字符串,或找到其他看似可用来构建与其类似的网址的字符串,则不会将它们移除。
这些是默认情况下启用的安全压缩模式的示例。但您可以停用这一“有备无患”处理方式,并指定资源压缩器只保留其确定已使用的资源。要执行此操作,请在 keep.xml
文件中将 shrinkMode
设置为 strict
,如下所示:
如果您确已启用严格压缩模式,并且代码也引用了包含动态生成字符串的资源(如上所示),则必须利用 tools:keep
属性手动保留这些资源。
移除未使用的备用资源
Gradle 资源压缩器只会移除未被您的应用代码引用的资源,这意味着它不会移除用于不同设备配置的备用资源。必要时,您可以使用 Android Gradle 插件的 resConfigs
属性来移除您的应用不需要的备用资源文件。
例如,如果您使用的库包含语言资源(例如使用的是 AppCompat 或 Google Play 服务),则 APK 将包括这些库中消息的所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。如果您想只保留应用正式支持的语言,则可以利用 resConfig
属性指定这些语言。系统会移除未指定语言的所有资源。
下面这段代码展示了如何将语言资源限定为仅支持英语和法语:
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
同理,您也可以利用 APK 拆分为不同设备构建不同的 APK,自定义在 APK 中包括的屏幕密度或 ABI 资源。
合并重复资源
默认情况下,Gradle 还会合并同名资源,例如可能位于不同资源文件夹中的同名可绘制对象。这一行为不受 shrinkResources
属性控制,也无法停用,因为在有多个资源匹配代码查询的名称时,有必要利用这一行为来避免错误。
只有在两个或更多个文件具有完全相同的资源名称、类型和限定符时,才会进行资源合并。Gradle 会在重复项中选择其视为最佳选择的文件(根据下述优先顺序),并只将这一个资源传递给 AAPT,以供在 APK 文件中分发。
Gradle 会在下列位置寻找重复资源:
- 与主源集关联的主资源,一般位于
src/main/res/
中。 - 变体叠加,来自构建类型和构建风味。
- 库项目依赖项。
Gradle 会按以下级联优先顺序合并重复资源:
依赖项 → 主资源 → 构建风味 → 构建类型
例如,如果某个重复资源同时出现在主资源和构建风味中,Gradle 会选择构建风味中的重复资源。
如果完全相同的资源出现在同一源集中,Gradle 无法合并它们,并且会发出资源合并错误。如果您在 build.gradle
文件的 sourceSet
属性中定义了多个源集,则可能会发生这种情况,例如,如果 src/main/res/
和 src/main/res2/
包含完全相同的资源,就可能会发生这种情况。
排查资源压缩问题
当您压缩资源时,Gradle Console 会显示它从应用软件包中移除的资源的摘要。例如:
Gradle 还会在
(ProGuard 输出文件所在的文件夹)中创建一个名为 resources.txt 的诊断文件。该文件包括诸如哪些资源引用了其他资源以及使用或移除了哪些资源等详情。
例如,要了解您的 APK 为何仍包含
@drawable/ic_plus_anim_016,请打开
resources.txt` 文件并搜索该文件名。您可能会发现,有其他资源引用了它,如下所示:
现在您需要了解为何 @drawable/add_schedule_fab_icon_anim
可以访问 - 如果您向上搜索,就会发现“The root reachable resources are:”之下列有该资源。这意味着存在对 add_schedule_fab_icon_anim
的代码引用(即在可访问代码中找到了其 R.drawable ID)。
如果您使用的不是严格检查,则存在看似可用于为动态加载资源构建资源名称的字符串常量时,可将资源 ID 标记为可访问。在这种情况下,如果您在构建输出中搜索资源名称,可能会找到类似下面这样的消息:
如果您看到一个这样的字符串,并且您能确定该字符串未用于动态加载给定资源,就可以按照有关如何自定义要保留的资源部分中所述利用 tools:discard
属性通知构建系统将它移除。`
先看如下两个比较常用的命令,很多童鞋可能会比较迷惑以下两者的区别。
-keep class cn.hadcn.test.**
-keep class cn.hadcn.test.*
一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了
-keep class cn.hadcn.test.* {*;}
ProGuard作用
压缩(Shrinking):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。
-dontshrink 关闭压缩
优化(Optimization):默认开启,在字节码级别执行优化,让应用运行的更快。
-dontoptimize 关闭优化-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5
混淆(Obfuscation):默认开启,增大反编译难度,类和类成员会被随机命名,除非用keep保护。
-dontobfuscate 关闭混淆
混淆后默认会在工程目录app/build/outputs/mapping/release
下生成一个mapping.txt
文件,这就是混淆规则,我们可以根据这个文件把混淆后的代码反推回源本的代码,所以这个文件很重要,注意保护好。原则上,代码混淆后越乱越无规律越好,但有些地方我们是要避免混淆的,否则程序运行就会出错,所以就有了下面我们要教大家的,如何让自己的部分代码避免混淆从而防止出错。
基本规则
先看如下两个比较常用的命令,很多童鞋可能会比较迷惑以下两者的区别。
-keep class cn.hadcn.test.**-keep class cn.hadcn.test.*
一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了
-keep class cn.hadcn.test.* {*;}
在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extend
,implement
等这些Java规则。如下例子就避免所有继承Activity的类被混淆
-keep public class * extends android.app.Activity
如果我们要保留一个类中的内部类不被混淆则需要用$
符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。
-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface { public *;}
再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用
; //匹配所有构造器; //匹配所有域; //匹配所有方法方法
你还可以在
或
前面加上private
、public
、native
等来进一步指定不被混淆的内容,如
-keep class cn.hadcn.test.One { public ;}
表示One
类下的所有public
方法都不会被混淆,当然你还可以加入参数,比如以下表示用JSONObject作为入参的构造函数不会被混淆
-keep class cn.hadcn.test.One { public (org.json.JSONObject);}
有时候你是不是还想着,我不需要保持类名,我只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers
,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格
保留 | 防止被移除或者被重命名 | 防止被重命名 |
---|---|---|
类和类成员 | -keep | -keepnames |
仅类成员 | -keepclassmembers | -keepclassmembernames |
如果拥有某成员,保留类和类成员 | -keepclasseswithmembers | -keepclasseswithmembernames |
移除是指在压缩(Shrinking)时是否会被删除。以上内容时混淆规则中需要重点掌握的,了解后,基本所有的混淆规则文件你应该都能看懂了。再配合以下几点注意事项,开启你为自己代码,实现混淆规则之旅吧。
注意事项
1,jni方法不可混淆,因为这个方法需要和native方法保持一致;
-keepclasseswithmembernames class * { # 保持native方法不被混淆 native ;}
2,反射用到的类不混淆(否则反射可能出现问题);
3,AndroidMainfest中的类不混淆,所以四大组件和Application的子类和Framework层下所有的类默认不会进行混淆。自定义的View默认也不会被混淆;所以像网上贴的很多排除自定义View,或四大组件被混淆的规则在Android Studio中是无需加入的;
4,与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;
5,使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;
6,有用到WebView的JS调用也需要保证写的接口方法不混淆,原因和第一条一样;
7,Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;
-keep class * implements Android.os.Parcelable { # 保持Parcelable不被混淆
public static final Android.os.Parcelable$Creator *;}
8,使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第二条规则。
-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }
Proguard关键字 | 描述 |
---|---|
keep | 保留类和类中的成员,防止被混淆或移除 |
dontwarn | dontwarn是一个和keep可以说是形影不离,尤其是处理引入的library时. |
keepnames | 保留类和类中的成员,防止被混淆,成员没有被引用会被移除 |
keepclassmembers | 只保留类中的成员,防止被混淆或移除 |
keepclassmembernames | 只保留类中的成员,防止被混淆,成员没有引用会被移除 |
keepclasseswithmembers | 保留类和类中的成员,防止被混淆或移除,保留指明的成员 |
keepclasseswithmembernames | 保留类和类中的成员,防止被混淆,保留指明的成员,成员没有引用会被移除 |
Proguard通配符
Proguard通配符 | 描述 |
---|---|
匹配类中的所有字段 | |
匹配类中所有的方法 | |
匹配类中所有的构造函数 | |
* | 匹配任意长度字符,不包含包名分隔符(.) |
** | 匹配任意长度字符,包含包名分隔符(.) |
*** | 匹配任意参数类型 |