AndroidStudio-apk瘦身

1、概述

注: 一下文中app压缩功能,均指AndroidStudio的shrinking功能。
为了是编译出的对外发布的app更小,在release版本编译时,应该打开app压缩功能,使编译时删除无用的代码和无用的资源文件。打开app压缩功能对混淆代码和优化代码都有好处。本文主要说明R8在编译过程如何起作用以及我们怎么自定义配置。
使用Android Gradle plugin3.4.0及更高版本后,插件不再使用ProGuard在编译阶段对代码进行优化,而是使用R8代替执行以下工作:

  • 代码压缩(Code shrinking or tree-shaking): 检测和安全的从app中删除无用的类、变量、方法、属性,以及从app依赖的库中删除。例如,app只使用了库中的很少api接口,压缩功能会识别出不用的代码,然后从app中删除。
  • 资源压缩:从app的资源包及依赖的库中删除无用的资源。该过程和删除代码同时发生,当删除无用代码,不在使用的资源也被删除。
  • 混淆:使类名及类中成员的名称更短,减小DEX文件的大小。
  • 优化:检查和重写代码,进一步减小DEX文件的大小。例如,R8检查到else {}分支不会被执行,R8删除这段代码。

当编译release版本的app时,R8会自动执行以上任务。可以通过ProGuard rules files关闭或定制化R8的行为。实际上,R8是基于已存在的ProGuard rules files运行的,即使更新了Android gradle插件,也不会影响R8的运行和不需要修改已有的规则。

2、打开压缩、混淆、优化功能

当使用AndroidStudio3.4或Android Gradle插件3.4.0及更高版本,R8默认转换Java bytecode为系统运行的DEX格式。但是在创建新的工程时,默认是关闭压缩、混淆、优化功能的,打开这些功能,会增加编译时间和可能在没有充分定义代码保留规则情况下引入代码问题
因此,在测试完成后,准备发布app,编译release时打开这些功能。在工程目录下的build.gradle做如下配置

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

3、R8的配置文件

3.1 概述

R8使用ProGuard rules files修改默认行为和更好的了解app的结构。这个规则文件我们可以进行修改,也有一些规则是编译过程中自动生成的,比如AAPT2,或者从依赖的库中继承的。下表说明R8使用的规则的文件

文件产生源位置说明
Android Studio<module-dir>/proguard-rules.pro当创建一个新的模块,会在模块的根目录下创建proguard-rules.pro文件,默认情况下,文件中未定义任何规则
Android Gradle plugin编译时Gradle生成Android Gradle插件生成proguard-android-optimize.txt,该文件对多数项目有用和对标记@Keep有效。注意 :插件会使用其他的规则文件,只是建议使用proguard-android-optimize.txt规则文件
依赖库(Library dependencies)AAR libraries:<library-dir>/proguar.txt JAR libraries:<library-dir>/META-INF/proguard/当依赖的AAR库发布时使用的是它自己的ProGuard rules file,那么在编译时R8会使用依赖库的规则文件。使用依赖库中的规则,好处是如果库能满足我们的需求,那么使用已有的规则可以省去我们调试的时间;不好的是,规则是叠加的,如果库中的规则关闭了代码优化功能,那么整个工程都不会使用优化功能了
Android Asset Package Tool2(AAPT2)设置minifyEnabled为true情况下,编译完成后,生成<module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txtAAPT2生成keep规则基于APP中的manifest、layouts和其他app的资源。例如,AAPT2为每个在manifest中注册且作为一个入口点的activity增加了keep规则
自定义配置文件默认情况下,当创建一个模块,会创建<module-dir>/proguard-rules.pro作为我们自己的规则文件可以在配置文件中通过包含额外规则文件,使R8在编译时使用

当设置minifyEnabled 为true时,R8会把所有以上表中列的可获取得规则文件结合为一个整体规则。因此,在排除问题时,要留意依赖库中我们不可见的规则。
为了获取R8编译过程中使用的完整的规则文件,在模块下的proguard-rules.pro文件中添加如下内容

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

3.2 添加额外配置文件

