写点什么

向工程腐化开炮 | proguard 治理

  • 2022 年 2 月 10 日
  • 本文字数:12055 字

    阅读完需:约 40 分钟

向工程腐化开炮 | proguard治理


作者:刘天宇(谦风)


工程腐化是 app 迭代过程中,一个非常棘手的问题,涉及到广泛而细碎的具体细节,对研发效能 &体验、工程 &产物质量、稳定性、包大小、性能,都有相对“隐蔽”而间接的影响。一般不会造成不可承受的障碍,却时常蹦出来导致“阵痛”,有点像蛀牙或智齿,到了一定程度不拔不行,但不同的是,工程的腐化很难通过一次性“拔除”来根治,任何一次“拔除”之后,需要有效的可持续治理方案,形成常态化的防腐体系。


工程腐化拆解来看,是组成 app 的代码工程中,工程结构本身,以及各类“元素”(manifest、代码、资源、so、配置)的腐化。优酷架构团队近年来,持续在进行思考、实践与治理,并沉淀了一些技术、工具、方案。现逐一分类汇总,辅以相关领域知识讲解,整理成为《向工程腐化开炮》系列技术文章,分享给大家。希望更多同学,一起加入到与工程腐化的这场持久战中。


本文为系列文章首篇,将聚焦于 java 代码 proguard,这一细分领域。对工程腐化,直接开炮!


在 Android(java)开发领域,一般提到“代码 proguard”,是指利用 Proguard 工具对 java 代码进行裁剪、优化、混淆处理,从而实现无用代码删除(tree-shaking)、代码逻辑优化、符号(类、变量、方法)混淆。proguard 处理过程,对 apk 构建耗时、产物可控性(运行时稳定性)、包大小、性能,都有重要影响。


很多时候开发者会用“混淆”来代指整个 Proguard 处理,虽然不准确,但结合语境来理解,只要不产生歧义,也无伤大雅。值得注意的是,google 官方已经在近几年的 Android Gradle Plugin 中,使用自研的 R8 工具替代了 Proguard 工具,来完成上述三个功能。但“代码 proguard”的说法,已经形成惯用语,在本文中除非特别说明,“代码 proguard”就是指处理过程,而非 Proguard 工具本身。

基础知识

本章先简要介绍一些基础知识,方便大家对 proguard 有一个“框架性”的清晰认知。

功能介绍

Proguard 的三个核心功能,作用如下:


  • 裁剪(shrink)。通过对所有代码引用关系,进行整体性的静态分析,检测并移除无用的类、变量、方法、属性。对最终 apk 的减小,具有重要作用;

  • 优化(optimize)。这是整个 Proguard 处理过程中,最复杂的一部分。通过对代码执行逻辑的深层次分析,移除无用的代码分支、方法参数、本地变量,对方法/类进行内联,甚至是优化指令集合,总计包含几十项优化项。一方面可以降低代码大小占用,另一方面,也是最为重要的,是能够降低运行时方法执行耗时;

  • 混淆(obfuscate)。通过缩短类、变量、方法名称的方式,降低代码大小占用,对最终 apk 的减小,同样具有重要作用。同时,也是增加 apk 防破解难度的一个初级技术方案。


上述三个处理过程,shrink 和 optimize 交替进行,根据配置可以循环多次(R8 不可配置循环次数)。一个典型的 Proguard 处理过程如下:



Proguard 处理过程


其中,app classes 包括 application 工程、sub project 工程、外部依赖 aar/jar、local jar、flat dir aar 中的所有 java 代码。library classes 则包括 android framework jar、legacy jars 等仅在编译期需要的代码,运行时由系统提供,不会打包到 apk 中。

配置项

Proguard 提供了强大的配置项,对整个处理过程进行定制。在这里,将其划分为全局性配置,以及 keep 配置两类。注意,R8 为了保持处理过程的一致可控性,以及更好的处理效果,取消了对大部分全局性配置的支持。

全局性配置

全局性配置,是指影响整体处理过程的一些配置项,一般又可以分为以下几类:


1、裁剪配置


  • -dontshrink。指定后,关闭裁剪功能;

  • -whyareyoukeeping。指定目标类、变量、方法,为什么被“keep 住”,而没有在 apk 中被裁剪掉。注意,R8 和 Proguard 给出的结果含义并不相同。来直观看下对比:


