天天看點

向工程腐化開炮 | proguard治理

作者:阿裡移動技術
向工程腐化開炮 | 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治理

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全局性配置

全局性配置,是指影響整體處理過程的一些配置項,一般又可以分為以下幾類:

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,大家感受下優化項:
向工程腐化開炮 | proguard治理

優化(optimize)項展示

    • 其它。包括-assumenosideeffects、-allowaccessmodification等,具體可參考文檔,不再詳述;

3、混淆配置

    • -dontobfuscate。指定後,關閉混淆功能;
    • 其它。包括-applymapping、-obfuscationdictionary、-useuniqueclassmembernames、dontusemixedcaseclassnames等若幹配置項,用于精細化控制混淆處理過程,具體可參考文檔。

2 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生成的一些檔案,用于了解處理結果,對排查裁剪、混淆相關問題很有幫忙(必要)。

向工程腐化開炮 | proguard治理

1 配置項集合

配置項集合,彙總了所有配置資訊,并對某些配置進行“展開”。由于配置項可以在多個檔案、多個工程中定義(後面會講到所有來源),是以配置項集合友善我們對此集中檢視。

通過配置項-printconfiguration <filepath>打開此項輸出,例如-printconfiguration build/outputs/proguard.cfg會生成${application工程根目錄}/build/outputs/proguard.cfg檔案,示例内容如下:

向工程腐化開炮 | proguard治理

2 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           

3 裁剪結果(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)           

注意,如果類被完整裁剪,隻列出類的全限定名;如果類沒有被裁剪,而是類中的變量、方法被裁剪,此處會先列出類名稱,再列出被裁剪掉的變量、方法。

4 混淆結果(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工具要好。二者的一些對比如下:

向工程腐化開炮 | proguard治理

雖然R8不提供全局性的處理過程控制選項,但是提供了兩種模式:

  • 正常模式。optimize(優化)政策與Proguard盡可能保持最大程度的相容性,一般app可以較平滑的從Proguard切換到R8正常模式;
  • 完整模式。在優化政策上,采用了更激進的方案,是以相對于Proguard,可能需要額外的keep規則來保障代碼可用性。開啟方式為在gradle.properties檔案中,增加配置:android.enableR8.fullMode=true。

在可用性上,R8已經達到比較成熟的狀态,建議還在使用proguard的app,盡快将切換R8計劃提上日程。不過,需要注意的是,即使是正常模式,R8的優化政策與progaurd還是存在一定差異,是以,需要進行全面的回歸驗證來提供品質保障。

自定義配置

前面講了很多關于配置項的内容,在具體的工程中,如何增加自定義配置規則呢?大部分同學應該都會覺得,這個問題簡單的不能再簡單,那我們換一個問題,最終參與到處理過程的配置,都來自于哪裡?

向工程腐化開炮 | proguard治理

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規則與代碼放在一起。

上述前三類規則,都屬于治理目标,現從分析、處理、驗證三個次元,來比較這三類規則的難度。

向工程腐化開炮 | proguard治理

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> ( ... ) ; }           

對于備援規則和過度規則,初步進行了小批量試清理,複雜度較高,同時風險難以掌控,先不進行批量清理,後續逐漸清理掉。

向工程腐化開炮 | proguard治理

keep規則分布&清理結果

至此,優酷的完整release包建構中progurad處理耗時減少了18%。接下來,一方面在application工程實行中心化管控(優酷禁用了外部子產品的consumerProguard),按團隊隔離配置檔案,并制定keep規則準入機制;另一方面,将無用keep配置作為一個卡口項,在版本疊代過程中部署,進入常态化治理階段。

治理全景

最後,對proguard腐化治理,給出一份全景圖:

向工程腐化開炮 | proguard治理

Proguard治理全景

還能做些什麼

工程腐化的其他細分戰場,還在進行。對于proguard治理,後續一方面在工具的檢測能力上,會針對「備援keep規則」以及「過度keep規則」,進行一些探索;另一方面,對存量keep規則的清理,也并非一蹴而就,任重而道遠,與諸君共勉。

【參考文檔】

  • Proguard官方文檔:https://www.guardsquare.com/manual/configuration/usage
  • R8官方文檔:https://developer.android.com/studio/build/shrink-code
  • consumerProguardFiles官方文檔:https://developer.android.com/studio/projects/android-library.html#Considerations

關注【阿裡巴巴移動技術】微信公衆号,每周 3 篇移動技術實踐&幹貨給你思考!