压缩代码和资源

为了使APK文件尽可能小,您应该启用缩小以删除您的发布版本中未使用的代码和资源。 下面描述如何做,以及如何指定在构建过程中要保留或丢弃的代码和资源。

通过ProGuard实现压缩代码是合适的,ProGuard从您的打包应用程序中检测和删除未使用的类,字段,方法和属性,包括来自包含的代码库(使其成为处理64k参考限制的有价值的工具)。 ProGuard还优化字节码,删除未使用的代码指令,并使用短名称混淆剩余的类,字段和方法。 混淆代码使您的APK难以反向工程,这在您的应用使用安全敏感功能(例如许可验证)时尤其有用。

通过the Android Plugin for Gradle实现压缩资源是合适的,它从您的打包应用程序中删除未使用的资源,包括代码库中未使用的资源。 它与代码缩减结合使用,以便一旦未使用的代码被删除,任何不再被引用的资源也可以被安全地删除。

本文档中的功能依赖于:

  1. SDK Tools 25.0.10 or higher
  2. 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规则:

  1. getDefaultProguardFile('proguard-android.txt')方法从Android SDK tools / proguard /文件夹获取默认的ProGuard设置。
    提示:对于更多的代码缩减,请尝试位于同一位置的proguard-android-optimize.txt文件。 它与proguard-android.txt包括相同的ProGuard规则,但是还包含在字节码级别(内部和跨方法 )执行分析的优化来进一步减少您的APK大小,并帮助它运行更快。
  2. 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生成以下文件:

  1. dump.txt
    描述APK中所有类文件的内部结构。
  2. mapping.txt
    提供原始和混淆后的类,方法和字段名称之间的转换。
  3. seeds.txt
    列出未被混淆的类和成员。
  4. usage.txt
    列出从APK中移除的代码。

这些文件被保存在 <module-name>/build/outputs/mapping/release/目录下。

定制要保留的代码

对于某些情况,默认的ProGuard配置文件(proguard-android.txt)就足够了,ProGuard删除所有未使用的代码。 然而,许多情况下,ProGuard很难正确分析,它可能会删除您的应用程序实际需要的代码。 可能会错误地删除代码的一些示例包括:

  1. 当应用程序引用一个仅来自于AndroidManifest.xml文件的类
  2. 当应用程序从Java Native Interface(JNI)调用方法时
  3. 当应用在运行时操作代码(如使用反射或内省)

应该通过测试应用程序来揭示由不适当删除的代码导致的任何错误,但也可以通过查看保存在<module-name> / build / outputs / mapping / release /中的usage.txt输出文件来检查删除的代码。

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

-keep public class MyClass

或者,您可以将@Keep注释添加到要保留的代码。 在类上添加@Keep会保持整个类不变。 将它添加到方法或字段上将保持方法/字段(和它的名称)以及类名称不变。 请注意,此注释仅在使用注释支持库时可用。

使用-keep选项时,您应该考虑许多因素; 有关自定义配置文件的更多信息,请阅读ProGuard手册“疑难解答”部分概述了在您的代码被删除时可能遇到的其他常见问题。

解码混淆的stack trace

ProGuard压缩代码后,读取stack trace是很困难(如果不是不可能),因为方法名称被混淆处理。 幸运的是,ProGuard每次运行时都会创建一个mapping.txt文件,它显示混淆后的名称与原始类,方法和字段名称之间的映射关系。 ProGuard将文件保存在<module-name> / build / outputs / mapping / release /目录中。

请注意,每次使用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)。 它位于<sdk-root> / tools / proguard /目录中。 该脚本采用mapping.txt文件和您的stack trace,产生一个新的,可读的stack trace。 使用retrace工具的语法是:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

举例:

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。

定制要保留的资源

如果有要保留或丢弃的特定资源,请使用<resources>tag在项目中创建一个XML文件,并在tools:discard属性中指定要保留的资源和在tools:keep属性中指定要丢弃的资源。 两个属性都接受以逗号分隔的资源名称列表。 您可以使用星号字符作为通配符。

举例:

<?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。 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,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools: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在以下位置查找重复的资源:

  1. The main resources,与the main source set相关,一般位于src / main / res /
  2. The variant overlays, from the build type and build flavors.。
  3. 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还在<module-name> / build / outputs / mapping / release /(与ProGuard生存的文件保存在相同的文件夹下)中创建一个名为resources.txt的诊断文件。 此文件包括详细信息,例如资源之间的引用关系以及哪些资源被使用或删除。

例如,要了解为什么@ 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

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

推荐阅读更多精彩内容