# 示例:类TestProguardMethodOnly被keep规则直接“keep住”,TestProguardMethodOnly中的一个方法中,调用了TestProguardFieldAndMethod类中的方法。
# Proguard给出的结果,是最短路径,即如果多个keep规则/引用导致,只会给出最短路径的信息Explaining why classes and class members are being kept...
com.example.myapplication.proguard.TestProguardMethodOnly is kept by a directive in the configuration.
com.example.myapplication.proguard.TestProguardFieldAndMethod is invoked by com.example.myapplication.proguard.TestProguardMethodOnly: void methodAnnotation() (13:15) is kept by a directive in the configuration.# 结果解读: # 1. “is kept by a directive in the configuration.”,TestProguardMethodOnly是被keep规则直接“keep住”# 2. “is invoked by xxxx",TestProguardFieldAndMethod是被TestProguardMethodOnly调用,导致被“keep住”;“is kept by a directive in the configuration.”,TestProguardMethodOnly被keep规则直接“keep住”

# R8给出的结果,是类被哪个keep规则直接命中,即如果类被其他保留下来的类调用,但是没有keep规则直接对应此类,那么此处给出的结果,是“Nothing is keeping xxx"com.example.myapplication.proguard.TestProguardMethodOnly|- is referenced in keep rule:| /Users/flyeek/workspace/code-lab/android/MyApplication/app/proguard-rules.pro:55:1Nothing is keeping com.example.myapplication.proguard.TestProguardFieldAndMethod# 结果解读: # 1. “is referenced in keep rule: xxx”,TestProguardMethodOnly是被具体的这一条规则直接“keep住”。不过,如果有多条规则均“keep住”了这个类,在此处只会显示一条keep规则。# 2. “Nothing is keeping xxxx",TestProguardFieldAndMethod没有被keep规则直接“keep住”
复制代码


2、优化配置


  • -dontoptimize。指定后,关闭优化功能;

  • -optimizationpasses。优化次数,理论上优化次数越多,效果越好。一旦某次优化后无任何效果,将停止下一轮优化;

  • -optimizations。配置具体优化项,具体可参考 Proguard 文档。下面是随手找的一个 proguard 处理过程 log,大家感受下优化项:



优化(optimize)项展示


  • 其它。包括-assumenosideeffects、-allowaccessmodification 等,具体可参考文档,不再详述;


3、混淆配置


  • -dontobfuscate。指定后,关闭混淆功能;

  • 其它。包括-applymapping、-obfuscationdictionary、-useuniqueclassmembernames、dontusemixedcaseclassnames 等若干配置项,用于精细化控制混淆处理过程,具体可参考文档。

keep 配置

相对于全局配置,keep 配置大家最熟悉和常用,用来指定需要被保留住的类、变量、方法。被 keep 规则直接命中,进而保留下来的类,称为 seeds(种子)。


在这里,我们可以思考一个问题:如果 apk 构建过程中,没有任何 keep 规则,那么代码会不会全部被裁剪掉?答案是肯定的,最终 apk 中不会有任何代码。可能有同学会说,我用 Android Studio 新建一个 app 工程,开启了 Proguard 但是没有配置任何 keep 规则,为什么最终 apk 中会包含一些代码?这个是由于 Android Gradle Plugin 在构建 apk 过程中,会自动生成一些混淆规则,关于所有 keep 规则的来源问题,在后面的章节会讲到。


好了,继续回到 keep 配置上来。keep 配置支持的规则非常复杂,在这里将其分为以下几类:


1、直接保留类、方法、变量;


  • -keep。被保留类、方法、变量,不允许 shrink(裁剪),不允许 obfuscate(混淆);

  • -keepnames。等效于-keep, allowshrinking。保留类、方法、变量,允许 shrink,如果最终被保留住(其它 keep 规则,或者代码调用),那么不允许 obfuscate;


2、如果类被保留(未裁剪掉),则保留指定的变量、方法;


  • -keepclassmembers。被保留的变量、方法,不允许 shrink(裁剪),不允许 obfuscate(混淆);

  • -keepclassmembernames。等效于-keepclassmembers, allowshrinking。被保留的变量、方法,允许 shrink,如果最终被保留住,那么不允许 obfuscate;


