作者:刘天宇(谦风)
工程腐化是 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、裁剪配置
# 示例:类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:1
Nothing 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)项展示
3、混淆配置
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.MainActivity
com.example.myapplication.MainActivity: MainActivity()
com.example.myapplication.MainActivity: void openContextMenu(android.view.View)
com.example.myapplication.R$array: int planets_array
com.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$attr
androidx.vectordrawable.R
androidx.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() -> methodAnnotation
com.example.myapplication.proguard.TestProguardAnnotation -> com.example.myapplication.proguard.TestProguardAnnotation:
com.example.myapplication.proguard.TestProguardFieldAndMethod -> com.example.myapplication.proguard.a:
void methodA() -> a
com.example.myapplication.proguard.TestProguardInterface -> com.example.myapplication.proguard.TestProguardInterface:
void methodA() -> methodA
com.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: 21
com.example.libraryaar1.LibraryAarClassOne -> a.a.a.a:
void test() -> a
com.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 不断迭代,会遇到以下两类问题:
“工欲善其事,必先利其器”,在实际入手治理前,分别进行了检测工具的开发。基于工具提供的检测结果,分别开展治理工作。(本文涉及工具,均属于优酷自研「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、处理
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 篇移动技术实践 &干货给你思考!
评论