前言
相信不少开发在发布时被代码混淆弄得一头雾水,大多都是百度一下,看看别人的混淆规则,复制粘贴拿来试一试,直到最后弄成了,也不知道为什么混淆规则要这么写,以及混淆都对自己的代码做了什么?不要问我为什么这么清楚,因为我也是这么过来的?
什么是混淆?
混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的情况下,即使被反编译,也将难以阅读。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。
其实混淆包含了一系列的复杂操作,上面的解释只是通俗意义上的解释,Android中的混淆分为两部分,代码压缩和资源压缩,而代码压缩又包含了压缩、优化、混淆、预校验四部分,其中优化和预校验两个步骤在Android中默认是关闭的。
- shrink:压缩,移除无效的类、类成员、方法、属性等。
- optimize:优化,分析和优化方法的二进制代码;根据
proguard-android-optimize.txt
中的描述,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。 - obfuscate:混淆,把类名、属性名、方法名替换为简短且无意义的名称。
- preverify:预校验,添加预校验信息。这个预校验是作用在Java平台上的,Android平台上不需要这项功能,去掉之后可以加快混淆速度。
对混淆有了一个初步的概念框架后,在看看Android官方是怎么介绍它的。
压缩代码和资源
要尽可能减小 APK 文件,您应该启用压缩来移除发布构建中未使用的代码和资源。此页面介绍如何执行该操作,以及如何指定要在构建时保留或舍弃的代码和资源。
代码压缩通过 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-rules.pro
文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle
文件旁)。
提示:要想做进一步的代码压缩,请尝试使用位于同一位置的
proguard-android-optimize.txt
文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。(混淆中的优化部分,默认关闭的)
每次构建时 ProGuard 都会输出下列文件:
dump.txt
说明 APK 中所有类文件的内部结构。
mapping.txt
提供原始与混淆过的类、方法和字段名称之间的转换。
seeds.txt
列出未进行混淆的类和成员。
usage.txt
列出从 APK 移除的代码。
这些文件保存在 <module-name>/build/outputs/mapping/release/
中。
自定义要保留的代码
对于某些情况,默认 ProGuard 配置文件 (proguard-android.txt
) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。不过,ProGuard 难以对许多情况进行正确分析,可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:
- 当应用引用的类只来自
AndroidManifest.xml
文件时 - 当应用调用的方法来自 Java 原生接口 (JNI) 时
- 当应用在运行时(例如使用反射或自检)操作代码时
Android默认已经帮我们保持了AndroidManifest.xml里面所有的类,这里的第一条我有些不明白是什么意思
测试应用应该能够发现因不当移除的代码而导致的错误,但您也可以通过查看 <module-name>/build/outputs/mapping/release/
中保存的 usage.txt
输出文件来检查移除了哪些代码。
要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行 -keep
代码。例如:
-keep public class MyClass
-keep的相关使用后面另说
或者,您可以向您想保留的代码添加 @Keep
注解。在类上添加 @Keep
可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。
通过 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'
}
}
}
压缩资源
资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在您添加包含资源的代码库时体现得尤为明显 - 您必须移除未使用的库代码,使库资源变为未引用资源,才能通过资源压缩器将它们移除。
要启用资源压缩,请在 build.gradle
文件中将 shrinkResources
属性设置为 true
(在用于代码压缩的 minifyEnabled
旁边)。例如:
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
shrinkResources只有在
minifyEnabled启用后才有用
注:资源压缩器目前不会移除
values/
文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本。
自定义要保留的资源
如果您有想要保留或舍弃的特定资源,请在您的项目中创建一个包含 <resources>
标记的 XML 文件,并在 tools:keep
属性中指定每个要保留的资源,在 tools:discard
属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。您可以使用星号字符作为通配符。
例如:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
tools:discard="@layout/unused2" />
将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml
。构建不会将该文件打包到 APK 之中。
启用严格引用检查
正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码调用 Resources.getIdentifier()
(或您的任何库进行了这一调用 - AppCompat 库会执行该调用),这就表示您的代码将根据动态生成的字符串查询资源名称。当您执行这一调用时,默认情况下资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。
例如,以下代码会使所有带 img_
前缀的资源标记为已使用。
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
资源压缩器还会浏览代码以及各种 res/raw/
资源中的所有字符串常量,寻找格式类似于 file:///android_res/drawable//ic_plus_anim_016.png
的资源网址。如果它找到与其类似的字符串,或找到其他看似可用来构建与其类似的网址的字符串,则不会将它们移除。
这些是默认情况下启用的安全压缩模式的示例。但您可以停用这一“有备无患”处理方式,并指定资源压缩器只保留其确定已使用的资源。要执行此操作,请在 keep.xml
文件中将 shrinkMode
设置为 strict
,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict" />
如果您已启用严格压缩模式,并且代码也引用了包含动态生成字符串的资源(如上所示),则必须利用 tools:keep
属性手动保留这些资源。
移除未使用的备用资源
Gradle 资源压缩器只会移除未被您的应用代码引用的资源,这意味着它不会移除用于不同设备配置的备用资源。必要时,您可以使用 Android Gradle 插件的 resConfigs
属性来移除您的应用不需要的备用资源文件。
例如,如果您使用的库包含语言资源(例如使用的是 AppCompat 或 Google Play 服务),则 APK 将包括这些库中消息的所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。如果您想只保留应用正式支持的语言,则可以利用 resConfig
属性指定这些语言。系统会移除未指定语言的所有资源。
下面这段代码展示了如何将语言资源限定为仅支持英语和法语:
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
Google的API是相当细致了,对混淆的使用、配置和混淆文件都做了详细的介绍。相信大部分开发接触混淆都是代码压缩上,以前我所了解的资源压缩只有那一句配置代码,看了文档才知道资源压缩其实涉及很多东西,可以做非常细致的定制,所以,没事还是多看看官方文档。。还有,上面只是文档中的部分内容,想要看完整介绍的可以点击这个
我们该混淆什么?
其实上面的文档中提到,Android默认的 ProGuard 配置文件 (proguard-android.txt
) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。那么我们就看看默认的 ProGuard 配置文件里都做了哪些配置
# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames
# 不去忽略非公共库的类
-dontskipnonpubliclibraryclasses
-verbose
#关闭字节码优化
-dontoptimize
#关闭预校验
-dontpreverify
# 保留Annotation不混淆
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
#保持本地方法
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
#保持集成自view对象的get、set方法
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
#保持Activity中的onClick方法
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
#保持枚举类成员属性
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#保持Android的Parcelable序列化对象
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
#保持R文件
-keepclassmembers class **.R$* {
public static <fields>;
}
# 忽略support包的警告
-dontwarn android.support.**
# Understand the @Keep support annotation.
# 保持keep注解相关类和类成员
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
哈哈,是不是有很多很眼熟的配置,百度混淆规则的时候,可没少看到吧,其实Android默认规则里就有了,所以根本不需要再添加到自己的混淆规则中。
那么我们要在自己的混淆文件中做哪些配置呢?
- 首先,照着上面先把自己配置文件中多余的规则去掉,不需要。
- 第三方依赖,需要混淆的话应该都有标明。
- 实体类,比如根据json转换相关的类。
- 跟反射相关、JNI相关的类和属性、方法以及应用引用的类只来自
AndroidManifest.xml
文件时
我的项目就按上面的配置来的,一点事儿没有,应该能满足大部分项目的混淆需求了,如果还需要加什么,到时跟着日志提示往配置里加就是了。
Keep指令的用法
配置文件里常见的什么-keep、-keepnames、-keepclassmembers...都是什么意思,又有啥区别?
作用范围 | 防止移除或重命名 | 防止重命名 |
类和类成员 | -keep | -keepnames |
类成员 | -keepclassmembers | -keepclassmembernames |
如果存在某成员,保留该成员和类 | -keepclasseswithmembers | -keepclasseswithmembernames |
简单来讲,后缀带names的指令只能防止被重命名,如果该资源没有被用到,还是会被移除,而后缀不带names的指令,不管资源有没有被用到,都会保留。至于带with的两条指令,看着好像和keep、keepnames都是保持类和类成员的,其实它们的区别看butterknife的混淆规则就知道了
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
这啥意思呢,这两句声明了凡是使用了butterknife注解的方法和属性和它们所在的类保持不混淆,如果有就不混淆,没有则该咋样咋样,相比keep、keepnames更加灵活一些。
示例
com.***.***.activity.MainActivity -> com.***.***.activity.MainActivity:
android.widget.LinearLayout mLlContent -> mLlContent
android.widget.RadioGroup mRadioGroup -> mRadioGroup
android.widget.RadioButton mRbFile -> mRbFile
android.support.v4.app.Fragment mCurrentFrag -> d
android.support.v4.app.FragmentManager mFragmentManager -> e
android.support.v4.app.FragmentTransaction mTransaction -> f
可以看到用了butterknife注解的属性和类没有混淆,其他属性还是混淆了。其实如果不知道用哪个合适,用keep准没错,简单粗暴。
keep指令修饰符
一句完整的keep指令包括
[保持命令] [类] {
[成员]
}
“类”代表类相关的限定条件,它将最终定位到某些符合该限定条件的类。它的内容可以使用:
- 具体的类
- 访问修饰符(public、protected、private)
- 通配符*,匹配任意长度字符,但不含包名分隔符(.)
- 通配符**,匹配任意长度字符,并且包含包名分隔符(.)
- extends,即可以指定类的基类
- implement,匹配实现了某接口的类
- $,内部类
“成员”代表类成员相关的限定条件,它将最终定位到某些符合该限定条件的类成员。它的内容可以使用:
- <init> 匹配所有构造器
- <fields> 匹配所有属性
- <methods> 匹配所有方法
- 通配符*,匹配任意长度字符,但不含包名分隔符(.)
- 通配符**,匹配任意长度字符,并且包含包名分隔符(.)
- 通配符***,匹配任意参数类型
- …,匹配任意长度的任意类型参数。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 这些方法。
- 访问修饰符(public、protected、private)
常用keep指令示例
不移除和混淆某个类和类成员
-keep class com.test.demo.TestBean{*;}
不移除和混淆某个类的子类的类成员
-keepclassmember class * extends com.test.demo.Test{*;}
不移除和混淆某个包下的所有类及类成员(包括子包)
-keep class com.test.demo.**{*;}
不混淆某个类的公共方法和公共静态属性
-keepclassmembernames class com.test.demo.Test{
public <methods>;
public static <fields>;
}
不混淆被@keep注解标注的构造方法
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}