乐趣区

关于app:向工程腐化开炮-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: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 篇挪动技术实际 & 干货给你思考!

退出移动版