压缩代码和资源

原文:https://developer.android.com/studio/build/shrink-code.html

要尽可能减小 APK 文件,您应该启用压缩来移除发布构建中未使用的代码和资源。此页面介绍如何执行该操作,以及如何指定要在构建时保留或舍弃的代码和资源。

代码压缩通过 ProGuard 提供,ProGuard 会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决64k 引用限制的有用工具)。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的 APK 难以被逆向工程,这在应用使用许可验证等安全敏感性功能时特别有用。

资源压缩通过 Android Plugin for Gradle 提供,该插件会移除封装应用中未使用的资源,包括代码库中未使用的资源。它可与代码压缩发挥协同效应,使得在移除未使用的代码后,任何不再被引用的资源也能安全地移除。

本文介绍的功能依赖下列组件:

SDK Tools25.0.10 或更高版本

Android Plugin for Gradle2.0.0 或更高版本

压缩代码

要启用通过 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。

除了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 都会输出下列文件:

dump.txt

说明 APK 中所有类文件的内部结构。

mapping.txt

提供原始与混淆过的类、方法和字段名称之间的转换。

seeds.txt

列出未进行混淆的类和成员。

usage.txt

列出从 APK 移除的代码。

这些文件保存在/build/outputs/mapping/release/。

自定义要保留的代码

对于某些情况,默认 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。不过,ProGuard 难以对许多情况进行正确分析,可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:

当应用引用的类只来自AndroidManifest.xml文件时

当应用调用的方法来自 Java 原生接口 (JNI) 时

当应用在运行时(例如使用反射或自检)操作代码时

测试应用应该能够发现因不当移除而导致的错误,但您还可通过查看/build/outputs/mapping/release/中保存的usage.txt输出文件来检查移除了哪些代码。

要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行-keep代码。例如:

-keeppublicclassMyClass

您还可以向您想保留的代码添加@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 中进行检查。如需了解详细信息,请参阅帮助中心有关如何对崩溃堆叠追踪进行去混淆处理的文章。

要自行将混淆过的堆叠追踪转换成可读的堆叠追踪,请使用retrace脚本(在 Windows 上为retrace.bat;在 Mac/Linux 上为retrace.sh)。它位于/tools/proguard/目录中。该脚本利用mapping.txt文件和您的堆叠追踪生成新的可读堆叠追踪。使用 retrace 工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt []

例如:

retrace.bat -verbose mapping.txt obfuscated_trace.txt

如果您不指定堆叠追踪文件,retrace 工具会从标准输入读取。

压缩资源

资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在您添加包含资源的代码库时体现得尤为明显——您必须移除未使用的内容库代码,使内容库资源变为未引用资源,才能通过资源压缩器将它们移除。

要启用资源压缩,请在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属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。您可以使用星号字符作为通配符。

例如:


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文件。

启用严格引用检查

正常情况下,资源压缩器可准确判定系统是否使用了资源。不过,如果您的代码调用Resources.getIdentifier()(或您的任何内容库进行了这一调用——AppCompat内容库会执行该调用),这就表示您的代码是根据动态生成的字符串查询资源名称。当您执行这一调用时,默认情况下资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。

例如,以下代码会使系统将所有带img_前缀的资源标记为已使用。

Stringname=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,如下所示:


tools: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 会显示它从应用软件包中移除的资源的摘要。例如:

:android:shrinkDebugResources

Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%

:android:validateDebugSigning

Gradle 还会在/build/outputs/mapping/release/(ProGuard 输出文件所在的文件夹)中创建一个名为resources.txt的诊断文件。该文件包括诸如哪些资源引用了其他资源以及使用或移除了哪些资源等详情。

例如,要了解您的 APK 为何仍包含@drawable/ic_plus_anim_016,请打开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)。

如果您使用的不是严格检查,则存在看似可用于为动态加载资源构建资源名称的字符串常量时,可将资源 ID 标记为可访问。在这种情况下,如果您在构建输出中搜索资源名称,可能会找到类似下面这样的消息:

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.

如果您看到一个这样的字符串,并且您能确定该字符串未用于动态加载给定资源,就可以按照有关如何自定义要保留的资源部分中所述利用tools:discard属性通知构建系统将它移除。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 为了使APK文件尽可能小,您应该启用缩小以删除您的发布版本中未使用的代码和资源。 下面描述如何做,以及如何指定在构...
    小芸论阅读 1,229评论 0 5
  • 这部分内容,自己其实看过很多遍,但是无一例外的是:在若干天后,自己在用到这一方面的内容时,竟然忘了,我怀疑自己有个...
    GYLEE阅读 374评论 0 1
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 原文链接:https://developer.android.com/studio/build/shrink-co...
    抹香君阅读 5,492评论 0 7
  • 昨天宝宝会扶着迈步了,周日时发现他已经玩拨珠玩得很溜了。不知不觉小家伙已经十个月,为什么我还天天云里雾里地,似乎一...
    清风田田阅读 96评论 0 0