文章目錄
初識ProGuard
Android開發的小夥伴們都或多或少的接觸過混淆,很多人都對混淆很困惑。需要發版的時候,從網上load一份混淆文件,或從其他項目中拷貝一份過來,修改一下,管用就不去管了,有問題就卡住了,各種baidu也不一定能解決問題。本文力求讓大家對混淆規則輕車熟路,能快速的上手。知其然也能知其所以然。
從Android Studio2.3開始,已經集成了ProGuard。ProGuard是一款Java類文件的混淆器,集成了壓縮器,優化器,混淆器和預驗證器。 與其他Java混淆器相比,ProGuard的主要優勢可能是其緊湊的基於模板的配置。通常只需幾個直觀的命令行選項或一個簡單的配置文件即可。ProGuard減少了處理後的代碼的大小,並帶來了一些潛在的效率提高。處理幾兆字節的程序和庫只需要幾秒鐘。
ProGuard的典型用途是:
創建更緊湊的代碼,以實現更小的代碼歸檔,更快的網絡傳輸,更快的加載和更小的內存佔用。
使程序和庫更難以逆向工程。
列出無效代碼,可以將其從源代碼中刪除。
重新定位和預先驗證Java 6的現有類文件,以充分利用Java 6更快的類加載速度。
在gradle中配置ProGuard開啓混淆很簡單:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
minifyEnabled 屬性設爲 true即開啓混淆。
proguardFiles 屬性指定了混淆文件的所在目錄。proguard-android.txt爲sdk路徑下默認的混淆文件,後面的’proguard-rules.pro’就是我們自定義的混淆文件。
ProGuard處理代碼的流程如下:
Shrunk壓縮器可檢測並刪除未使用的類,字段,方法和屬性。 Optimize優化器分析並優化了方法的字節碼。 Obfuscate混淆器使用簡短的無意義名稱重命名其餘的類,字段和方法。 這些步驟使代碼庫更小,更有效,並且更難以逆向工程。 最後的Preverify預驗證器將預驗證信息添加到類中,這對於Java Micro Edition是必需的,可以縮短Java 6的啓動時間。
ProGuard詳解
什麼在壓縮?
Java源代碼(.java文件)通常被編譯爲字節碼(.class文件)。字節碼比Java源代碼更緊湊,但是字節碼仍可能包含許多未使用的代碼,尤其是在包含程序庫的情況下。壓縮程序(例如ProGuard)可以分析字節碼並刪除未使用的類,字段和方法。該程序在功能上保持等效,包括異常堆棧跟蹤中給出的信息。
什麼是混淆?
默認情況下,已編譯的字節碼仍包含許多調試信息:源文件名,行號,字段名,方法名,參數名,變量名等。此信息使直接編譯字節碼和對整個程序進行反向工程變得很簡單。有時,這是不可取的。諸如ProGuard之類的混淆器可以刪除調試信息,並以無意義的字符序列替換所有名稱,這使得對代碼進行反向工程變得更加困難。同時它進一步壓縮了代碼。該程序在功能上保持等效,除了在異常堆棧跟蹤中給出的類名,方法名和行號。
反射
反射給代碼的自動處理帶來了特殊的問題。 在ProGuard中,必須將代碼中動態創建或調用的類或類成員指定爲入口點, 用keep選項保護起來。 例如,Class.forName()構造可以在運行時引用任何類。 通常無法預見必須保留哪些類(及其原始名稱),比如可以從配置文件中讀取類名稱。 因此,必須在ProGuard配置中使用相同的簡單-keep選項來指定它們。
此外,如果需要保留某些類或類成員,ProGuard將提供一些建議。 例如,ProGuard將注意類似“(SomeClass)Class.forName(variable).newInstance()”的結構。 這些可能表明該類或接口SomeClass或它的實現可能需要保留。 然後,我們可以相應地調整混淆配置。
混淆選項
一份混淆文件主要有一系列的keep選項及非keep選項構成。keep選項用來告訴ProGuard哪些類、類成員不被混淆;非keep選項包括輸入、壓縮、優化、 混淆、常規等選項,用來告訴ProGuard額外的配置。
非keep選項
-
輸入選項
-skipnonpubliclibraryclasses
指定在讀取庫jar時跳過非公共類,以加快處理速度並減少ProGuard的內存使用量。默認情況下,ProGuard會讀取非公共和公共庫類。但是,非公用類通常不相關,只要它們不影響輸入jar中的實際程序代碼即可。然後忽略它們可以加快ProGuard的速度,而不會影響輸出。不幸的是,某些庫,包括最近的JSE運行時庫,都包含由公共庫類擴展的非公共庫類。如果由於設置了此選項而無法找到類,則ProGuard將打印警告。-dontskipnonpubliclibraryclasses
指定不忽略非公共庫類。從4.5版開始爲默認設置。-dontskipnonpubliclibraryclassmembers
指定不忽略包可見的庫類成員(字段和方法)。默認情況下,ProGuard在解析庫類時會跳過這些類成員,因爲程序類通常不會引用它們。但是,有時程序類與庫類位於同一包中,並且它們確實引用其包可見的類成員。在這種情況下,實際讀取類成員可能很有用,以確保處理後的代碼保持一致。 -
壓縮選項
默認開啓壓縮; 除各種-keep選項列出的類以及它們直接或間接依賴的類之外,所有類和類成員 都將被刪除。 在每個優化(optimization )步驟之後,還會執行壓縮步驟,因爲優化後可能會再次暴露一些未被使用的類和成員。
關閉壓縮:-dontshrink -
優化選項
默認開啓優化。所有方法都在字節碼級別進行了優化。但某些時候,優化可能導致程序執行異常,它可能會改變程序原有的邏輯。比如刪除了某些特殊的註釋,刪除了它認爲無意義的空loop。
關閉優化:-dontoptimize-optimizationpasss n
指定要執行的優化遍數。默認情況下,執行一次通過。多次通過可能會有進一步的改進。如果 在優化通過後未發現任何改進,則優化結束。僅在優化時適用。-allowaccessmodification
指定在處理過程中可以擴大類和類成員的訪問修飾符。這樣可以改善優化步驟的結果。 -
混淆選項
默認開啓混淆。除了各種-keep選項列出的名稱外,類和類成員會收到新的簡短隨機名稱。刪除了對調試有用的內部屬性,例如源文件名,變量名和行號。
關閉混淆:-dontobfuscate-printmapping [文件名]
指定爲已重命名的類和類成員打印從舊名稱到新名稱的映射。映射將打印到標準輸出或給定文件。-useuniqueclassmembernames
該選項將爲需要混淆的類生成唯一的混淆名稱。如果沒有該選項,則將更多的類成員映射到相同的短名稱,如“ a”,“ b”等。-dontusemixedcaseclassnames
指定在混淆時不生成大小寫混合的類名,即全部小寫。 默認情況下,混淆的類名可以包含大寫字符和小寫字符的混合。-keeppackagenames [package_filter]
指定不混淆指定的包名稱。 可選的過濾器是包名稱的逗號分隔列表。包名稱可以包含?,*和**通配符,或在其前面加上!。
主工程不同的庫工程時,不同的庫工程混淆後的類名可能衝突,比如都是a.a.a.a。當主工程引用混淆後的庫aar時就會編譯出錯:
#Duplicate class a.a.a.a found in modules classes.jar (:libA-release:) and classes.jar (:libB-release:)
這時可以keeppackagenames指定一個庫的包名稱不混淆來避免此問題。
-keepattributes [attribute_filter ]
指定要保留的可選屬性。可以使用一個或多個-keepattributes指令指定屬性。可選過濾器是用逗號分隔的屬性名稱列表。屬性名稱可以包含?,*和**通配符,或在其前面加上!。
典型的可選屬性包括:
Exceptions,Signature,InnerClasses,Deprecated,SourceFile,SourceDir,LineNumberTable,LocalVariableTable,LocalVariableTypeTable,Synthetic,EnclosingMethod,RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations和AnnotationDefault。
例如,在處理庫時,至少應保留Exceptions,InnerClasses和Signature屬性。還應該保留SourceFile和LineNumberTable屬性,以產生有用的混淆堆棧跟蹤。最後,如果您的代碼依賴註釋,則可能需要保留註釋。
示例:
-keepattributes Exceptions,InnerClasses,Signature #保留內部接口或內部類、內部類、泛型簽名類型
-renamesourcefileattribute SourceFile #將崩潰日誌文件來源重命名爲“SourceFile”
-keepattributes SourceFile,LineNumberTable #產生有用的混淆堆棧跟蹤
-keepattributes *Annotation* #保留註釋
-
常規選項
-verbose
指定在處理期間輸出更多信息。如果程序因異常終止,則此選項將打印出整個堆棧跟蹤,而不僅僅是異常消息。-dontnote [class_filter]
指定不打印有關配置中潛在錯誤或遺漏的註釋,例如類名中的錯字或缺少可能有用的選項。可選過濾器class_filter是一個正則表達式; ProGuard不會打印與可選名稱匹配的類的註釋。-dontwarn [class_filter]
指定不警告尚未解決的引用和其他重要問題。可選過濾器class_filter是一個正則表達式; ProGuard不會打印與可選名稱匹配的類的警告。忽視警告可能很危險。例如,如果確實需要對未解析的類或類成員進行處理,則處理後的代碼將無法正常運行。僅當知道自己在做什麼時才使用此選項!-ignorewarnings
指定打印有關未解決引用和其他重要問題的任何警告,但在任何情況下都將繼續處理。忽視警告可能很危險。例如,如果確實需要對未解析的類或類成員進行處理,則處理後的代碼將無法正常運行。僅當知道自己在做什麼時才使用此選項!文件過濾器
文件過濾器是逗號分隔的文件名列表,可以包含通配符。 支持以下通配符:
? 匹配名稱中的任何單個字符。
* 匹配名稱的不包含包分隔符“.”或目錄分隔符"/"的任何部分。
** 匹配名稱的任何部分,可能包含任意數量的包分隔符或目錄分隔符。
例如,“java / **.class,javax / **.class” 匹配java和javax中的所有類文件。“ foo,*bar”匹配名稱foo和所有以bar結尾的名稱。
此外,名稱前可以帶有一個負號“!”。從匹配的文件名中排除該文件名。例如,
"!**.gif,images/** " # 匹配images目錄中的所有文件,gif文件除外。
"!foobar,*bar" #匹配所有以bar結尾的名稱,但foobar除外。
keep選項
keep選項用來在混淆規則中聲明需要保留的類和類成員,防止它們被刪除和重命名。一般的格式如下:
-keep選項 class_specification
class_specification是類和成員的模板,用來指定應用keep規則的若干類及其成員.
根據能否在壓縮階段被刪除和在混淆階段被重命名,keep選項分爲兩類:
第一類,不帶names,不能被刪除、不能被重命名:-keep、-keepclassmembers、-keepclasseswithmembers,分別對應 同時保留類和類成員、只保留類成員、根類據成員找到滿足條件的所有類而不用指定類名,保留類名和成員名。
第二類,帶names,不能被重命名:-keepnames、-keepclassmembernames 、-keepclasseswithmembernames,分別對應 同時保留類和類成員不被重命名、只保留類成員不被重命名、根類據成員找到滿足條件的所有類而不用指定類名,保留類名和成員名不被重命名。對於第二類,如果類沒有被調用到,則在壓縮階段就會被刪除。
如圖:
class_specification
class_specification是類和成員的模板,用來指定應用keep規則的若干類及其成員。格式如下:
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
[extends|implements [@annotationtype] classname]
[{
[@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> |
(fieldtype fieldname);
[@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> |
<init>(argumenttype,...) |
classname(argumenttype,...) |
(returntype methodname(argumenttype,...));
[@annotationtype] [[!]public|private|protected|static ... ] *;
...
}]
解釋一下:
-
方括號“ []”表示其內容是可選的。 省略號“ …”表示可以指定任何數量的前述項目。 豎線“ |” 界定兩個選擇。 括號“()”僅將規範中屬於同一部分的部分分組。
-
class關鍵字是指任何接口或類。 interface關鍵字將匹配項限制爲接口類。 enum關鍵字將匹配項限制爲枚舉類。在接口或枚舉關鍵字之前加!將匹配分別限制爲不是接口或枚舉的類。
-
每個類名必須完全合格,例如java.lang.String。可以將類名指定爲包含以下通配符的正則表達式:
? 匹配類名稱中的任何單個字符,但不匹配包分隔符"."。
* 匹配除包分隔符之外的類名的任何部分。"mypackage.*" 與mypackage中的所有類匹配,但與子包中的所有類都不匹配。
** 匹配類名的任何部分,可能包含任意數量的包分隔符。
**.Test 匹配除根包以外的所有包中的所有Test類。 "mypackage.**" 與mypackage及其子包中的所有類匹配。
-
@annotationtype 可用於將類和類成員限制爲使用指定註釋類型進行註釋的成員。指定註釋類型就像類名一樣。字段和方法的指定與Java中的指定非常相似,註釋類型的方法參數列表不包含參數名稱。
-
類名* 表示任何類,無論其包如何。
-
< init> 匹配任何構造函數
< fields> 匹配任何字段
< methods> 匹配任何方法
* 匹配任何字段或方法。
請注意,上述通配符沒有返回類型。僅< init>通配符具有參數列表 -
字段和方法名稱可以包含以下通配符:
?匹配方法名稱中的任何單個字符。
* 匹配方法名稱的任何部分。 -
描述符中的類型可以包含以下通配符:
% 匹配任何原始類型(“ boolean”,“ int”等,但不匹配“ void”)。
? 匹配名稱中的任何單個字符。
* 與不包含包分隔符的名稱的任何部分匹配。
** 匹配類名的任何部分,可能包含任意數量的包分隔符。
*** 匹配任何類型(原始或非原始,數組或非數組)。
… 匹配任何類型的任意數量的參數。
1. 請注意,?,*和** 通配符永遠與基本類型不匹配。
2. 此外,只有*** 通配符可以匹配任何維度的數組類型。
例如,"**get*()"匹配"java.lang.Object getObject()",但不匹配" float getFloat()",也不匹配“”java.lang.Object [] getObjects()"。
3. 也可以使用構造函數的短類名(不帶包)或完整的類名來指定構造函數。與Java語言一樣,構造函數規範具有參數列表,但沒有返回類型。
4. 允許組合多個類成員訪問修飾符標誌(例如public static)。
ProGuard其他需要注意的事項
- 保留native方法
對於native方法,則需要保留它們的名稱和類的名稱,以便它們可以鏈接到本地庫:
-keepclasseswithmembernames class * {
native <methods>;
}
- 保留枚舉
如果程序代碼中包含枚舉類,則必須保留一些特殊方法。 Java 5中引入了枚舉。java編譯器將枚舉轉換爲具有特殊結構的類。 值得注意的是,這些類包含一些靜態方法的實現,運行時環境可以通過內省訪問。必須明確指定這些內容,以確保它們不會被刪除或混淆:
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
ProGuard的一些技術問題
使用ProGuard時,您應該注意一些技術問題,可以輕鬆避免或解決所有這些問題。ProGuard在處理代碼時,可能會打印出一些注意事項和非致命警告:
- Note: … calls ‘(…)Class.forName(variable).newInstance()’
ProGuard會列出動態創建的類實例的所有類強制轉換,例如“(MyClass)Class.forName(variable).newInstance()”。我們可能需要使用“ -keep class MyClass”之類的選項來保留所提及的類,或者使用“ -keep class * implements MyClass”之類的選項來保留其實現。
- Note: … accesses a field/method ‘…’ dynamically
ProGuard列出了許多構造,例如“ .getField(“ myField”)“。我們可能需要弄清楚所提到的類成員的定義位置,並使用“ -keep class MyClass {MyFieldType myField;}”之類的選項來保留它們。否則,ProGuard可能會刪除或混淆類成員。
-
優化導致的意外錯誤
通常是在優化步驟中ProGuard遇到了意外,可能不會恢復。可以使用-dontoptimize選項來避免這種情況 -
保留註釋類
如果要基於註釋保留類,則可能避免在壓縮步驟中刪除註釋類本身。您可以使用“ -keep @interface *”之類的選項將所有註釋類明確保留在程序代碼中。 -
ClassNotFoundException
代碼可能正在調用Class.forName,試圖動態創建缺少的類。 ProGuard只能檢測常量名稱參數,例如Class.forName(“ mypackage.MyClass”)。對於像Class.forName(someClass)這樣的變量名參數,您必須使用適當的-keep選項來保留所有可能的類,例如:
"-keep class mypackage.MyClass"
"-keep class * implements mypackage.MyInterface".
- NoSuchMethodException
代碼可能正在調用諸如myClass.getMethod之類的內容,試圖動態查找某些方法。而這些方法已經被混淆了。因此必須使用適當的-keep選項:
"-keep class mypackage.MyClass { void myMethod(); }"
更具體地說,如果報告爲丟失的方法是value或valueOf,則可能必須保留一些與枚舉有關的方法。
-
Disappearing annotations
默認情況下,混淆步驟將刪除所有註釋。如果您的應用程序依賴註釋來正常運行,則應使用"-keepattributes * Annotation *" 明確保留它們。 -
Disappearing loops
如果您的代碼包含空的繁忙等待循環,則ProGuard的優化步驟可能會將其刪除。如果與實際邏輯衝突,則必須使用-dontoptimize選項關閉優化。 -
ClassCastException: class not an enum, or
IllegalArgumentException: class not an enum type
應確保保留枚舉類型的特殊方法,運行時環境通過自省調用該方法。 -
ArrayStoreException: sun.reflect.annotation.EnumConstantNotPresentExceptionProxy
可能正在處理涉及枚舉的註釋。 同樣,您應該確保保留枚舉類型的特殊方法。 -
對於dex編譯器和Dalvik VM,預驗證是無關緊要的,因此我們可以使用-dontpreverify選項將其關閉。
-
-optimizations選項禁用Dalvik 1.0和1.5無法處理的某些算術簡化。Dalvik VM也無法處理(靜態字段)過度的過載。
總結一下,就是:
- 需要動態訪問的類或類成員,需要保留
- 慎用優化,甚至直接使用-dontoptimize禁用
- Android中預驗證無效,使用-dontpreverify將其關閉
- 根據需要保留註釋和枚舉
一份通用的ProGuard混淆文件
最後,提供一份較通用的ProGuard混淆文件參考。
我們保留了應用程序的AndroidManifest.xml文件可能引用的所有基本類。如果清單文件包含其他類和方法,可能還必須指定它們。
我們保留註釋,因爲它們可能由自定義RemoteView使用。
我們將使用典型的構造函數保留所有自定義View擴展和其他類,因爲它們可能是從XML佈局文件引用的。
我們還將所需的靜態字段保留在Parcelable或Serializable實現中,因爲可以通過自省訪問它們。
最後,我們保留了自動生成的R類的引用內部類的靜態字段,以使調用代碼通過自省訪問這些字段。
如果您使用的是Google的可選許可證驗證庫,則可以將其代碼與自己的代碼混淆。 您必須保留其ILicensingService接口以使庫正常工作:
-keep public interface com.android.vending.licensing.ILicensingService
如果您使用的是Android兼容性庫,則應添加以下行,以使ProGuard知道該庫引用了並非所有版本的API都可用的某些類:
-dontwarn android.support.**
“Exceptions”屬性必須保留,以使編譯器知道哪些方法可能引發異常。
僅當動態調用了其他任何非公共類或方法時,才應使用附加的-keep選項來指定它們。
對於可以從庫外部引用的任何內部類,也必須保留“ InnerClasses”屬性。否則,javac編譯器將無法找到內部類。
在JDK 5.0及更高版本中進行編譯時,必須具有“Signature”屬性才能訪問泛型。
最後,我們保留“ Deprecated”屬性和用於生成有用的堆棧跟蹤的屬性。
-keepattributes Exceptions,InnerClasses,Signature,Deprecated
此外,正規的第三方庫一般都會在接入文檔中寫好所需混淆規則,使用第三方庫時注意添加。
WebView中JavaScript調用的方法時,也需要保留。
Layout佈局使用的View構造函數、android:onClick等,也需要保留。
#指定要執行的優化遍數
-optimizationpasses 5
#混淆時不生成大小寫混合的類名,即全部小寫
-dontusemixedcaseclassnames
#指定不忽略非公共的庫的類
-dontskipnonpubliclibraryclasses
#指定不忽略包可見的庫類成員(字段和方法)。
-dontskipnonpubliclibraryclassmembers
#把混淆類中的方法名也混淆了
#爲需要混淆的類生成唯一的混淆名稱
-useuniqueclassmembernames
#關閉預驗證
-dontpreverify
# 打印過程日誌,在處理期間輸出更多信息
-verbose
#-dontshrink #禁用壓縮
#指定優化算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#關閉優化
-dontoptimize
#擴大類和類成員的訪問權限,使優化時允許訪問並修改有修飾符的類和類的成員
-allowaccessmodification
#四大組件和Application的子類不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
#如果使用的是Google的可選許可證驗證庫,則可以將其代碼與自己的代碼混淆。 必須保留其ILicensingService接口以使庫正常工作
-keep public interface com.android.vending.licensing.ILicensingService
#將混淆堆棧跟蹤文件來源重命名爲“SourceFile”
-renamesourcefileattribute SourceFile
#保護註解。如果代碼依賴註釋,則可能需要保留註釋,典型應用EventBus的事件接收回調
-keepattributes *Annotation*
#保留源文件名,變量名和行號,以產生有用的混淆堆棧跟蹤
-keepattributes SourceFile,LineNumberTable
#保留異常,內部類/接口,泛型,Deprecated不推薦的方法
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,EnclosingMethod
#如果引用了v4或者v7包, 不報警告,使ProGuard知道該庫引用了並非所有版本的API都可用的某些類
-dontwarn android.support.**
#保留native方法
-keepclasseswithmembernames class * {
native <methods>;
}
#保留自定義View的類及構造函數,以使它們可以被XML佈局文件引用
-keep class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#保留自定義View的get和set相關方法
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
#保持Activity中View及其子類爲入參的方法,比如android:onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
#保留符合指定構造函數類型的自定義控件類,如果和下面的寫在一起,那麼只有同時有這兩類構造函數的類才滿足
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#保留R文件的靜態成員,以使調用代碼通過自省訪問這些字段
-keepclassmembers class **.R$* {
public static <fields>;
}
#保留枚舉
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#保留實現了Parcelable接口的類中的靜態成員
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#保持所有實現Serializable接口的類成員
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
#Fragment不需要在AndroidManifest.xml中註冊,需要額外保護下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment
#指定不混淆指定的包名稱
-keeppackagenames com.milanac007.*
#指定包名下的文件都保留
-keep class com.milanac007..blecommsdk.**{*;}