作者:刘天宇(谦风)
工程腐化是 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: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)项展现
- 其它。包含 -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.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 一直迭代,会遇到以下两类问题:
- 全局性配置,被非预期批改。是否混同、是否裁剪、优化次数、优化类型等一旦被批改,会导致代码产生较大变动,影响稳定性、包大小、性能;
- 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/m…
- R8 官网文档:https://developer.android.com…
- consumerProguardFiles 官网文档:https://developer.android.com…
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!