尽可能减少APK体积,是我们发布应用时应该遵守的准则之一,一是可以帮用户节省下载时的流量,二是可以减少所占用的内存。如何做呢?当然,最简单的就是移除发布构建中未使用的代码和资源!
Android的代码压缩通过Proguard提供,Proguard会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中未使用的项。Proguard还可以优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码还可以防止我们发布的apk被逆向工程,即反编译。
要通过Proguard启用代码压缩,需要在build.gradle文件内相应的构建类型中添加:
minifyEnabled true
但是请注意,代码压缩会使我们的构建速度变慢,因此我们应该尽可能的避免在调试构建中使用代码压缩。更重要的一点是我们一定要为最终测试的apk启用代码压缩,因为如果我们不能充分的自定义要保留的代码(混淆规则中定义的不被混淆的代码),可能会引发错误。
下面这段来自 build.gradle 文件的代码用于为发布构建启用代码压缩,调试构建时不启用代码压缩
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard- android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
注:AndroidStudio会在InstantRun时停用Proguard
除了使用minifyEnabled属性外,还有自定义Proguard规则的proguardFiles属性:
getDefaultProguardFile(‘proguard-android.txt’) 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。
提示:要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。
构建产生的文件说明:
每次构建时Proguard都会输出以下文件(位于项目的/build/outputs/mapping/release/目录下):
对于一般情况,默认的Proguard规则(proguard-rule.pro)足以满足我们的需要。Proguard会移除所有(并且只会移除)未使用的代码。不过Proguard难以对复杂的情况进行分析,可能会移除真正有用的代码。举例来说,可能错误移除代码的情况包括:
要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行 -keep 代码。例如:
-keep public class MyClass
或者,我们可以向想保留的代码添加 @Keep 注解。在类上添加 @Keep 可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。在使用 -keep 选项时,有许多事项需要考虑;如需了解有关自定义配置文件的详细信息,可查阅Proguard相关手册
在 ProGuard 压缩代码后,读取堆叠追踪变得困难(即使并非不可行),因为方法名称经过了混淆处理。幸运的是,ProGuard 每次运行时都会创建一个 mapping.txt 文件,其中显示了与混淆过的名称对应的原始类名称、方法名称和字段名称。ProGuard 将该文件保存在应用的/build/outputs/mapping/release/ 目录中。
请注意,我们每次使用 ProGuard 创建发布构建时都会覆盖 mapping.txt 文件,因此我们每次发布新版本时都必须小心地保存一个副本。通过为每个发布构建保留一个 mapping.txt 文件副本,我们就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试。
在 Google Play 上发布应用时,我们可以上传每个 APK 版本的 mapping.txt 文件。Google Play 将根据用户报告的问题对收到的堆叠追踪进行去混淆处理,以便我们在 Google Play Developer Console 中进行检查
case:国内Android开发者使用友盟进行第三方统计时,如果我们发布的apk是经过混淆处理的,那么我们统计的错误日志我们是无法直接进行查看的,需要上传对应版本的mapping文件。才会看到具体的错误信息。当然,我们也可以使用proguard工具(sdk/tools/proguard/bin/proguardgui.bat工具):
图中标注的地方分别是:进行反混淆编译的按钮,导入mapping文件的位置,混淆后的日志
如果代码压缩在我们调试构建应用时非常重要,那么我们必须尝试使用Gradle的Android插件内置的试用代码压缩器,此压缩器支持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进行代码压缩,那我们需先启用它,然后在使用shrinkReserouces压缩资源,因为我们可能需要编辑proguard-rule.pro去自定义保留动态创建或者调用的类、方法等。然后才开始移除资源。
注:资源压缩器目前不会移除 values/ 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本
如果想要保留或舍弃特定的资源,我们可以在项目中创建一个包含 标记的 XML 文件,并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。我们可以使用星号字符作为通配符
<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 之中。
指定要舍弃的资源可能看似愚蠢,因为本可将它们删除,但在使用构建变体时,这样做可能很有用。例如,如果明知给定资源表面上会在代码中使用(并因此不会被压缩器移除),但实际不会用于给定构建变体,就可以将所有资源放入公用项目目录,然后为每个构建变体创建一个不同的 keep.xml 文件。构建工具也可能无法根据需要正确识别资源,这是因为编译器会添加内联资源 ID,而资源分析器可能不知道真正引用的资源和恰巧具有相同值的代码中的整数值之间的差别。
Gradle 资源压缩器只会移除未被应用代码引用的资源,这意味着它不会移除用于不同设备配置的备用资源。必要时,您可以使用 Android Gradle 插件的 resConfigs 属性来移除我们的应用不需要的备用资源文件。
例如,如果我们使用的库包含语言资源(例如使用的是 AppCompat 或 Google Play 服务),则 APK 将包括这些库中消息的所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。如果想只保留应用正式支持的语言,则可以利用 resConfig 属性指定这些语言。系统会移除未指定语言的所有资源。
下面这段代码展示了如何将语言资源限定为仅支持英语和法语:
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
默认情况下,Gradle 还会合并同名资源,例如可能位于不同资源文件夹中的同名可绘制对象。这一行为不受 shrinkResources 属性控制,也无法停用,因为在有多个资源匹配代码查询的名称时,有必要利用这一行为来避免错误。
只有在两个或更多个文件具有完全相同的资源名称、类型和限定符时,才会进行资源合并。Gradle 会在重复项中选择其视为最佳选择的文件(根据下述优先顺序),并只将这一个资源传递给 AAPT,以供在 APK 文件中分发。
Gradle 会在下列位置寻找重复资源:
Gradle 会按以下级联优先顺序合并重复资源:依赖项 → 主资源 → 构建风味 → 构建类型
例如,如果某个重复资源同时出现在主资源和构建风味中,Gradle 会选择构建风味中的重复资源。
如果完全相同的资源出现在同一源集中,Gradle 无法合并它们,并且会发出资源合并错误。如果在 build.gradle 文件的 sourceSet 属性中定义了多个源集,则可能会发生这种情况,例如,如果 src/main/res/ 和 src/main/res2/ 包含完全相同的资源,就可能会发生这种情况