2
头图

作者:刘天宇(谦风)

工程腐化是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规则的清理,也并非一蹴而就,任重而道远,与诸君共勉。

【参考文档】

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


阿里巴巴终端技术
336 声望1.3k 粉丝

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