当我们新增一个模块(module)时,会创建<module-dir>/proguard-rules.pro文件用于添加自定义规则。也可以在模块下的build.gradle中通过proguardFiles添加其他规则文件。
例如,也可以为某一个编译变体指定一个规则文件,在相对应的风味产品对应的配置块中通过proguardFiles添加,示例如下,flavor2 版本总共会使用3个规则文件

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile(
              'proguard-android-optimize.txt'),
              // List additional ProGuard rules for the given build type here. By default,
              // Android Studio creates and includes an empty rules file for you (located
              // at the root directory of each module).
              'proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
          ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

4、压缩代码

4.1 概述

当minifyEnabled为true时,默认使用R8压缩代码。
代码压缩是R8认为在运行过程中不会使用的代码移除的过程。这个过程 很大程度上减小apk的大小,例如,app依赖了很多库,但仅仅一小部分功能被利用。
R8首先从配置的规则文件中收集所有进入app的进入点。进入点包含Android系统打开app的activity或services。从每一个 进入点开始,R8检阅代码,编译绘制图表,包含所有方法、成员变量和其他的在运行时可能被调用的类。没有被连接的部分图表,被认为是不被使用的,则从app中移除。
在这里插入图片描述
R8根据配置文件中的-keep确定哪些是app的进入点。也就是说,keep规则指定了R8在压缩代码时不应该抛弃的代码,keep指定的也是R8认为是进入app的进入点。Android 插件和AAPT2自动生成keep规则,其中keep规则是根据多数app需要的决定的,比如app的activities、views、services。

4.2 自定义keep规则

在多数情况下,默认的混淆规则文件(proguard-android- optimize.txt)足够R8用于移除不使用的代码。但是,在某些情况,R8很难正确分析正确而导致移除了实际上被使用的代码。一些可能错误移除代码例子如下:

  • app调用的方法接口来自jni。
  • app通过反射调用的接口

测试app会暴露因为错误删除代码导致的错误,也可以生成删除代码的报告,检阅报告查找异常删除。
通过以下方式修复错误删除,增加一条-keep在混淆规则文件中。例如

-keep public class MyClass

也可以在代码中使用@Keep标记,声明要保留的代码。对一个class使用@Keep,会按照class的原样保留。对一个方法或者变量使用@Keep,也会如对class使用,原样保留。注意: 标记在使用AndroidX Annotations Library和打开代码压缩功能时有效。
使用-keep选项,有很多注意事项;尤其是自定义规则时,自定义规则多参考混淆手册(ProGuard Manual)。当代码运行超出了预期,手册中的常见问题(Troubleshooting) 部分可能有遇到的问题解决方案。

5、精化本地库

5.1 概述

默认情况下,编译release app时会精化本地库。精化过程包括移除本地库中的符号表和调试信息。精化的可以明显的减小本地库文件大小。但是,也会导致调试的信息不再有,比如类名、函数名。

5.2 支持本地代码crash分析

Google Play收集设备的本地代码crash,这里不做说明。
Release版本精化本地库后,如果出现crash,是无法定位的,此种情况,可以重新编译一般未精化的版本,使用crash信息进行定位。编译未精化版本的方法,和具体的Gradle插件版本有关。

  • Gradle插件4.1及之后版本
    如果编译app Bundle,在app的build.gradle中添加如下内容,release版本也可以携带未精化的本地库
android.buildTypes.release.ndk.debugSymbolLevel = 'FULL'

注意: 未精化的本地库最大限制为300M,如果调试信息和符号表太大,使用SYMBOL_TABLE代替FULL,如此可以减小文件大小。
如果编译是apk,使用build.gradle编译显示设置信息之前,单独编译生成调试符号文件。如果使用Google Play Console,上传调试符号文件到Google Play。作为编译过程的一部分,gradle插件输出调试符号文件位置如下:

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip
  • Gradle插件4.0及之前版本
    作为编译apk或Bundle的一部分,gradle插件会拷贝保留一份未精化的库文件,目录结构如下:
app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgamenegine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgamenegine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgamenegine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so

注意: 如果使用不同的编译系统,可以按需修改目录结构。

  1. 压缩该目录
$ cd app/build/intermediates/cmake/universal/release/obj
$ zip -r symbols.zip .
  1. 若使用Google Play,手动上传symbols.zip到Google Play Console。
    注意: 调试符号文件有最大300M的限制,如果文件太大,可能因为.so文件中包含了符号表(function name),和DWARF调试信息(file name and lines of code)。这些信息对于定位代码都不需要,可以使用命令精化:$OBJCOPY --strip-debug lib.so lib.so.symOBJCOPY指向的是特定版本的精化命令,如ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-objcopy

