7

向工程腐化开炮 | proguard治理

 2 years ago
source link: https://my.oschina.net/alimobile/blog/5438485
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

向工程腐化开炮 | proguard治理 - 阿里巴巴移动技术 - OSCHINA - 中文开源技术交流社区

作者:刘天宇(谦风)

工程腐化是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规则治理难度对比

  • 无用。通过将每条keep规则,与每个class进行匹配,即可确定是否对此class有“影响”。这个匹配的难度,主要来自于keep规则的复杂度,以及与proguard的匹配结果保持一致;
  • 冗余。如果是一条规则,效果完全被其它规则所“包含”,这种可以先计算每条keep规则对每个class的影响,最后再找出“保留”范围相同,或具有“包含”关系,理论上可以实现。但是对于一条规则,被另外多条规则“包含”时,检测复杂度会变得很高;
  • 过度。这个基本无法精准检测,因为哪些类、变量、方法应该被保留,本来就需要通过“运行时被如何使用”进行判断。如果过度规则可以被检测,那么所有keep规则理论上也无需手动添加;
  • 无用。直接删除即可;
  • 冗余。删除其中一条或多条规则,或者合并几条规则;
  • 过度。增加限定词、改写规则等。需要对预期效果有清晰的认识,以及keep规则的熟练掌握;
  • 无用。对最终裁剪、混淆结果,无任何影响。验证辅助文件中的「裁剪结果」、「混淆结果」即可,为了进一步确认影响,也可以对比验证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 篇移动技术实践&干货给你思考!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK