End

Gradle 翻译 ProGuard Shrink 混淆和压缩

本文地址


目录

官方文档

压缩代码和资源

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

代码压缩 Code shrinking】

通过 ProGuard 提供,ProGuard 会检测和移除封装应用中未使用的类、字段、方法和属性,包括 libraries 中的未使用项(这使其成为以 变通方式 解决 64k引用限制 的有用工具)。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的 APK 难以被逆向工程。

资源压缩 Resource shrinking】

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

压缩代码

要通过 ProGuard 启用代码压缩,请在 build.gradle 文件内相应的构建类型中添加 minifyEnabled true

请注意,代码压缩会拖慢构建速度,因此您应该尽可能避免在 debug build 中使用。不过,重要的是您一定要为用于测试的最终 APK 启用代码压缩,因为如果您不能充分地 自定义要保留的代码,可能会引入错误。

例如,下面这段来自 build.gradle 文件的代码用于为 release build 启用代码压缩:

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 SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。

提示:要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

  • proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。
Add project specific特定的 ProGuard rules here. 
You can control the set of applied应用的 configuration files using the proguardFiles setting in build.gradle. 
For more details, see http://developer.android.com/guide/developing/tools/proguard.html

If your project uses WebView with JS, uncomment取消注释 the following and specify指定 the fully qualified class name to the JavaScript interface class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

Uncomment this to preserve the line number information for debugging stack traces 保留调试堆栈跟踪的行号信息.
#-keepattributes SourceFile,LineNumberTable

If you keep the line number information, uncomment this to hide the original source file name.
#-renamesourcefileattribute SourceFile

要添加更多各 build variant 专用的 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 移除的代码。

这些文件保存在 <module-name>/build/outputs/mapping/(productFlavor)/release/ 中。

自定义要保留的代码

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

  • 当应用引用的类 only from AndroidManifest.xml 文件时
  • 当应用调用的方法来自 Java Native Interface (JNI)
  • 当应用在运行时 runtime(例如使用反射或自检introspection)操作代码时

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

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

-keep public class MyClass

或者,您可以向您想保留的代码添加 @Keep 注解。在类上添加 @Keep 可原样保留整个类,在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用 Annotations Support Library 时,才能使用此注解。

在使用 -keep 选项时,有许多事项需要考虑;如需了解有关自定义配置文件的详细信息,请阅读 ProGuard 手册问题排查 一章概述了您可能会在混淆代码时遇到的其他常见问题。

解码混淆过的堆叠追踪

在 ProGuard 压缩代码后,读取堆叠追踪变得困难(即使并非不可行),因为方法名称经过了混淆处理。幸运的是,ProGuard 每次运行时都会创建一个 mapping.txt 文件,其中显示了与混淆过的名称对应的原始类名称、方法名称和字段名称。

请注意,您每次使用 ProGuard 创建发布构建时都会覆盖 mapping.txt 文件,因此您每次发布新版本时都必须小心地保存一个副本。通过为每个发布构建保留一个 mapping.txt 文件副本,您就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试。

在 Google Play 上发布应用时,您可以上传每个 APK 版本的 mapping.txt 文件。Google Play 将根据用户报告的问题对收到的堆叠追踪进行去混淆处理,以便您在 Google Play Developer Console 中进行检查。如需了解详细信息,请参阅帮助中心有关如何 对崩溃堆叠追踪进行去混淆处理 的文章。

要自行将混淆过的堆叠追踪转换成可读的堆叠追踪,请使用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 <sdk-root>/tools/proguard/bin 目录中。

该脚本利用 mapping.txt 文件和您的堆叠追踪生成新的可读堆叠追踪。使用 retrace 工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
//例如:【retrace.bat -verbose mapping.txt obfuscated_trace.txt】

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

通过 Instant Run 启用代码压缩

如果代码压缩在您 增量构建 incrementally building 应用时非常重要,请尝试 适用于 Gradle 的 Android 插件 内置的试用代码压缩器。与 ProGuard 不同,此压缩器支持 Instant Run。