3、如果方法/变量,均满足指定条件,则保留对应类、变量、方法;


  • -keepclasseswithmembers。被保留类、方法、变量,不允许 shrink(裁剪),不允许 obfuscate(混淆);

  • keepclasseswithmembernames。等效于-keepclasseswithmembers, allowshrinking。被保留类、方法、变量,允许 shrink,如果最终被保留住,那么不允许 obfuscate。


完整 keep 规则格式如下,感受下复杂度:


 -keepXXX [,modifier,...] class_specification  # support modifiers: includedescriptorclasses includecode allowshrinking allowoptimization allowobfuscation  # class_specification format: [@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname    [extends|implements [@annotationtype] classname][{    [@annotationtype]    [[!]public|private|protected|static|volatile|transient ...]    <fields> | (fieldtype fieldname [= values]);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> | <init>(argumenttype,...) | classname(argumenttype,...) | (returntype methodname(argumenttype,...) [return values]);}]
# 此外,不同位置均支持不同程度的通配符,不详述.
复制代码


在实际工作中,一般不会用到非常复杂的 keep 规则,所以完整用法不必刻意学习,遇到时能够通过查文档看懂即可。举一个比较有意思的例子,来结束本小节。


===================== 示例 =====================# 示例类:package com.example.myapplication.proguard;public class TestProguardFieldOnly {    public static String fieldA;    public int fieldB;}
package com.example.myapplication.proguard;public class TestProguardMethodOnly { public static void methodA() { Log.d("TestProguardClass", "void methodA"); }}
package com.example.myapplication.proguard;public class TestProguardFieldAndMethod { public int fieldB;
public static void methodA() { Log.d("TestProguardClass", "void methodA"); }}
# keep规则:-keepclasseswithmembers class com.example.myapplication.proguard.** { *;}
# 问题:上述这条keep规则,会导致哪几个示例类被“保留”?# 答案:TestProguardFieldOnly和TestProguardFieldAndMethod
复制代码

辅助文件

这里要讲的辅助文件,是指 progaurd 生成的一些文件,用于了解处理结果,对排查裁剪、混淆相关问题很有帮忙(必要)。



辅助文件

配置项集合

配置项集合,汇总了所有配置信息,并对某些配置进行“展开”。由于配置项可以在多个文件、多个工程中定义(后面会讲到所有来源),因此配置项集合方便我们对此集中查看。


通过配置项-printconfiguration <filepath>打开此项输出,例如-printconfiguration build/outputs/proguard.cfg会生成${application工程根目录}/build/outputs/proguard.cfg文件,示例内容如下:


keep 结果(seeds.txt)

keep 结果,是对 keep 规则直接“保留”类、变量、方法的汇总。注意,被其它保留方法调用,导致间接“保留”的类、变量、方法,不在此结果文件中。


通过配置项-printseeds <filepath>打开此项输出,例如-printseeds build/outputs/mapping/seeds.txt会生成${application工程根目录}/build/outputs/mapping/seeds.txt文件,示例内容如下:


com.example.libraryaar1.proguard.TestProguardConsumerKeep: void methodA()com.example.myapplication.MainActivitycom.example.myapplication.MainActivity: MainActivity()com.example.myapplication.MainActivity: void openContextMenu(android.view.View)com.example.myapplication.R$array: int planets_arraycom.example.myapplication.R$attr: int attr_enum
复制代码

裁剪结果(usage.txt)

裁剪结果,是对被裁剪掉类、变量、方法的汇总。


通过配置项-printusage <filepath>打开此项输出,例如-printusage build/outputs/mapping/usage.txt会生成${application工程根目录}/build/outputs/mapping/usage.txt文件,示例内容如下:


androidx.drawerlayout.R$attrandroidx.vectordrawable.Randroidx.appcompat.app.AppCompatDelegateImpl    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)    public boolean hasWindowFeature(int)    public void setHandleNativeActionModesEnabled(boolean)
复制代码


注意,如果类被完整裁剪,只列出类的全限定名;如果类没有被裁剪,而是类中的变量、方法被裁剪,此处会先列出类名称,再列出被裁剪掉的变量、方法。

混淆结果(mapping.txt)

裁剪结果,是对被混淆类、变量、方法的汇总。


