作者:刘天宇(谦风)
工程腐化是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处理过程如下:
其中,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: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,大家感受下优化项:
- 其它。包括-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 ...]
| (fieldtype fieldname [= values]);
[@annotationtype]
[[!]public|private|protected|static|synchronized|native|abstract|strictfp ...]
| (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
打开此项输出,例如-printconfiguration build/outputs/proguard.cfg
会生成${application工程根目录}/build/outputs/proguard.cfg
文件,示例内容如下:
keep结果(seeds.txt)
keep结果,是对keep规则直接“保留”类、变量、方法的汇总。注意,被其它保留方法调用,导致间接“保留”的类、变量、方法,不在此结果文件中。
通过配置项-printseeds
打开此项输出,例如-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
打开此项输出,例如-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
打开此项输出,例如-printmapping build/outputs/mapping/mapping.txt
会生成${application工程根目录}/build/outputs/mapping/mapping.txt
文件,示例内容如下:
===================== Proguard示例:列出被保留的所有类,以及混淆结果 =====================
com.example.myapplication.MyApplication -> com.example.myapplication.MyApplication:
void () ->
com.example.myapplication.proguard.TestProguardAndroidKeep -> com.example.myapplication.proguard.TestProguardAndroidKeep:
int filedA -> filedA
void () ->
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 () ->
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 { (); }
# Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/build/intermediates/merged_manifests/fullRelease/AndroidManifest.xml:21
-keep class com.example.myapplication.MyApplication { (); }
# 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 { (...); }
# 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规则与代码放在一起。
上述前三类规则,都属于治理目标,现从分析、处理、验证三个维度,来比较这三类规则的难度。
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 { ( ... ) ; }
|-- -keep class com.example.myapplication.proguard.**
|-- -keepclasseswithmembers class * { @android.support.annotation.Keep ( ... ) ; }
project:library-aar-1:1.0
|-- -keep interface * { ; }
2、【检测】keep规则命中类检测。每个keep规则,命中哪些类,以及这些类所属模块。
* [1] -keep class com.youku.android.widget.TextSetView { ( ... ) ; } // 这是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 ; }
| |-- -keepclasseswithmembernames class * { native ; }
| |-- -keepclasseswithmembers class * { native ; }
| |-- -keepclassmembers class * { native ; }
|-- com.youku.arch.CFixer
| |-- -keepclasseswithmembers , includedescriptorclasses class * { native ; }
| |-- -keepclasseswithmembernames class * { native ; }
| |-- -keepclasseswithmembers class * { native ; }
| |-- -keepclassmembers class * { native ; }
4、【检测】无用keep规则检测。哪些keep规则未命中任何类。
* -keep class com.youku.android.NoScrollViewPager { ( ... ) ; }
* -keep class com.youku.android.view.LFPlayerView { ( ... ) ; }
* -keep class com.youku.android.view.LFViewContainer { ( ... ) ; }
* -keep class com.youku.android.view.PLayout { ( ... ) ; }
* [ignored] -keep class com.youku.android.view.HAListView { ( ... ) ; }
* -keep class com.youku.android.CMLinearLayout { ( ... ) ; }
* [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] () ->
*- [delete] com.youku.graphbiz.GraphSearchContentViewDelegate
| *- [delete] mSearchUrl -> h
| *- [delete] () ->
*- [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类找不到的异常。
# 生成的keep规则为:-keep class com.example.myapplication.NonExistView { ( ... ) ; }
对于冗余规则和过度规则,初步进行了小批量试清理,复杂度较高,同时风险难以掌控,先不进行批量清理,后续逐步清理掉。
至此,优酷的完整release包构建中progurad处理耗时减少了18%。接下来,一方面在application工程实行中心化管控(优酷禁用了外部模块的consumerProguard),按团队隔离配置文件,并制定keep规则准入机制;另一方面,将无用keep配置作为一个卡口项,在版本迭代过程中部署,进入常态化治理阶段。
治理全景
最后,对proguard腐化治理,给出一份全景图:
还能做些什么
工程腐化的其他细分战场,还在进行。对于proguard治理,后续一方面在工具的检测能力上,会针对「冗余keep规则」以及「过度keep规则」,进行一些探索;另一方面,对存量keep规则的清理,也并非一蹴而就,任重而道远,与诸君共勉。
【参考文档】
- Proguard官方文档:https://www.guardsquare.com/m...
- R8官方文档:https://developer.android.com...
- consumerProguardFiles官方文档:https://developer.android.com...
关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践&干货给你思考!