作者:劉天宇(謙風)
工程腐化是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 篇移動技術實踐&乾貨給你思考!