通过配置项-printmapping <filepath>打开此项输出,例如-printmapping build/outputs/mapping/mapping.txt会生成${application工程根目录}/build/outputs/mapping/mapping.txt文件,示例内容如下:


===================== Proguard示例:列出被保留的所有类,以及混淆结果 =====================com.example.myapplication.MyApplication -> com.example.myapplication.MyApplication:    void <init>() -> <init>com.example.myapplication.proguard.TestProguardAndroidKeep -> com.example.myapplication.proguard.TestProguardAndroidKeep:    int filedA -> filedA    void <init>() -> <init>    void methodA() -> methodA    void methodAnnotation() -> methodAnnotationcom.example.myapplication.proguard.TestProguardAnnotation -> com.example.myapplication.proguard.TestProguardAnnotation:com.example.myapplication.proguard.TestProguardFieldAndMethod -> com.example.myapplication.proguard.a:    void methodA() -> acom.example.myapplication.proguard.TestProguardInterface -> com.example.myapplication.proguard.TestProguardInterface:    void methodA() -> methodAcom.example.myapplication.proguard.TestProguardMethodOnly -> com.example.myapplication.proguard.TestProguardMethodOnly:    void <init>() -> <init>    void methodAnnotation() -> methodAnnotation
===================== R8示例:仅列出被保留,且被混淆的类、变量、方法 =====================# compiler: R8# compiler_version: 1.4.94# min_api: 21com.example.libraryaar1.LibraryAarClassOne -> a.a.a.a: void test() -> acom.example.libraryaar1.R$layout -> a.a.a.b:com.example.libraryaar1.R$styleable -> a.a.a.c:com.example.myapplication.proguard.TestProguardFieldAndMethod -> a.a.b.a.a: void methodA() -> a
复制代码


Proguard 和 R8 的输出内容,以及格式,有一些差异。在实际解读时,需要注意。

工程应用

在对 proguard 基础知识,具备一个整体“框架性”认知后,接下来看看在实际工程中,为了更好的使用 proguard,需要了解到的一些事项。本节不会讲述最基础的使用方式,这些可以在官方文档和各类文章中很容易找到。

工具选择

首先,看看有哪些工具可以选择。对于 Android 开发领域,有 Proguard 和 R8 两个工具可供选择(很久以前还有一个 AGP - Android Gradle Plugin 内置的代码裁剪工具,完全过时,不再列出),其中后者是 google 官方自研的 Proguard 工具替代者,在裁剪和优化的处理耗时,以及处理效果上,都比 Proguard 工具要好。二者的一些对比如下:



虽然 R8 不提供全局性的处理过程控制选项,但是提供了两种模式:


  • 正常模式。optimize(优化)策略与 Proguard 尽可能保持最大程度的兼容性,一般 app 可以较平滑的从 Proguard 切换到 R8 正常模式;

  • 完整模式。在优化策略上,采用了更激进的方案,因此相对于 Proguard,可能需要额外的 keep 规则来保障代码可用性。开启方式为在 gradle.properties 文件中,增加配置:android.enableR8.fullMode=true。


在可用性上,R8 已经达到比较成熟的状态,建议还在使用 proguard 的 app,尽快将切换 R8 计划提上日程。不过,需要注意的是,即使是正常模式,R8 的优化策略与 progaurd 还是存在一定差异,因此,需要进行全面的回归验证来提供质量保障。

自定义配置

前面讲了很多关于配置项的内容,在具体的工程中,如何增加自定义配置规则呢?大部分同学应该都会觉得,这个问题简单的不能再简单,那我们换一个问题,最终参与到处理过程的配置,都来自于哪里?



AAPT 生成的混淆规则,来看几个示例,有助于大家了解哪些 keep 规则已经被自动添加进来,无须手动处理:


# Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/build/intermediates/merged_manifests/fullRelease/AndroidManifest.xml:28-keep class com.example.myapplication.MainActivity { <init>(); }# Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/build/intermediates/merged_manifests/fullRelease/AndroidManifest.xml:21-keep class com.example.myapplication.MyApplication { <init>(); }# Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/library-aar-1/build/intermediates/packaged_res/release/layout/layout_use_declare_styleable1.xml:7-keep class com.example.libraryaar1.CustomImageView { <init>(...); }
# Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/res/layout/activity_main.xml:9-keepclassmembers class * { *** onMainTextViewClicked(android.view.View); }
复制代码