6、压缩资源

6.1 概述

资源压缩紧随代码压缩进行。代码压缩移除不使用的代码后,资源压缩识别出需要使用的资源。对于依赖的库也是如此。
打开资源压缩功能,需要在build.gradle中设置shrinkResources为true,同时minifyEnabled也需要为true。例如

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}

在资源压缩之前先调试好代码压缩,代码压缩调试好之后,打开资源压缩功能,再编译时会根据代码压缩来压缩资源。

6.2 自定义需要保留的资源

如果想要指明哪些资源保留,哪些资源遗弃,创建一个\<resource>命名的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。
指明哪些资源文件是需要遗弃的,似乎是一个很傻的做法,遗弃的资源应该直接删掉。这种做法在多个编译变体存在的情况下很有用。例如,我们可以把所有的资源文件放在通用的资源文件目录,在各编译变体的目录下放置keep.xml,在文件中声明哪些文件被相应的编译变体使用,哪些文件不被编译变体使用。也会存在被编译变体识别错误的情况:把需要的资源遗弃。这种情况出现,是因为编译器把资源ID内嵌在代码中,资源分析器可能无法区别值相同的真实使用的资源和代码中的整数值。

6.3 使能严格的相关检查

正常情况下,资源压缩可以准确的决定哪些资源是被利用的。但是,如果代码中使用Resources.getIdentifier() 或者依赖库中使用(如AppCompat),这意味着代码中寻找资源名称,资源名是动态生成的字符串。这么做了,资源压缩默认会谨慎的按照资源名标记哪些是潜在使用的和不使用的,不使用的则移除。
例如,如下代码使所有img_前缀的资源被编辑为被使用的

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

资源压缩也会浏览代码中所有字符串常量,如res/raw/,查找资源使用的URLs类似file:///android_res/drawable//ic_plus_anim_016.png,如果找到一个字符串类似这个或者其他的,会被用来创建URL且不会被移除。
默认情况下,安全的资源压缩模式是打开的。当然也可以关闭这种“利大于弊”的做法,并告诉资源压缩仅保留确认需要保留的资源。若要这做,在keep.xml中设置shrinkMode为strict。示例如下

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

如果使用了严格的资源压缩,且代码中使用了动态生成字符串的方式(如上文提到),那么必须手动使用tools:keep来保留资源。

6.4 删除无用的供选择的资源

Gradle资源压缩仅移除app代码中未涉及到的资源,也以为这不会移除不同配置选择使用的资源。必要情况下,可以在gradle插件脚本中使用resConfigs属性来移除app中不需要的供选择的资源。
例如,如果使用了一个包含语言资源的库,那么apk也就包含了被翻译了的不同语言字符串的消息,而不关心app的其他部分是否做了相同的翻译。如果想要仅保留官方语言的支持,可以使用resConfigs属性指明需要的语言。未被指明的语言会被移除。
示例如下,保留英语和法语两种语言资源

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

类似的,可以通过编译多样apk指明不同配置的设备使用的apk包含不同的屏幕密度、ABI资源。

6.5 合并重复的资源

默认情况下,Gradle会合并相同名称的资源,如相同名称在不同目录的矢量图资源。这种行为,shrinkResources属性无法控制也不可以被关闭,因为这样可以避免一个资源名称匹配到多个资源的错误出现。
资源合并发生在两个或多个文件有相同的名称、类型和修饰符。Gradle会在重复的文件中选择一个认为是最好的文件(基于优先级),传递给AAPT用于分配给apk。
Gradle查找重复文件的位置:

  • 主资源集下的资源,通常目录为src/main/res/.
  • 编译变体目录,如编译类型或风味版本下的资源
  • 依赖库中的资源

Gradle合并重复资源的优先级(从低到高):依赖库中资源->Main->风味版本->编译类型
如果相同的资源出现在同一个资源集,Gradle不会合并,会提示一个错误。例如src/main/res/ 和src/main/res2/中存在相同的资源。

7、混淆代码

7.1 概述

混淆代码的目的是通过缩短类名、方法名、变量名,减小app大小。如下示例使用R8混淆代码

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