您也可以使用与 ProGuard 相同的配置文件来配置 Android 插件压缩器。但是,Android 插件压缩器不会对您的代码进行混淆处理或优化,它只会删除未使用的代码。因此,您应该仅将其用于 debug builds,并为 release builds 启用 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 构建应用,请先尝试使用它,然后再启用 shrinkResources,因为您可能需要编辑 proguard-rules.pro 文件以保留动态创建或调用的类或方法,然后再开始移除资源。

注:资源压缩器目前不会移除 values/ 文件夹中定义的例如字符串、尺寸、样式和颜色等资源(但是会移除res目录下的drawable、layout、color等资源)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本。有关详情,请参阅问题 70869。

自定义要保留的资源

如果您有想要保留或舍弃的特定资源,请在您的项目中创建一个包含 <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 之中。

指定要舍弃的资源可能看似愚蠢,因为您本可将它们删除,但在使用 build variants 时,这样做可能很有用。例如,如果您明知给定资源表面上会在代码中使用[appears to be used in code](并因此不会被压缩器移除),但实际不会用于给定构建变体,就可以将所有资源放入公用项目目录,然后为每个构建变体创建一个不同的 keep.xml 文件。It's also possible that 构建工具无法根据需要正确识别资源,这是因为编译器会添加内联资源 ID[adds the resource IDs inline],而资源分析器可能不知道真正引用的资源和恰巧具有相同值的代码中的整数值之间的差别。

启用严格引用检查

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

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

String name = String.format("img_%1d", angle + 1);
int res = getResources().getIdentifier(name, "drawable", getPackageName());

资源压缩器还会浏览代码以及各种 res/raw/ 资源中的所有字符串常量[looks through all the string constants in your code],寻找格式类似于[looking for resource URLs in a format similar to] 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"
    }
}

Similarly, you can customize自定义 which screen density or ABI resources to include in your APK by building multiple APKs that each target a different device configuration.

合并重复资源

Merge duplicate resources

默认情况下,Gradle 还会合并同名资源[merges identically named resources],例如可能位于不同资源文件夹中的同名可绘制对象。这一行为不受 shrinkResources 属性控制,也无法停用,因为在有多个资源匹配代码查询的名称时,有必要利用这一行为来避免错误。

只有在两个或更多个文件具有完全相同的资源名称、类型和限定符时,才会进行资源合并。Gradle 会在重复项中选择其视为最佳选择的文件(根据下述优先顺序),并只将这一个资源传递给 AAPT,以供在 APK 文件中分发。

Gradle 会在下列位置寻找重复资源:

  • 与主源集[main source set]关联的主资源,一般位于 src/main/res/ 中。
  • 变体叠加[variant overlays],来自 build type and build flavors。
  • 库项目依赖项。

Gradle 会按以下级联优先顺序合并重复资源:

Dependencies → [Main] → Build flavor → Build type

例如,如果某个重复资源同时出现在主资源和 Build flavor 中,Gradle 会选择 Build flavor 中的重复资源。

如果完全相同的资源出现在同一 source set 中,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 还会在ProGuard 输出文件所在的文件夹中创建一个名为 resources.txt 的诊断文件,该文件包括诸如哪些资源引用了其他资源以及使用或移除了哪些资源等详情。

例如,要了解您的 APK 为何仍包含 @drawable/ic_plus_anim_016,请打开 resources.txt 文件并搜索该文件名。您可能会发现,有其他资源引用了它[it's referenced from another resource],如下所示:

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 属性通知构建系统将它移除。

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates.

上次更新日期:四月 25, 2018

附:一些引用文件或生成的文件

proguard-android.txt

文件路径

文件内容(注意,里面提示此文件不会再维护了,并且是在构建是生成默认的文件):

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained维护 and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead相反, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.

-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose

# Optimization优化 is turned off by default. Dex does not like code run through the ProGuard 
# optimize and preverify steps 优化和预验证步骤 (and performs some of these optimizations on its own).
-dontoptimize
-dontpreverify

# Note that if you want to enable optimization, you cannot just include optimization flags 
# in your own project configuration file; instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your project.properties file.

-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
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# 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);
}