可以看到 layout 中 onClick 属性值对应的函数名称,无法被混淆,同时会生成一条容易导致过度 keep 的规则,因此在实际代码中,不建议这种使用方式。


对于子工程/外部模块中携带的配置,需要特别注意,如果不谨慎处理,会带来意想不到的结果。

治理实践

前面两章,对 proguard 的基础知识,以及工程应用,进行了相关讲解,相信大家已经对 proguard 形成了初步的整体认知。由于配置项来源广泛,尤其是 consumerProguard 机制的存在,导致依赖的外部模块中可能携带“问题”配置项,这让配置项难以整体管控。此外,keep 配置与目标代码分离,代码删除后,keep 配置非常容易被保留下来。在工程实践中,随着 app 不断迭代,会遇到以下两类问题:


  • 全局性配置,被非预期修改。是否混淆、是否裁剪、优化次数、优化类型等一旦被修改,会导致代码发生较大变化,影响稳定性、包大小、性能;

  • keep 配置,不断增加,逐渐腐化。keep 规则数量,与构建过程中 proguard 耗时,成非线性正比(去除无用/冗余 keep 规则,可以提高构建速度)。过于广泛的 keep 规则,会导致包大小增加,以及代码无法被优化,进而影响运行时性能。


“工欲善其事,必先利其器”,在实际入手治理前,分别进行了检测工具的开发。基于工具提供的检测结果,分别开展治理工作。(本文涉及工具,均属于优酷自研「onepiece 检测分析套件」的一部分)

全局配置

全局配置检测能力(工具),提供 proguard 全局性配置检测能力,并基于白名单机制,对目标配置项的值,与白名单不一致情况,及时感知。同时,提供选项,当全局性配置发生非预期变化时,终止构建过程,并给出提示。


当存在与白名单不一致的全局配置时,生成的检测结果文件中,会列出不一致的配置项,示例内容如下:


* useUniqueClassMemberNames|-- [whitelist] true|-- [current] false
* keepAttributes|-- [whitelist] [Deprecated, Signature, Exceptions, EnclosingMethod, InnerClasses, SourceFile, *Annotation*, LineNumberTable]|-- [current] [Deprecated, Signature, Exceptions, EnclosingMethod, InnerClasses, SourceFile, AnnotationDefault, *Annotation*, LineNumberTable, RuntimeVisible*Annotations]
复制代码


通过这个检测能力,实现了对关键全局性配置的保护,从而有效避免非预期变化发生(当然,坑都是踩过的,不止一次...)。

keep 配置

keep 配置的治理,则要困难很多。以对最终 apk 影响来看,keep 配置可以划分为以下四类:


  • 无用规则。对最终处理结果,完全没有任何影响。换句话讲,如果一条 keep 规则,不与任何 class 匹配,那么这条规则就是无用规则;

  • 冗余规则。一条规则的 keep 效果,完全可以被已有的其它一条或多条规则所包含。这会导致不必要的配置解析,以及处理过程耗时增加(每一条 keep 规则,都会拿来与所有 class 进行匹配);

  • 过度规则。超越必要的 keep 范围,将不必要类、变量、方法进行了保留。在这里,也包括本来只需要 keepnames,但是却直接 keep 的情况;

  • 精准规则。遵循最小保留原则的必要规则。无需处理,但是需要注意的是,app 中的自研业务代码,尽量使用 support 或 androidX 中提供的 @keep 注解,做到 keep 规则与代码放在一起。


上述前三类规则,都属于治理目标,现从分析、处理、验证三个维度,来比较这三类规则的难度。



keep 规则治理难度对比


1、分析


  • 无用。通过将每条 keep 规则,与每个 class 进行匹配,即可确定是否对此 class 有“影响”。这个匹配的难度,主要来自于 keep 规则的复杂度,以及与 proguard 的匹配结果保持一致;

  • 冗余。如果是一条规则,效果完全被其它规则所“包含”,这种可以先计算每条 keep 规则对每个 class 的影响,最后再找出“保留”范围相同,或具有“包含”关系,理论上可以实现。但是对于一条规则,被另外多条规则“包含”时,检测复杂度会变得很高;

  • 过度。这个基本无法精准检测,因为哪些类、变量、方法应该被保留,本来就需要通过“运行时被如何使用”进行判断。如果过度规则可以被检测,那么所有 keep 规则理论上也无需手动添加;