混淆代码并不会移除代码,只是dex的大小被显著的减小。混淆代码是重命名了不同部分的代码,一些任务,如跟踪堆栈检查、额外工具需求。
此外,当代码中使用反射,依赖类、方法的的命名,应该把这些签名信息作为入口点增加到代码压缩使用的规则文件中,规则告诉R8,这些代码不仅保留且按照原样保留。

7.2 解析混淆代码后的栈信息

R8混淆代码后,看懂一个堆栈信息是很困难的。除了重命名,R8也会改变堆栈信息的行号。幸运的是,R8每次运行都会创建一个映射文件mapping.txt,该文件中包含了混淆代码前后的对应关系,也包含了堆栈信息的行号映射到原代码的行号。R8保存映射文件在<module- name>/build/outputs/mapping/<build-type>/。
注意: R8生成mapping.txt是覆盖的,建议每次release编译合理保存一份mapping.txt文件,方便在使用旧的release版本时出现问题后分析问题。
为了转换一个混淆后的堆栈信息为可读的,使用ReTrace脚本,该脚本来自代码混淆官方(ProGuard)。

8、代码优化

为了更进一步压缩代码,R8深层次的检阅代码,移除更多的无用代码或重写不够精简的代码。以下是部分优化代码的例子:

  • 如果代码从来不会执行一个else{},那么R8会移除掉else{}
  • 如果一个方法只被一个地方调用,R8会移动代码到调用的地方(即做inline处理)
  • 如果R8发现只有一个子类的类,且这个类不可以被实例化,那么R8会合并这两个类,只保留一个类
  • 了解更多,请关注Jake Wharto的博客 R8 optimization blog posts
    R8不支持打开或关闭离散的优化或修改优化的默认行为。R8会忽略混淆规则中试图改变优化行为的配置,如-optimizations和-optimizationpasses。这个限制对于R8的开发团队很重要,因为R8在持续改进、维护一个标准的优化行为,如此方便AndroidStudio团队很容易分析和解决使用中可能遇到的问题。

打开进取型的优化功能
R8包含很多默认未被打开的额外的优化方法。我们可以在工程下的gradle.properties中使用以下配置打开

android.enableR8.fullMode=true

因为额外的优化行为不同于ProGuard提供的,因此需要增加额外的ProGuard规则来避免运行中出错。例如,我们的代码通过反射调用了一个类,默认情况下,R8假设代码将会在运行时使用这个类,即使实际上并没有,因此该类会被保留和静态初始化。但是,当使用“full mode”,R8不会做将会使用的假设,如果R8判定在运行时不会用到这个类,就会从最终的dex文件中删除这个类;如果要保留这个类,需要在规则文件中增加一条保留规则。
如果在使用R8的‘full mode’中,遇到了任何问题,在R8 FAQ page查找解决方法,若最终未能解决,提交一个bug到report a bug

9、分析解决使用R8的问题

本节介绍一些策略,解决使用R8的压缩、混淆、优化出现的问题。如果在一下介绍中未能找到解决方案,可以阅读 R8 FAQ pageProGuard’s troubleshooting guide

  • 生成移除或保留代码的报告

为了方便分析解决R8的问题,生成一个R8移除的代码报告。在需要报告的模块下的自定义规则文件中增加-printusage <output-dir>/usage.txt。当打开R8,编译app会在指定目录下生成一个报告。报告内容示例如下:

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

如果要查看保留代码的报告,在自定义规则文件中增加-printseeds <output-dir>/seeds.txt。当打开R8,编译app时会生成一个指定位置和名称的报告。报告内容如下示例:

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...
  • 分析资源压缩问题

当使用压缩资源功能,编译窗口会显示移除apk的资源统计。示例如

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

Gradle也会生成一个诊断报告resources.txt在<module-name>/build/outputs/mapping/release/目录下。这个文件包含了详细信息,关于哪些资源和其他资源有关系、哪些资源被保留和哪些资源被移除。
例如查找为什么@drawable/ic_plus_anim_016 被留在了apk中,打开resource.txt,然后查找,结果如下:

16:25:48.005 [QUIET] [system.out] &#64;drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     &#64;drawable/ic_plus_anim_016

想要知道为什么是“@drawable/add_schedule_fab_icon_anim is reachable”,向上查看,会发现该资源文件被列在“The root reachable resources are:”之下。这意味着有代码使用add_schedule_fab_icon_anim(换句话说,该资源文件的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”移除该资源,规则添加方法参考“自定义需要保留的资源”章节。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值