-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

# The support library contains references to对...的引用 newer platform versions.
# Don't warn警告 about those in case this app is linking against链接到 an older
# platform version.  We know about them, and they are safe.
-dontwarn android.support.**

# Understand the @Keep support annotation.
-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>(...);
}

proguard-android-optimize.txt

它包括与 proguard-android.txt 相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.
# Optimizations优化: If you don't want to optimize, use the proguard-android.txt configuration file 
# instead of this one, which turns off the optimization flags. Adding optimization introduces带来
# certain risks一定的风险, since for example not all optimizations performed by ProGuard works on all
# versions of Dalvik. The following flags turn off various各个 optimizations known to have issues问题, 
# but the list may not be complete or up to date最新的. (The "arithmetic"算术 optimization can be used if 
# you are only targeting Android 2.0 or later.)  Make sure you test thoroughly彻底 if you go this route路线.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*    #*/
-optimizationpasses 5
-allowaccessmodification
-dontpreverify

# The remainder其余部分 of this file is identical相同 to the non-optimized非优化 version of the Proguard
# configuration file (except除了 that the other file has flags to turn off optimization).

...
# 后面的与 proguard-android.txt 完全一样

构建时 ProGuard 输出的文件

dump.txt

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

_____________________________________________________________________
- Program class: com/bqt/test/MainActivity
  Superclass:    android/app/ListActivity
  Major version: 0x33
  Minor version: 0x0
    = target 1.7
  Access flags:  0x21
    = public class com.bqt.test.MainActivity extends android.app.ListActivity

Interfaces (count = 0):

Constant Pool (count = 268):
  - Integer [17367043]
  - Integer [2130968617]
  - Integer [2131361821]
  - String []
  - String [
类的限定类名:]
  - String [  
...
193K+ 行

mapping.txt

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

com.bqt.test.MainActivity -> com.bqt.test.MainActivity:
    char[] HEX_DIGITS -> a
    void <init>() -> <init>
    void onCreate(android.os.Bundle) -> onCreate
    void onListItemClick(android.widget.ListView,android.view.View,int,long) -> onListItemClick
    android.os.Bundle getMetaData(android.content.Context) -> a
    java.lang.String getAppSignatureSHA1(android.content.Context) -> b
    android.content.pm.Signature[] getAppSignature(android.content.Context) -> c
    java.lang.String encryptSHA1ToString(byte[]) -> a
    java.lang.String bytes2HexString(byte[]) -> b
    byte[] encryptSHA1(byte[]) -> c
    void <clinit>() -> <clinit>
...
6K+ 行

seeds.txt

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

com.bqt.test.MainActivity
com.bqt.test.MainActivity: MainActivity()
com.bqt.test.R$anim: int abc_fade_in
com.bqt.test.R$color: int material_grey_300
...
4K+ 行

usage.txt

列出从 APK 移除的代码

android.arch.core.BuildConfig
android.arch.core.R
...
3K+ 行

resources.txt

诊断文件,包括哪些资源引用了其他资源,以及使用或移除了哪些资源等详情。

例如,要了解 APK 为何包含 @drawable/icon3,在 resources.txt 中搜索该文件名,会发现,有其他资源引用了它:

layout:layout_a:2131296283 => [drawable:icon3:2131099733]

而搜索另一个文件 @drawable/icon2,会发现,因为没有其他资源引用它,所以最后被移除了(Skipped 跳过):

Skipped unused resource res/drawable/icon2.png: 5720 bytes (replaced with small dummy file of size 67 bytes)

2018-7-22

posted @ 2018-07-22 02:21  白乾涛  阅读(1634)  评论(0编辑  收藏  举报