2、处理


  • 无用。直接删除即可;

  • 冗余。删除其中一条或多条规则,或者合并几条规则;

  • 过度。增加限定词、改写规则等。需要对预期效果有清晰的认识,以及 keep 规则的熟练掌握;


3、验证


  • 无用。对最终裁剪、混淆结果,无任何影响。验证辅助文件中的「裁剪结果」、「混淆结果」即可,为了进一步确认影响,也可以对比验证 apk 本身;

  • 冗余。和无用规则一样,都是对处理结果无影响,验证方式也一致;

  • 过度。对最终裁剪、优化、混淆结果,都有影响。需要通过功能回归的方式进行验证。


在工具开发上,实现了一个辅助定位功能,以及三个检测能力:


1、【辅助】模块包含 keep 规则列表。每个模块包含的 keep 规则,方便查看每一条 keep 规则的来源。


project:app:1.0|-- -keepclasseswithmembers class com.example.myapplication.proguard.** { * ; }|-- -keepclassmembers class com.example.myapplication.proguard.** { * ; }|-- -keep class com.example.libraryaar1.CustomImageView { <init> ( ... ) ; }|-- -keep class com.example.myapplication.proguard.**|-- -keepclasseswithmembers class * { @android.support.annotation.Keep <init> ( ... ) ; }
project:library-aar-1:1.0|-- -keep interface * { <methods> ; }
复制代码


2、【检测】keep 规则命中类检测。每个 keep 规则,命中哪些类,以及这些类所属模块。


* [1] -keep class com.youku.android.widget.TextSetView { <init> ( ... ) ; }    // 这是keep规则,[x]中的数字,表示keep规则命中模块的数量|-- [1] com.youku.android:moduleOne:1.21.407.6     // 这是keep命中模块,[x]中的数字,表示模块中被命中类的数量|   |-- com.youku.android.widget.TextSetView    // 这是模块中,被命中的类
* [2] -keep public class com.youku.android.vo.** { * ; }|-- [32] com.youku.android:ModuleTwo:1.2.1.55| |-- com.youku.android.vo.MessageSwitchState$xxx| |-- com.youku.android.vo.MessageCenterNewItem$xxxx......|-- [14] com.youku.android:ModuleThree:1.0.6.47| |-- com.youku.android.vo.MCEntity| |-- com.youku.android.vo.NUMessage| |-- com.youku.android.vo.RPBean$xxxx......
复制代码


3、【检测】类被 keep 规则命中检测。每个 class(以及所属模块),被哪些 keep 规则命中。相对于-whyareyoukeeping,本检测聚焦类被哪些 keep 规则直接“影响”。


* com.youku.arch:ModuleOne:2.8.15   // 这个是模块maven坐标|-- com.youku.arch.SMBridge    // 这个是类名称,以下为命中此类的keep规则列表|   |-- -keepclasseswithmembers , includedescriptorclasses class * { native <methods> ; }|   |-- -keepclasseswithmembernames class * { native <methods> ; }|   |-- -keepclasseswithmembers class * { native <methods> ; }|   |-- -keepclassmembers class * { native <methods> ; }|-- com.youku.arch.CFixer|   |-- -keepclasseswithmembers , includedescriptorclasses class * { native <methods> ; }|   |-- -keepclasseswithmembernames class * { native <methods> ; }|   |-- -keepclasseswithmembers class * { native <methods> ; }|   |-- -keepclassmembers class * { native <methods> ; }
复制代码


4、【检测】无用 keep 规则检测。哪些 keep 规则未命中任何类。


* -keep class com.youku.android.NoScrollViewPager { <init> ( ... ) ; }* -keep class com.youku.android.view.LFPlayerView { <init> ( ... ) ; }* -keep class com.youku.android.view.LFViewContainer { <init> ( ... ) ; }* -keep class com.youku.android.view.PLayout { <init> ( ... ) ; }* [ignored] -keep class com.youku.android.view.HAListView { <init> ( ... ) ; }* -keep class com.youku.android.CMLinearLayout { <init> ( ... ) ; }* [ignored] -keepclassmembers class * { *** onViewClick ( android.view.View ) ; }  // 当某条keep规则位于ignoreKeeps配置中时,会加上[ignored]标签
复制代码


