为了使APK文件尽可能小,您应该启用缩小以删除您的发布版本中未使用的代码和资源。 下面描述如何做,以及如何指定在构建过程中要保留或丢弃的代码和资源。
通过ProGuard实现压缩代码是合适的,ProGuard从您的打包应用程序中检测和删除未使用的类,字段,方法和属性,包括来自包含的代码库(使其成为处理64k参考限制的有价值的工具)。 ProGuard还优化字节码,删除未使用的代码指令,并使用短名称混淆剩余的类,字段和方法。 混淆代码使您的APK难以反向工程,这在您的应用使用安全敏感功能(例如许可验证)时尤其有用。
通过the Android Plugin for Gradle实现压缩资源是合适的,它从您的打包应用程序中删除未使用的资源,包括代码库中未使用的资源。 它与代码缩减结合使用,以便一旦未使用的代码被删除,任何不再被引用的资源也可以被安全地删除。
本文档中的功能依赖于:
- SDK Tools 25.0.10 or higher
- Android Plugin for Gradle 2.0.0 or higher
压缩代码
要使用ProGuard启用压缩代码,请将minifyEnabled true添加到build.gradle文件中的相应构建类型。
请注意,代码缩减会减慢构建时间,因此,如果可能,应避免在调试版本上使用它。 不过,重要的是,您必须在用于测试的最终APK上启用代码缩减,因为如果您没有足够自定义要保留的代码,它可能会导致错误。
例如,来自build.gradle文件的以下代码段使构建release版本时压缩代码:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(‘proguard-android.txt'),
'proguard-rules.pro'
}
}
...
}
注意:使用Instant Run时Android Studio会禁用ProGuard。
除了minifyEnabled属性,proguardFiles属性定义了ProGuard规则:
- getDefaultProguardFile('proguard-android.txt')方法从Android SDK tools / proguard /文件夹获取默认的ProGuard设置。
提示:对于更多的代码缩减,请尝试位于同一位置的proguard-android-optimize.txt文件。 它与proguard-android.txt包括相同的ProGuard规则,但是还包含在字节码级别(内部和跨方法 )执行分析的优化来进一步减少您的APK大小,并帮助它运行更快。 - proguard-rules.pro文件是您可以添加自定义ProGuard规则的位置。 默认情况下,此文件位于模块的根目录(在build.gradle文件旁边)。
要添加特定于每个build variant的更多ProGuard规则,请在相应的productFlavor块中添加另一个proguardFiles属性。 例如,以下Gradle文件将flavor2-rules.pro添加到flavor2 product flavor中。 现在flavor2使用三个ProGuard规则,因为来自release块的那些规则也被应用。
android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
productFlavors {
flavor1 {
}
flavor2 {
proguardFile 'flavor2-rules.pro'
}
}
}
对于每次构建,ProGuard生成以下文件:
- dump.txt
描述APK中所有类文件的内部结构。 - mapping.txt
提供原始和混淆后的类,方法和字段名称之间的转换。 - seeds.txt
列出未被混淆的类和成员。 - usage.txt
列出从APK中移除的代码。
这些文件被保存在
定制要保留的代码
对于某些情况,默认的ProGuard配置文件(proguard-android.txt)就足够了,ProGuard删除所有未使用的代码。 然而,许多情况下,ProGuard很难正确分析,它可能会删除您的应用程序实际需要的代码。 可能会错误地删除代码的一些示例包括:
- 当应用程序引用一个仅来自于AndroidManifest.xml文件的类
- 当应用程序从Java Native Interface(JNI)调用方法时
- 当应用在运行时操作代码(如使用反射或内省)
应该通过测试应用程序来揭示由不适当删除的代码导致的任何错误,但也可以通过查看保存在
要修复错误并强制ProGuard保留某些代码,请在ProGuard配置文件中添加一个-keep行。 例如:
-keep public class MyClass
或者,您可以将@Keep注释添加到要保留的代码。 在类上添加@Keep会保持整个类不变。 将它添加到方法或字段上将保持方法/字段(和它的名称)以及类名称不变。 请注意,此注释仅在使用注释支持库时可用。
使用-keep选项时,您应该考虑许多因素; 有关自定义配置文件的更多信息,请阅读ProGuard手册。 “疑难解答”部分概述了在您的代码被删除时可能遇到的其他常见问题。
解码混淆的stack trace
ProGuard压缩代码后,读取stack trace是很困难(如果不是不可能),因为方法名称被混淆处理。 幸运的是,ProGuard每次运行时都会创建一个mapping.txt文件,它显示混淆后的名称与原始类,方法和字段名称之间的映射关系。 ProGuard将文件保存在
请注意,每次使用ProGuard创建release版本时,mapping.txt文件都会被覆盖,因此每次发布新release版本时都必须小心保存副本。 通过为每个 release build保留mapping.txt文件的副本,如果用户从旧版本的应用程序提交混淆的stack trace,您将能够调试问题。
在Google Play上发布app时,您可以为每个版本的APK上传mapping.txt档案。 然后,Google Play会从用户报告的问题中反混淆进入的stack traces,以便您可以在Google Play开发者控制台中查看这些stack traces。 有关详细信息,请参阅帮助中心文章,了解如何反混淆crash stack traces。
要将混淆的stack trace转换为可读的stack trace,请使用回溯脚本(Windows上为retrace.bat; Mac上为retrace.sh)。 它位于
retrace.bat|retrace.sh [-verbose] mapping.txt []
举例:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
如果不指定stack trace文件,则retrace工具从标准输入读取。
压缩资源
压缩资源只能与压缩代码相结合。 代码压缩器删除所有未使用的代码后,资源压缩器可以识别应用程序仍在使用哪些资源。 尤其当您添加包含资源的代码库时,您必须删除未使用的库代码,以便库资源变为未引用,从而可由资源压缩器移除。
要启用资源压缩,请在build.gradle文件(同时设置minifyEnabled来启动代码压缩)中将shrinkResources属性设置为true。 例如:
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
如果你还没有使用代码压缩构建你的app,那么在启用shrinkResources移除资源之前,你可能需要先编辑proguard-rules.pro文件以保护动态执行和创建的类和方法(比如反射调用的代码)。
注意:资源缩小器当前不会删除在values/ 文件夹中定义的资源(例如strings, dimensions, styles, and colors)。 这是因为Android资源打包工具(AAPT)不允许Gradle插件为资源指定预定义版本。 有关详细信息,请参阅问题70869。
定制要保留的资源
如果有要保留或丢弃的特定资源,请使用
举例:
将此文件保存在项目资源中,例如res / raw / keep.xml。 build时不会把这个文件打包到APK中。
当然可以直接删除资源时却指定要丢弃的资源可能看起来很愚蠢,但这在使用 build variants时可能很有用。 例如,您可以将所有资源放入公共项目目录,然后为每个build variant创建一个不同的keep.xml文件,当给定资源在代码中被使用(因此不会被缩小器删除),但是该资源实际上不会用于给定的build variant,那么该build variant的keep.xml文件应该指定该资源被丢弃。
启用strict reference检查
通常,资源压缩器可以精确地确定一个资源是否被使用。 但是如果代码中调用了Resources.getIdentifier()(或者任何一个库调用了这个方法),这意味着你的代码是基于动态生成的字符串查找资源名称。 当你这样做时,资源压缩器在默认情况下会防御性地运行,并将匹配的名称格式的所有资源标记为可能已使用且无法删除。
例如,以下代码会将所有带有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的格式查找资源URL。 如果它发现这样的字符串或者其他看起来可以用于构造这样的URL,就不会删除URL对应的资源。
这些都是默认情况下启用的安全压缩模式的示例。 但是,您可以关闭此“better safe than sorry”处理,并指定资源压缩器仅保留其确定使用的资源。 为此,请在keep.xml文件中将shrinkMode设置为strict,如下所示:
如果您确实启用了strict压缩模式,并且代码还引用了动态生成的字符串的资源,如上所示,那么您必须使用tools:keep属性手动保留这些资源。
删除未使用的备用资源
Gradle资源压缩器只会移除app code未引用的资源,这意味着它不会移除对应不同设备的备选资源。 如果需要,您可以使用Android Gradle插件的resConfigs属性来删除app不需要的备选资源文件。
例如,如果您使用的库包含语言资源(例如AppCompat或Google Play Services),则APK会包含这些库中所有语言的字符串,无论您的应用程序的其他部分是否有对应的语言。 如果您只想保留app正式支援的语言,可以使用resConfig属性指定这些语言。 将删除未指定语言的任何资源。
以下代码段显示了如何将您的语言资源限制为仅限英语和法语:
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
同样,您可以自定义要在APK中包含哪些screen density 或 ABI资源,并使用APK拆分为不同设备构建不同的APK。
合并重复资源
默认情况下,Gradle还合并相同名称的资源,例如在不同资源文件夹中具有相同名称的drawables。 此行为不受shrinkResources属性控制且不能禁用,因为必须避免在多个资源与代码查找的名称匹配时出现错误。
仅当两个或多个文件共享相同的资源名称,类型和限定符时,才会进行资源合并。 Gradle选择哪个被认为是重复项中最佳选择的文件(基于下面描述的优先级顺序),并且仅将那个资源传递给AAPT以在APK文件中分发。
Gradle在以下位置查找重复的资源:
- The main resources,与the main source set相关,一般位于src / main / res /
- The variant overlays, from the build type and build flavors.。
- The library project dependencies.
Gradle以以下级联优先级顺序合并重复资源:
Dependencies → Main → Build flavor → Build type
例如,如果重复资源同时出现在main resources和build flavor中,Gradle将选择build flavor中的一个。
如果相同的资源出现在同一源集中,则Gradle不能合并它们并发出资源合并错误。 如果在build.gradle文件的sourceSet属性中定义多个源集,例如,如果src / main / res /和src / main / res2 /包含相同的资源,则可能会发生这种情况。
排查资源压缩问题
当压缩资源时,Gradle Console会显示从应用程序包中删除的资源的摘要。 例如:
:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning
Gradle还在
例如,要了解为什么@ drawable / ic_plus_anim_016仍在您的APK中,请打开resources.txt文件并搜索该文件名。 您可能会发现它被另一个资源引用,如下所示:
16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out] @drawable/ic_plus_anim_016
你现在需要知道为什么@ drawable / add_schedule_fab_icon_anim是可及的,如果你向上搜索,你会发现该资源列在“The root reachable resources are:”。 这意味着有一个代码引用add_schedule_fab_icon_anim(也就是说,它的R.drawable ID在可达代码中找到)。
如果不使用strict检查,如果有字符串常量看起来像被用来构造资源名称从而动态加载资源,则该资源ID可以标记为可达。 在这种情况下,如果您在build输出中搜索资源名称,您可能会发现这样的消息:
10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
used because it format-string matches string pool constant ic_plus_anim_%1$d.
如果您看到其中一个字符串,并且您确定该字符串未用于动态加载给定资源,则可以使用以下工具:discard属性通知build系统将其删除,可以参考上面的 定制要保留的资源。
该文章是对于Google文档的翻译:
Shrink Your Code and Resources