此外,还提供了「裁剪结果」、「混淆结果」的对比分析工具,便于对无用/冗余 keep 规则的清理结果,进行验证。


===================== 裁剪结果对比 =====================*- [add] android.support.annotation.VisibleForTestingNew*- [delete] com.youku.arch.nami.tasks.refscan.RefEdge*- [delete] com.example.myapplication.R$style*- [modify] com.youku.arch.nami.utils.elf.Flags|   *- [add] private void testNew()|   *- [delete] public static final int EF_SH4AL_DSP|   *- [delete] public static final int EF_SH_DSP
===================== 混淆结果对比 =====================*- [add] com.cmic.sso.sdk.d.q| *- [add] a(com.cmic.sso.sdk.d.q$a) -> a| *- [add] <clinit>() -> <clinit>*- [delete] com.youku.graphbiz.GraphSearchContentViewDelegate| *- [delete] mSearchUrl -> h| *- [delete] <init>() -> <init>*- [modify] com.youku.alixplayermanager.RemoveIdRecorderListener ([new]com.youku.a.f : [old]com.youku.b.f)*- [modify] com.youku.saosao.activity.CaptureActivity ([new/old]com.youku.saosao.activity.CaptureActivity)| *- [modify] hasActionBar() ([new]f : [old]h)| *- [modify] showPermissionDenied() ([new]h : [old]f)*- [modify] com.youku.arch.solid.Solid ([new/old]com.youku.arch.solid.e)| *- [add] downloadSo(java.util.Collection,boolean) -> a| *- [delete] buildZipDownloadItem(boolean,com.youku.arch.solid.ZipDownloadItem) -> a
复制代码


优酷主客,治理基线版本,共有 3812 条 keep 规则,通过分析工具,发现其中 758 条(20%)未命中任何类,属于无用规则。对其中 700 条进行了清理,并通过对比「裁剪结果」和「混淆结果」,确保对最终 apk 无影响。剩余大部分来自于 AAPT 编译资源时,自动产生的规则,但是资源中引用到的类在 apk 中不存在,由此导致 keep 规则无用。想要清理这些规则,需要删除资源中对这些不存在类的引用,暂时先加到白名单。


# layout中引用不存在的class,在apk编译过程中,并不会引发构建失败,但依然会生成相对应的keep规则。# 这个layout一旦在运行时被“加载“,那么会引发Java类找不到的异常。
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent">
<com.example.myapplication.NonExistView android:id="@+id/main_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"/>
</LinearLayout>
# 生成的keep规则为:-keep class com.example.myapplication.NonExistView { <init> ( ... ) ; }
复制代码


对于冗余规则和过度规则,初步进行了小批量试清理,复杂度较高,同时风险难以掌控,先不进行批量清理,后续逐步清理掉。



keep 规则分布 &清理结果


至此,优酷的完整 release 包构建中 progurad 处理耗时减少了 18%。接下来,一方面在 application 工程实行中心化管控(优酷禁用了外部模块的 consumerProguard),按团队隔离配置文件,并制定 keep 规则准入机制;另一方面,将无用 keep 配置作为一个卡口项,在版本迭代过程中部署,进入常态化治理阶段。

治理全景

最后,对 proguard 腐化治理,给出一份全景图:



Proguard 治理全景

还能做些什么

工程腐化的其他细分战场,还在进行。对于 proguard 治理,后续一方面在工具的检测能力上,会针对「冗余 keep 规则」以及「过度 keep 规则」,进行一些探索;另一方面,对存量 keep 规则的清理,也并非一蹴而就,任重而道远,与诸君共勉。


【参考文档】


  • Proguard 官方文档:https://www.guardsquare.com/manual/configuration/usage

  • R8 官方文档:https://developer.android.com/studio/build/shrink-code

  • consumerProguardFiles 官方文档:https://developer.android.com/studio/projects/android-library.html#Considerations


关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践 &干货给你思考!

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2018.07.07 加入

阿里巴巴移动&终端技术官方账号。

评论

发布
暂无评论
向工程腐化开炮 | proguard治理